Skip to content

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:

  1. Create the order (POST /api/v1/orders).
  2. Get / initiate a payment session.
  3. Redirect the customer to the returned paymentGatewayUrl.
  4. The gateway calls back to BD Voucher; we re-verify and mark the payment SUCCEEDED / FAILED / CANCELLED.
  5. 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/orders
Authorization: Bearer $TOKEN
Content-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, or
  • POST /api/v1/payments/initiate with orderId in the body.

Supported methods

methodGatewayCustomer-facing options
sslcommerzSSLCommerzCards, internet banking, MFS aggregator
bkashbKash Tokenized Checkout (v1.2.0-beta)bKash wallet only
paystationPayStationMulti-MFS (bKash, Nagad, Rocket, cards…)
dcbDirect Carrier BillingOperator (Robi/GP) postpaid/prepaid bill
nagadNagad directNagad wallet

1. Initiate payment explicitly

POST /api/v1/payments/initiate/12345
Authorization: Bearer $TOKEN
Content-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

VerbPath
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

VerbPath
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

VerbPath
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/:invoiceNumber

This 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

SituationBehaviour
Gateway init returns no URL502 Bad Gateway, payment row marked FAILED
Customer cancels at gatewayPayment CANCELLED, order CANCELLED
Gateway returns failurePayment FAILED, order FAILED
Verification API downOne retry, then FAILED; admin alert emitted
Fulfillment fails post-paymentOrder → NEEDS_ATTENTION, see Fulfillment recovery

See the API reference for full request/response schemas, and the webhooks guide for the events we emit downstream.