رفتن به محتوای اصلی

صفحهٔ پرداخت و بازگشت به سایت

مشتری در checkout_url چه می‌بیند، دو زبانهٔ پرداخت و مرحلهٔ رسید، API صفحهٔ پرداخت و اینکه امضای لینک بازگشت را چطور بررسی کنید.

هر پرداخت یک checkout_url روی bezanbecard.com/pay/… دارد. مشتری را به آن بفرستید و بقیه با بزن به کارت: مبلغ و جای پرداختش را نشان می‌دهد، رسید مشتری را می‌گیرد، پرداخت را زنده دنبال می‌کند و در پایان مشتری را به سایت شما برمی‌گرداند. صفحه روی هر گوشی، به فارسی و انگلیسی و در حالت روشن و تیره کار می‌کند.

مشتری چه می‌بیند

  1. نام و لوگوی کسب‌وکار شما، شمارهٔ سفارش و شمارش معکوس تا expires_at.
  2. مبلغ دقیق واریز به تومان و زیرش رقم دقیق ریالی. این همان expected_amount است که مخصوص همین پرداخت است، مگر اینکه مبلغ یکتا را خاموش کرده باشید.
  3. دو زبانه درست بالای مقصد، کارت به کارت و انتقال بانکی، وقتی پرداخت هر دو روش را بپذیرد؛ و فقط یک زبانه وقتی یک روش را بپذیرد. هر وقت پرداخت کارت را بپذیرد، زبانهٔ کارت اول باز می‌شود.
  4. جای پرداخت. زبانهٔ کارت بانک، شمارهٔ کامل کارت و نام دارنده را نشان می‌دهد. زبانهٔ انتقال بانکی شبا را در دو سطر، نام دارنده و بانک، و انواع انتقالی را که مشتری می‌تواند به کار ببرد (پل، پایا، ساتنا یا انتقال داخلی همان بانک) با یک توضیح کوتاه و محتاطانه برای هر کدام نشان می‌دهد. هر دو زبانه دکمه‌هایی برای کپی شماره و مبلغ دارند.
  5. مرحلهٔ رسید. مشتری بعد از پرداخت تا سه فایل (عکس، اسکرین‌شات یا PDF رسید بانک) بارگذاری می‌کند و «فرستادن رسید» را می‌زند. در زبانهٔ انتقال بانکی می‌گوید کدام نوع انتقال را انجام داده است؛ هیچ گزینه‌ای از پیش انتخاب نشده است. می‌تواند کد پیگیری رسید را هم بنویسد.
  6. هر کدام از نام، شمارهٔ موبایل و ایمیل که در تنظیمات صفحهٔ پرداخت خواسته‌اید و پرداخت هنوز ندارد، و اگر خواسته باشید چهار رقم آخر کارت پرداخت‌کننده (به‌طور پیش‌فرض خاموش است، چون رسید خودش کارت را نشان می‌دهد).

فایل رسید به‌طور پیش‌فرض الزامی است. اگر در تنظیمات صفحهٔ پرداخت خاموشش کنید، مشتری می‌تواند بدون فایل «فرستادن رسید» را بزند؛ آن وقت تیم شما حساب بانکی را خودش نگاه می‌کند، یا اپ دریافت پیامک که اختیاری است واریز را پیدا می‌کند.

بعد از «فرستادن رسید» صفحه نشان می‌دهد که پرداخت منتظر تأیید شماست، همراه با هر رسیدی که مشتری فرستاده و وضعیتش. همان لحظه‌ای که تیم شما تصمیم بگیرد، صفحه خودش به‌روز می‌شود. رسید ردشده دلیلش را نشان می‌دهد و مشتری می‌تواند رسید دیگری بفرستد. تأیید بخشی از مبلغ یک نوار پیشرفت نشان می‌دهد با مبلغی که رسیده و یک مبلغ یکتای تازه برای بقیه. پرداخت موفق پیام موفقیت نشان می‌دهد و اگر از لینک پرداخت آمده باشد، پیامی را هم که روی لینک گذاشته‌اید. پرداخت آزمایشی یک نوار مشخص دارد تا کسی آن را با پرداخت واقعی اشتباه نگیرد.

شمارهٔ کارت و شبا فقط تا وقتی نشان داده می‌شوند که پرداخت منتظر واریز است. وقتی موفق شد، بسته شد یا مهلت اضافه‌اش تمام شد، صفحه فقط بانک و چهار رقم آخر را نشان می‌دهد.

پیشنهاد، نه قاعده

سقف مبلغ و ساعت کار کارت‌به‌کارت، پل، پایا و ساتنا را بانک‌ها و بانک مرکزی تعیین می‌کنند و بی‌خبر تغییر می‌دهند. صفحهٔ پرداخت به خاطر آن‌ها هیچ چیزی را پنهان، غیرفعال یا رد نمی‌کند، چیزی را از پیش انتخاب نمی‌کند و هیچ سقف یا ساعتی را اعلام نمی‌کند. وقتی مبلغی برای کارت‌به‌کارت زیاد به نظر برسد، زبانهٔ کارت با ملایمت می‌گوید شاید انتقال بانکی راحت‌تر باشد، و زبانهٔ انتقال بانکی نوع‌هایی را که شاید به این مبلغ بخورند «پیشنهادی» علامت می‌زند. تصمیم با بانک مشتری است. انتقال بانکی را ببینید.

بازگشت به سایت شما

بازگشت به سایت شما
مشتری از کجا خارج می‌شودبه کجا می‌رود
پیام موفقیت پرداختreturn_url
هر وضعیت دیگری (منصرف شده، پرداخت منقضی یا لغو شده)cancel_url، و اگر cancel_url نگذاشته باشید return_url

بزن به کارت سه پارامتر به این نشانی اضافه می‌کند و هر پارامتری را که خودتان در نشانی گذاشته‌اید نگه می‌دارد:

Text
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_typescard، transfer یا هر دو: کدام زبانه‌ها را نشان دهید.
destination.typecard (کارت بانکی) یا account (شبای بدون کارت).
destination.panشمارهٔ کامل کارت، یا null برای حساب و برای پرداختی که کارت‌به‌کارت را نمی‌پذیرد. فقط با کلید مخفی.
destination.ibanشبای کامل، یا null وقتی پرداخت انتقال بانکی را نمی‌پذیرد یا مقصد شبا ندارد. فقط با کلید مخفی.
destination.bank، destination.last4، destination.iban_last4، destination.holder_nameکد بانک، چهار رقم آخر کارت و شبا، و نام دارنده.
expires_atزمان بسته شدن صفحهٔ پرداخت به ثانیهٔ یونیکس.

کلید محدود و وب‌هوک هرگز pan و iban را ندارند. هر دو را از لاگ‌ها و ابزارهای تحلیل دور نگه دارید.

بعد رسید را بگیرید و از سرورتان با POST /v1/payments/:id/receipts بفرستید (رسیدها را ببینید)، یا مستقیم از مرورگر مشتری به API صفحهٔ پرداخت در پایین که کلید نمی‌خواهد. نتیجه را از وب‌هوک بگیرید و وقتی پرداخت کم‌واریزpartially_paid شد، expected_amount تازه را برای باقی‌مانده نشان دهید.

API صفحهٔ پرداخت

صفحهٔ میزبانی‌شده روی یک API کوچک و عمومی ساخته شده که با توکن انتهای checkout_url کار می‌کند. کلید API نمی‌خواهد و به هر مبدأیی پاسخ می‌دهد؛ پس صفحهٔ خودتان می‌تواند بدون لو دادن هیچ رازی از مرورگر مشتری به آن وصل شود.

Text
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. هیچ‌وقت به مبلغ بستگی ندارد.
channelspol، 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 /checkout/<token>/receipts بدنهٔ multipart/form-data می‌گیرد:

فرستادن رسید
فیلدتوضیح
filesتا ۳ فایل و هر کدام حداکثر ۱۰ مگابایت: JPEG، PNG، WebP، HEIC یا PDF که از روی محتوای فایل شناخته می‌شوند، نه از روی نامش. وقتی receipt_required باشد الزامی است.
channelcard، pol، paya، satna یا internal. وقتی پرداخت کارت را بپذیرد پیش‌فرض card است؛ وگرنه الزامی است.
referenceکد پیگیری روی رسید: ۴ تا ۴۰ حرف و رقم. رقم فارسی هم پذیرفته می‌شود.
amountریالی که مشتری می‌گوید فرستاده است. برای همان مبلغ مورد انتظار نفرستیدش.
paid_atزمان پرداخت به ISO 8601.
payer_name، payer_last4اختیاری. وقتی payer_required باشد، payer_last4 در کانال کارت الزامی است.
name، phone، emailمشخصه‌هایی از collect که پرداخت هنوز لازم دارد.
replacetrue وقتی این رسید جای رسید در انتظار قبلی مشتری را می‌گیرد و واریز دومی نیست.
curl
curl https://a.bezanbecard.com/checkout/<token>/receipts \
  -F channel=paya \
  -F reference=140508041234 \
  -F [email protected]

پرداخت processing می‌شود و رسید در صف بررسی شما منتظر می‌ماند. ردها:

فرستادن رسید
وضعیتکدکِی
409checkout.closedصفحهٔ پرداخت دیگر رسید نمی‌گیرد؛ payload.status دلیلش را می‌گوید.
400checkout.field_requiredpayload.param می‌گوید چه چیزی کم است: files، channel، payer_last4، name، phone یا email.
400checkout.channel_unavailableپرداخت این نوع انتقال را نمی‌پذیرد.
400file.too_many، file.invalid_type، file.invalid_uploadبیش از سه فایل، فایلی که عکس یا PDF نیست، یا بدنه‌ای که خوانده نمی‌شود (فایل‌ها را در files بفرستید).
413file.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" رد می‌شود. سقف درخواستش با رسیدها مشترک است.