1. Authentication and Security
- Requirements
- Headers
- Signature
- Auth errors
| Requirement | Description |
|---|---|
| HTTPS | Send API requests over HTTPS. |
| JSON | Requests and responses use JSON. |
| API key | Use an enabled merchant API key with v2 access and its signing secret. |
| IP whitelist | Use an allowed request source IP. |
| API prefix | External v2 endpoints use /api/v2. |
| Money format | Positive JSON numbers with at most two decimal places, for example 500.25. |
| Changelog | Contract updates are recorded in the v2 changelog. |
BDT Merchant API v2 creates deposits (PayIn) and withdrawals (PayOut), and retrieves an individual transaction status. The API base path is /api/v2. This guide uses BDT and an active BDT shop in its examples; use your assigned shop code as method. Use HTTPS and JSON. Ask your integration contact to enable v2 for your API key, provide its signing secret, configure the allowed source IPs, and confirm your active shop codes and their fields schemas.
Confirm with your integration contact that the revised v2 contract is available in your environment and enabled for each intended shop, currency, and payment direction. Agree on the required customer fields, available payer-account data, callback address, and PayIn return-page configuration before live payments.
For POST, send a JSON object in the body and no query string. Query parameters are rejected with HTTP 400, including parameters that repeat signed body fields. A JSON array, scalar, or malformed JSON is also rejected. Send your signing secret only to your own signing implementation; never include it in the request or a browser URL.
Required headers
| Header | When | Value |
|---|---|---|
X-Api-Key | Every request | Your enabled merchant API key. Header names are case-insensitive. |
X-Signature | Every request | Lowercase hexadecimal HMAC-SHA256 of the raw request body, using the API key's signing secret. |
Content-Type | POST requests | application/json. |
| Step | Description |
|---|---|
| 1 | Take the exact raw JSON request body for POST, or an empty string for GET. |
| 2 | Compute HMAC-SHA256 with the API signing secret and encode it as lowercase hexadecimal. |
| 3 | Send the result in X-Signature; send the same body bytes you signed. |
For GET requests, sign the empty string. The URL, path, query, timestamp, and HTTP method are not part of the v2 signature. v2 does not require X-Timestamp or X-Request-Id. Keep the exact bytes used to calculate the signature: reformatting the JSON after signing changes the signature.
X-Signature = hex(HMAC-SHA256(api_secret, raw_body))
GET raw_body = ""
| HTTP | Cause | Action |
|---|---|---|
| 401 | Missing, inactive, or v2-disabled API key; missing or invalid signature; disallowed source IP. | Verify the key, raw-body HMAC and source-IP allowlist. |
See API errors for the v2 JSON error envelope.
Signature examples
- POST
- GET (standard)
Pass the complete create-request JSON as rawBody; send those same bytes after signing.
import { createHmac } from 'node:crypto';
function v2Signature(rawBody, apiSecret) {
return createHmac('sha256', apiSecret).update(rawBody, 'utf8').digest('hex');
}
// Sign and send the same raw JSON bytes for POST; sign '' for GET.
Sign an empty string and send no request body. The URL is not part of the signature.
import { createHmac } from 'node:crypto';
const signature = createHmac('sha256', apiSecret).update('', 'utf8').digest('hex');
Version boundaries
v2 uses order_id, method, fields, and a JSON number for amount. The v1 names external_id, shop_code, and paymentData, its decimal-string amount and timestamp-based signature are different contracts. Unknown top-level create fields are rejected.
method is the exact code of one of your active shops. If omitted, your default active shop is used. fields is a JSON object validated against the selected shop's configured PayIn or PayOut schema. See the shop fields contract and confirm the schema for your shop code and direction before sending a request.
For BDT migration, use the v1-to-v2 request-field mapping. In particular, the deposit mobile number moves to fields.mobile_number. Arbitrary string customer IDs and the v1 customer IP do not currently have equivalent v2 inputs; sharing payment processing does not make the request fields interchangeable.
Payment flow
- Create a deposit or withdrawal with a stable
order_idand store the returnedtransaction_id. - For a deposit, send the customer to the complete
redirect_url. The page may initially show payment preparation. A withdrawal has no customer redirect. - Use the status endpoint to read the current result. HTTP 200 from create means that the operation exists, not that money has been received or sent.
- When a callback arrives, verify its raw-body signature, durably accept the notification, respond with HTTP 200, and retrieve the current transaction status.
v1 and v2 use the same payment processing rules, balances, and configured payment routes. Their authentication and wire formats remain separate. Keep existing v1 operations on v1; a v2 status request cannot retrieve them.
Available operations
| Action | Endpoint | Guide |
|---|---|---|
| Create withdrawal | POST /api/v2/withdrawals | Withdrawals |
| Create deposit | POST /api/v2/deposits | Deposits |
| Get either transaction's status | GET /api/v2/transactions/{transaction_id} | Transaction status |
See errors, statuses, and callbacks. Only transactions created through v2 can be retrieved by the v2 status endpoint.
Retrying a create request
If a create response is lost, retry the same endpoint with the same order_id and original request data, signing each request's exact bytes. An accepted repeat resolves to the same operation and transaction_id; it does not create another operation. Do not change method, amount, recipient, URLs, or fields on a retry. A conflict returns HTTP 409.
Idempotency is scoped to your merchant and operation type across shops. Switching shops or API versions is not a way to retry the same payment. Do not use a new ID merely because a response, callback, or payment page is delayed. Check status, or contact your integration contact if the outcome remains uncertain.
Because callbacks contain only order_id, prefer IDs unique across both deposits and withdrawals when using a shared callback endpoint. If you reuse an ID across directions, use separate callback URLs or another unambiguous server-side order mapping.