Payments
BD Voucher orchestrates three independent payment gateways behind a single unified API. From a client’s point of view the flow is always the same:
- Create the order (
POST /api/v1/orders). - Get / initiate a payment session.
- Redirect the customer to the returned
paymentGatewayUrl. - The gateway calls back to BD Voucher; we re-verify and mark the
payment
SUCCEEDED/FAILED/CANCELLED. - Client polls
GET /api/v1/payments/status/:transactionId(or waits for the redirect) and renders the result.
Two valid integration patterns
A. Recommended: inline payment init from POST /orders
POST /api/v1/orders accepts payment_method and, by default,
init_payment=true. That means the order response already contains a payment
session payload and gateway redirect URL.
POST /api/v1/ordersAuthorization: Bearer $TOKENContent-Type: application/json
{ "items": [{ "variant_id": 22402, "quantity": 1 }], "payment_method": "bkash"}Use this when you want a one-call checkout flow.
B. Explicit two-step flow
If you prefer to create the order first and initiate payment later, send:
{ "items": [{ "variant_id": 22402, "quantity": 1 }], "payment_method": "bkash", "init_payment": false}Then call either:
POST /api/v1/payments/initiate/:orderId, orPOST /api/v1/payments/initiatewithorderIdin the body.
Supported methods
method | Gateway | Customer-facing options |
|---|---|---|
sslcommerz | SSLCommerz | Cards, internet banking, MFS aggregator |
bkash | bKash Tokenized Checkout (v1.2.0-beta) | bKash wallet only |
paystation | PayStation | Multi-MFS (bKash, Nagad, Rocket, cards…) |
dcb | Direct Carrier Billing | Operator (Robi/GP) postpaid/prepaid bill |
nagad | Nagad direct | Nagad wallet |
1. Initiate payment explicitly
POST /api/v1/payments/initiate/12345Authorization: Bearer $TOKENContent-Type: application/json
{ "amount": 500, "currency": "BDT", "method": "bkash", "description": "Order #12345"}Response
{ "success": true, "message": "Payment session created", "data": { "transactionId": "BDV-12345-1730000000000-abcdef12", "paymentGatewayUrl": "https://tokenized.sandbox.bka.sh/.../execute?paymentID=TR0011...", "method": "bkash" }}Redirect the customer’s browser to paymentGatewayUrl. The order must be in
INITIATED, PENDING, FAILED, or CANCELLED status — otherwise the call
returns 400 Bad Request.
If you used inline initiation from POST /orders, you can skip this step and
redirect immediately using the payment.paymentGatewayUrl from the order-create
response.
2. Callbacks (per gateway)
Every callback is re-verified server-side before we trust the result.
We never mark a payment SUCCEEDED based purely on the redirect.
SSLCommerz
| Verb | Path |
|---|---|
POST | /api/v1/payments/sslcommerz/callback |
We call SSLCommerz’s validate API (or transactionQueryByTransactionId
when val_id is missing). Accepted statuses: VALID, VALIDATED,
VALID_AND_CONFIRMED. Anything else → FAILED (or CANCELLED if the
callback says so).
bKash
| Verb | Path |
|---|---|
GET/POST | /api/v1/payments/bkash/callback |
Query / body fields: status (success / cancel / failure) and
paymentID. On success we call bKash executePayment; only
transactionStatus === "Completed" is accepted. We retry once on a
transient Initiated / PENDING response.
PayStation
| Verb | Path |
|---|---|
GET/POST | /api/v1/payments/paystation/callback |
PayStation POSTs form-data with at least invoice_number. We always
re-verify via PayStation’s /transaction-status endpoint before marking
the payment SUCCEEDED. processing triggers a single retry after 3 s.
3. Polling status
GET /api/v1/payments/status/BDV-12345-1730000000000-abcdef12{ "success": true, "data": { "transactionId": "BDV-12345-1730000000000-abcdef12", "status": "SUCCEEDED", "orderId": 12345, "orderNumber": "12345678", "orderStatus": "PENDING", "orderStatusLabel": "Payment received", "orderStatusMessage": "Payment confirmed — we are preparing your voucher delivery.", "orderStatusSeverity": "info", "createdAt": "2026-04-28T10:00:00.000Z" }}Use this for the post-redirect polling loop (every 2-3 s, max ~30 s).
Once status leaves PENDING you can stop polling and route the
user based on orderStatus.
PayStation explicit confirmation endpoint
If your frontend lands on the PayStation browser-return URL and you want to force an immediate server-side recheck before rendering, call:
GET /api/v1/payments/paystation/confirm/:invoiceNumberThis endpoint re-verifies against PayStation /transaction-status, applies the
same state-transition rules used by callbacks, and returns the normalized
payment + order status payload.
Idempotency
All callback endpoints are safe to retry. A payment that is already
SUCCEEDED will short-circuit subsequent verification calls (we’ll log
the duplicate and return success). Order fulfillment uses a Redis lock
keyed by order_id to prevent double-fulfillment under concurrent
callbacks.
Failure modes
| Situation | Behaviour |
|---|---|
| Gateway init returns no URL | 502 Bad Gateway, payment row marked FAILED |
| Customer cancels at gateway | Payment CANCELLED, order CANCELLED |
| Gateway returns failure | Payment FAILED, order FAILED |
| Verification API down | One retry, then FAILED; admin alert emitted |
| Fulfillment fails post-payment | Order → NEEDS_ATTENTION, see Fulfillment recovery |
See the API reference for full request/response schemas, and the webhooks guide for the events we emit downstream.