صفحهٔ پرداخت و بازگشت به سایت
مشتری در checkout_url چه میبیند، دو زبانهٔ پرداخت و مرحلهٔ رسید، API صفحهٔ پرداخت و اینکه امضای لینک بازگشت را چطور بررسی کنید.
هر پرداخت یک checkout_url روی bezanbecard.com/pay/… دارد. مشتری را به آن بفرستید و بقیه با بزن به کارت: مبلغ و جای پرداختش را نشان میدهد، رسید مشتری را میگیرد، پرداخت را زنده دنبال میکند و در پایان مشتری را به سایت شما برمیگرداند. صفحه روی هر گوشی، به فارسی و انگلیسی و در حالت روشن و تیره کار میکند.
مشتری چه میبیند
- نام و لوگوی کسبوکار شما، شمارهٔ سفارش و شمارش معکوس تا
expires_at. - مبلغ دقیق واریز به تومان و زیرش رقم دقیق ریالی. این همان
expected_amountاست که مخصوص همین پرداخت است، مگر اینکه مبلغ یکتا را خاموش کرده باشید. - دو زبانه درست بالای مقصد، کارت به کارت و انتقال بانکی، وقتی پرداخت هر دو روش را بپذیرد؛ و فقط یک زبانه وقتی یک روش را بپذیرد. هر وقت پرداخت کارت را بپذیرد، زبانهٔ کارت اول باز میشود.
- جای پرداخت. زبانهٔ کارت بانک، شمارهٔ کامل کارت و نام دارنده را نشان میدهد. زبانهٔ انتقال بانکی شبا را در دو سطر، نام دارنده و بانک، و انواع انتقالی را که مشتری میتواند به کار ببرد (پل، پایا، ساتنا یا انتقال داخلی همان بانک) با یک توضیح کوتاه و محتاطانه برای هر کدام نشان میدهد. هر دو زبانه دکمههایی برای کپی شماره و مبلغ دارند.
- مرحلهٔ رسید. مشتری بعد از پرداخت تا سه فایل (عکس، اسکرینشات یا PDF رسید بانک) بارگذاری میکند و «فرستادن رسید» را میزند. در زبانهٔ انتقال بانکی میگوید کدام نوع انتقال را انجام داده است؛ هیچ گزینهای از پیش انتخاب نشده است. میتواند کد پیگیری رسید را هم بنویسد.
- هر کدام از نام، شمارهٔ موبایل و ایمیل که در تنظیمات صفحهٔ پرداخت خواستهاید و پرداخت هنوز ندارد، و اگر خواسته باشید چهار رقم آخر کارت پرداختکننده (بهطور پیشفرض خاموش است، چون رسید خودش کارت را نشان میدهد).
فایل رسید بهطور پیشفرض الزامی است. اگر در تنظیمات صفحهٔ پرداخت خاموشش کنید، مشتری میتواند بدون فایل «فرستادن رسید» را بزند؛ آن وقت تیم شما حساب بانکی را خودش نگاه میکند، یا اپ دریافت پیامک که اختیاری است واریز را پیدا میکند.
بعد از «فرستادن رسید» صفحه نشان میدهد که پرداخت منتظر تأیید شماست، همراه با هر رسیدی که مشتری فرستاده و وضعیتش. همان لحظهای که تیم شما تصمیم بگیرد، صفحه خودش بهروز میشود. رسید ردشده دلیلش را نشان میدهد و مشتری میتواند رسید دیگری بفرستد. تأیید بخشی از مبلغ یک نوار پیشرفت نشان میدهد با مبلغی که رسیده و یک مبلغ یکتای تازه برای بقیه. پرداخت موفق پیام موفقیت نشان میدهد و اگر از لینک پرداخت آمده باشد، پیامی را هم که روی لینک گذاشتهاید. پرداخت آزمایشی یک نوار مشخص دارد تا کسی آن را با پرداخت واقعی اشتباه نگیرد.
شمارهٔ کارت و شبا فقط تا وقتی نشان داده میشوند که پرداخت منتظر واریز است. وقتی موفق شد، بسته شد یا مهلت اضافهاش تمام شد، صفحه فقط بانک و چهار رقم آخر را نشان میدهد.
پیشنهاد، نه قاعده
سقف مبلغ و ساعت کار کارتبهکارت، پل، پایا و ساتنا را بانکها و بانک مرکزی تعیین میکنند و بیخبر تغییر میدهند. صفحهٔ پرداخت به خاطر آنها هیچ چیزی را پنهان، غیرفعال یا رد نمیکند، چیزی را از پیش انتخاب نمیکند و هیچ سقف یا ساعتی را اعلام نمیکند. وقتی مبلغی برای کارتبهکارت زیاد به نظر برسد، زبانهٔ کارت با ملایمت میگوید شاید انتقال بانکی راحتتر باشد، و زبانهٔ انتقال بانکی نوعهایی را که شاید به این مبلغ بخورند «پیشنهادی» علامت میزند. تصمیم با بانک مشتری است. انتقال بانکی را ببینید.
بازگشت به سایت شما
| مشتری از کجا خارج میشود | به کجا میرود |
|---|---|
| پیام موفقیت پرداخت | return_url |
| هر وضعیت دیگری (منصرف شده، پرداخت منقضی یا لغو شده) | cancel_url، و اگر cancel_url نگذاشته باشید return_url |
بزن به کارت سه پارامتر به این نشانی اضافه میکند و هر پارامتری را که خودتان در نشانی گذاشتهاید نگه میدارد:
https://shop.example/orders/1042/thanks?payment=pay_8xkq2LmZr0Pa1Bcd&status=succeeded&signature=3f0c…مشتریای که وقتی رسیدش منتظر بررسی شماست صفحه را ترک کند با status=processing برمیگردد: به او نشان دهید سفارش منتظر تأیید است و سفارش را از روی وبهوک بهروز کنید.
بررسی امضا
signature همان HMAC-SHA256 رشتهٔ <payment id>.<status> بهصورت hex با حروف کوچک است و کلیدش کلید امضای بازگشت شماست (rds_…). هر حالت کلید خودش را دارد. آن را از توسعهدهندگان ← کلیدهای API ← کلید امضای بازگشت ببینید؛ افزونههای فروشگاهی هنگام اتصال خودشان آن را میگیرند.
مقایسه را در زمان ثابت انجام دهید و لینکی را که امضایش درست نیست رد کنید.
import crypto from 'node:crypto';
function verifyReturn(query, secret) {
const [payment, status, signature] = ['payment', 'status', 'signature'].map((name) => String(query[name] ?? ''));
const expected = crypto.createHmac('sha256', secret).update(`${payment}.${status}`).digest('hex');
return Buffer.byteLength(signature) === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// Express: verifyReturn(req.query, process.env.BEZANBECARD_REDIRECT_SECRET)GET /v1/payments/:id بخوانید (یا به وبهوک payment.succeeded تکیه کنید) و status و amount_received آن را بررسی کنید.ساختن صفحهٔ پرداخت خودتان
میتوانید پرداخت را در صفحهٔ خودتان نشان دهید. با کلید مخفی، پرداخت هر چیزی را که مشتری لازم دارد همراه دارد:
| فیلد | توضیح |
|---|---|
expected_amount | مبلغ دقیق واریز به ریال. آن را با رقم ریالی نشان دهید و هرگز گرد نکنید. |
payment_method_types | card، transfer یا هر دو: کدام زبانهها را نشان دهید. |
destination.type | card (کارت بانکی) یا account (شبای بدون کارت). |
destination.pan | شمارهٔ کامل کارت، یا null برای حساب و برای پرداختی که کارتبهکارت را نمیپذیرد. فقط با کلید مخفی. |
destination.iban | شبای کامل، یا null وقتی پرداخت انتقال بانکی را نمیپذیرد یا مقصد شبا ندارد. فقط با کلید مخفی. |
destination.bank، destination.last4، destination.iban_last4، destination.holder_name | کد بانک، چهار رقم آخر کارت و شبا، و نام دارنده. |
expires_at | زمان بسته شدن صفحهٔ پرداخت به ثانیهٔ یونیکس. |
کلید محدود و وبهوک هرگز pan و iban را ندارند. هر دو را از لاگها و ابزارهای تحلیل دور نگه دارید.
بعد رسید را بگیرید و از سرورتان با POST / بفرستید (رسیدها را ببینید)، یا مستقیم از مرورگر مشتری به API صفحهٔ پرداخت در پایین که کلید نمیخواهد. نتیجه را از وبهوک بگیرید و وقتی پرداخت کمواریزpartially_paid شد، expected_amount تازه را برای باقیمانده نشان دهید.
API صفحهٔ پرداخت
صفحهٔ میزبانیشده روی یک API کوچک و عمومی ساخته شده که با توکن انتهای checkout_url کار میکند. کلید API نمیخواهد و به هر مبدأیی پاسخ میدهد؛ پس صفحهٔ خودتان میتواند بدون لو دادن هیچ رازی از مرورگر مشتری به آن وصل شود.
GET https://a.bezanbecard.com/checkout/<token> the view model
POST https://a.bezanbecard.com/checkout/<token>/receipts send a receipt (multipart)
POST https://a.bezanbecard.com/checkout/<token>/claim a receipt without a file (JSON)
GET https://a.bezanbecard.com/checkout/<token>/stream server-sent events named "status"پاسخ هر کدام مدل نمایش است. توکن ناشناخته یا بدشکل 404 با checkout.not_found میگیرد. ردها یک وضعیت HTTP و یک کد نقطهدارند، مثل { "message": "checkout.closed", "payload": { "status": "succeeded" } }، و شکل شیء error در /v1 را ندارند.
مدل نمایش
زمانها ISO 8601 هستند. مدل نمایش هیچوقت شناسهٔ پرداخت (جز در لینکهای بازگشت امضاشده)، شناسهٔ رسید، فایل یا هش را ندارد.
| فیلد | توضیح |
|---|---|
status | وضعیت پرداخت. processing یعنی رسیدی منتظر بررسی فروشنده است. |
amount.requested، amount.received، amount.remaining | به ریال. |
amount.expected | مبلغ دقیق واریز؛ وقتی پرداخت بسته شود یا مهلتش بگذرد null است. وقتی رسیدی منتظر بررسی است (processing) یا پرداخت در requires_review است هم پر میماند، پس واریز را فقط برای requires_payment و partially_paid و تا وقتی claimable درست است نشان دهید، همانطور که صفحهٔ پرداخت میزبانیشده نشان میدهد. |
methods، default_method | روشهای پرداخت و زبانهای که اول باز میشود: هر وقت پرداخت کارت را بپذیرد card. هیچوقت به مبلغ بستگی ندارد. |
channels | pol، paya، satna و internal وقتی پرداخت انتقال بانکی را میپذیرد و مبلغی انتظار دارد، وگرنه خالی. هیچوقت بر اساس مبلغ یا زمان فیلتر نمیشود. |
suggestion | { method, channels } یا null: یک پیشنهاد ملایم برای این مبلغ. آرام نشانش دهید و به خاطرش هیچ چیزی را پنهان، غیرفعال یا از پیش انتخاب نکنید. |
destination | { kind, bank, holder, pan, last4, iban, iban_last4 }. pan فقط برای کارت، تا وقتی پرداخت باز است و کارت را میپذیرد؛ iban فقط تا وقتی پرداخت باز است و انتقال بانکی را میپذیرد. |
waiting | { channel } در وضعیت processing: مشتری گفته از چه راهی پرداخت کرده است. |
claimable، claimable_until | اینکه رسید الان پذیرفته میشود یا نه، و تا کِی. |
receipt_required | اینکه فایل رسید لازم است یا نه. |
receipts | رسیدهای مشتری از قدیم به جدید: status، channel، amount، created_at و برای رسید ردشده rejection.reason. |
payer_required | اینکه چهار رقم آخر کارت پرداختکننده در کانال کارت لازم است یا نه. |
collect | { name, phone, email }: برای هر مشخصهای که پرداخت هنوز لازم دارد true. |
review | { reason } وقتی پرداخت در بررسی است: no_sms، amount_mismatch، partial_disabled، expired_partial یا null. |
expires_at، server_time | شمارش معکوس را از server_time حساب کنید، هرگز از ساعت دستگاه. |
return_url، cancel_url | لینکهای بازگشت امضاشده. |
overpayment | { status, amount }: اگر فروشنده بدهی برگشتی داشته باشد. |
merchant، reference، description، message، locale، unit، livemode | فروشنده، سفارش، پیام لینک بعد از موفقیت، زبان صفحه، تومان یا ریال و حالت. |
وقتی مبلغ یکتا خاموش است، amount.expected با amount.requested برابر است؛ پس رقمی نیست که به مشتری گوشزد شود.
فرستادن رسید
POST / بدنهٔ multipart/form-data میگیرد:
| فیلد | توضیح |
|---|---|
files | تا ۳ فایل و هر کدام حداکثر ۱۰ مگابایت: JPEG، PNG، WebP، HEIC یا PDF که از روی محتوای فایل شناخته میشوند، نه از روی نامش. وقتی receipt_required باشد الزامی است. |
channel | card، pol، paya، satna یا internal. وقتی پرداخت کارت را بپذیرد پیشفرض card است؛ وگرنه الزامی است. |
reference | کد پیگیری روی رسید: ۴ تا ۴۰ حرف و رقم. رقم فارسی هم پذیرفته میشود. |
amount | ریالی که مشتری میگوید فرستاده است. برای همان مبلغ مورد انتظار نفرستیدش. |
paid_at | زمان پرداخت به ISO 8601. |
payer_name، payer_last4 | اختیاری. وقتی payer_required باشد، payer_last4 در کانال کارت الزامی است. |
name، phone، email | مشخصههایی از collect که پرداخت هنوز لازم دارد. |
replace | true وقتی این رسید جای رسید در انتظار قبلی مشتری را میگیرد و واریز دومی نیست. |
curl https://a.bezanbecard.com/checkout/<token>/receipts \
-F channel=paya \
-F reference=140508041234 \
-F [email protected]پرداخت processing میشود و رسید در صف بررسی شما منتظر میماند. ردها:
| وضعیت | کد | کِی |
|---|---|---|
| 409 | checkout.closed | صفحهٔ پرداخت دیگر رسید نمیگیرد؛ payload.status دلیلش را میگوید. |
| 400 | checkout.field_required | payload.param میگوید چه چیزی کم است: files، channel، payer_last4، name، phone یا email. |
| 400 | checkout.channel_unavailable | پرداخت این نوع انتقال را نمیپذیرد. |
| 400 | file.too_many، file.invalid_type، file.invalid_upload | بیش از سه فایل، فایلی که عکس یا PDF نیست، یا بدنهای که خوانده نمیشود (فایلها را در files بفرستید). |
| 413 | file.too_large | فایلی بزرگتر از ۱۰ مگابایت است. |
| 429 | رسید بیش از حد: ساعتی ۱۰ رسید برای هر پرداخت و ۳۰ رسید برای هر بازدیدکننده، یا سرور سرگرم خواندن فایلهای دیگر است. به اندازهٔ Retry-After صبر کنید. |
رسید بدون فایل
POST /checkout/<token>/claim بدنهٔ JSON به شکل { channel?, reference?, last4?, name?, phone?, email? } میگیرد و رسیدی بدون فایل ثبت میکند. last4 مال کانال کارت و reference مال کانالهای انتقال بانکی است. تا وقتی receipt_required باشد با checkout.field_required و param: "files" رد میشود. سقف درخواستش با رسیدها مشترک است.