Developer docs

One REST API for pay-ins, payouts and wallet routing

Seven JSON endpoints, one API key, signed webhooks. Send us an order — the routing engine picks a healthy agent wallet, respects every limit and cooldown, and pushes the result back to you within about a second.

Base URL

https://xpaybd.com/api/v1

Auth

key field inside the JSON body

Webhook

HMAC-SHA256 X-Signature header

Quick start

  1. 1

    Get your key & salt

    Generate the API key and salt in your portal. Both are shown once — store them in your server vault.

  2. 2

    Whitelist your server IP

    Only whitelisted IPs may call the API. Add every outbound IP of your backend.

  3. 3

    Create an order

    POST a payin or payout with your own client_txn_id. The engine picks the wallet for you.

  4. 4

    Receive the webhook

    We push the final result to your webhook URL and sign it. Verify, then update your ledger.

Core concepts

Eight words carry the whole API. Read these once and every endpoint below reads like plain English.

API user

That's you. One API key + salt pair identifies your platform on every call and every webhook we send back.

Agent

The wallet owner behind the money movement. Agents connect their bKash / Nagad accounts in the agent dashboard; you never see or pick an individual wallet.

Channel (bank_id)

The rail an order runs on — bKash Personal, bKash Agent, Nagad Personal / Agent or Nagad Merchant. You choose the rail, the engine chooses the wallet on it.

Order

One pay-in (money coming in) or payout (money going out). Every order carries your own client_txn_id plus our order_id.

client_txn_id

Your idempotency key. Reuse it on a retry and you get 409 instead of a duplicate order; it frees up again after 24 hours.

Routing engine

Picks a healthy wallet using balance, per-transaction limits, daily limits and cooldown, then rotates traffic evenly across wallets.

Webhook

The signed callback that tells you the final state. It is the source of truth — polling order_status is only a fallback.

api_user_fee

The flat platform fee added per transaction, returned on the order and in the webhook so your ledger always balances.

Authentication

Every endpoint is POST with a JSON body and lives under https://xpaybd.com/api/v1. There is no Authorization header — pass your key as the key field in the body. A bad or missing key returns 401 Unauthorized.

minimal request
POST https://xpaybd.com/api/v1/wallet_balance
Content-Type: application/json

{ "key": "YOUR_API_KEY" }
  • Key — authenticates requests. Server-side only; never ship it to a browser or mobile app.
  • Salt — the HMAC secret used to verify webhook signatures. Different job, same rule: keep it secret.
  • Rotation — regenerating invalidates the old pair immediately, so deploy the new values first.
  • IP whitelist — calls from unlisted IPs are rejected before they reach the routing engine.

Channels & limits

Pass the channel you want as bank_id. Amounts outside the per-transaction range below are rejected. Check live headroom with /available_limits before sending large batches — it returns overall and per-gateway capacity.

bank_idChannelServicesMinMax
3bKash PersonalPayIn / PayOut20050,000
2bKash AgentPayIn / PayOut20050,000
1Nagad PersonalPayIn / PayOut20050,000
1Nagad AgentPayIn / PayOut20050,000
4Nagad Merchant (non-API)PayIn only200100,000

Order lifecycle

Both directions follow the same shape: you create an order, the routing engine does the work, and a signed webhook closes the loop. Average round trip is about one second.

PayIn — money in

  1. 1

    You call /create_payin_order

    Send amount, bank_id and your client_txn_id.

  2. 2

    Engine picks a wallet

    Balance, limits, cooldown and rotation are checked in one pass — roughly a second.

  3. 3

    Customer pays

    We return the deposit account (or a hosted checkout link) and the order sits pending for 1 hour.

  4. 4

    You submit the TrxID

    Call /update_payin_order with the customer's bank_txn_id so we can match the deposit.

  5. 5

    Webhook fires

    payin_success or payin_failed lands on your endpoint, signed with X-Signature.

PayOut — money out

  1. 1

    You call /create_payout_order

    Send amount, receiver account, bank_id and client_txn_id.

  2. 2

    Engine reserves the wallet

    Only wallets with real balance and free limit are eligible.

  3. 3

    Money is sent

    The agent wallet disburses to the receiver account.

  4. 4

    Instant reconciliation

    We fetch the wallet statement and match the transfer before calling it done.

  5. 5

    Webhook fires

    payout_success (with bank_txn_id) or payout_failed with a reason.

API playground

Live requests

Test every endpoint right here with your own API key — no Postman needed. Requests are relayed server-side to https://xpaybd.com/api/v1 and the raw response is shown exactly as your backend will receive it. Your key stays in this browser tab only and is never stored on our servers.

Your unique reference — alphanumeric/hyphen, max 36 chars. Reusable after 24 hours.

Collection amount (positive).

Target channel: 3 bKash Personal, 2 bKash Agent, 1 Nagad Personal/Agent, 4 Nagad Merchant (non-API, payin only). See channel limits.

Required when the bank has the checkout page enabled. http/https only, max 2048 chars; localhost and private IPs are rejected.

Request body
{
  "key": "••••••••",
  "client_txn_id": "payin-001",
  "amount": 500,
  "bank_id": 1,
  "redirect_url": "https://your-site.com/thank-you"
}
Response
// Send a request to see the live response here.
Equivalent cURL
curl -X POST 'https://xpaybd.com/api/v1/create_payin_order' \
  -H 'Content-Type: application/json' \
  -d '{
  "key": "YOUR_API_KEY",
  "client_txn_id": "payin-001",
  "amount": 500,
  "bank_id": 1,
  "redirect_url": "https://your-site.com/thank-you"
}'

Endpoints

POST/api/v1/create_payin_orderCreate PayIn Order

Open a collection order. The engine picks a healthy agent wallet and returns the deposit account (plus a hosted checkout link when the bank supports it).

FieldTypeRequiredDescription
keystringYesYour API key.
client_txn_idstringYesYour unique reference — alphanumeric/hyphen, max 36 chars. Reusable after 24 hours.
amountnumberYesCollection amount (positive).
bank_idintegerYesTarget channel: 3 bKash Personal, 2 bKash Agent, 1 Nagad Personal/Agent, 4 Nagad Merchant (non-API, payin only). See channel limits.
redirect_urlstringConditionalRequired when the bank has the checkout page enabled. http/https only, max 2048 chars; localhost and private IPs are rejected.
curl request
curl -X POST 'https://xpaybd.com/api/v1/create_payin_order' \
  -H 'Content-Type: application/json' \
  -d '{
    "key": "YOUR_API_KEY",
    "client_txn_id": "payin-001",
    "amount": 500,
    "bank_id": 1,
    "redirect_url": "https://your-site.com/thank-you"
  }'
201 — Created
response
{
  "status_code": 201,
  "message": "Payin order created successfully",
  "status": "success",
  "data": {
    "order_id": 1,
    "client_txn_id": "payin-001",
    "amount": 500,
    "total_amount": 500,
    "bank_name": "Nagad",
    "bank_account": "0171XXXXXXX",
    "checkout_url": "https://checkout.example.com/checkout/payin/...",
    "expired_at": "2026-08-04T17:23:05.992Z",
    "status": "pending"
  }
}
422 — Validation
response
{
  "status_code": 422,
  "message": "amount must be a positive number",
  "status": "error"
}
400 — No bank available
response
{
  "status_code": 400,
  "message": "No available bank account found for payin",
  "status": "error"
}

Note — checkout_url is only returned when the target bank has the checkout page enabled. Send the customer there: they pay into the shown account and submit their TrxID. PayIn orders expire after 1 hour by default (see expired_at) and then auto-move to expired.

POST/api/v1/update_payin_orderUpdate PayIn Order

Submit the customer's bank transaction ID against a pending payin. On a match the order flips to success and wallet settlement runs.

FieldTypeRequiredDescription
keystringYesYour API key.
client_txn_idstringOne ofYour reference (max 36 chars).
order_idintegerOne ofSystem order ID.
txn_idstringYesPayment reference from the sender's bank (max 100 chars).
mobile_nostringNoSender mobile number (7–15 digits). Left blank if omitted.
curl request
curl -X POST 'https://xpaybd.com/api/v1/update_payin_order' \
  -H 'Content-Type: application/json' \
  -d '{
    "key": "YOUR_API_KEY",
    "client_txn_id": "payin-001",
    "txn_id": "A1B2C3D4E5"
  }'
200 — Matched
response
{
  "status_code": 200,
  "message": "Payin order updated successfully",
  "status": "success",
  "data": {
    "order_id": 42,
    "client_txn_id": "payin-001",
    "bank_txn_id": "A1B2C3D4E5",
    "paid_amount": 500,
    "requested_amount": 500,
    "status": "success",
    "api_user_fee": 15.00,
    "created_at": "2026-08-04T10:00:00.000Z",
    "updated_at": "2026-08-04T10:05:00.000Z"
  }
}
404 — Order not found
response
{
  "status_code": 404,
  "message": "Order not found with this client_txn_id",
  "status": "error",
  "data": {}
}
400 — Wrong status / expired
response
{
  "status_code": 400,
  "message": "Order cannot be updated in its current status: success",
  "status": "error"
}

Note — Only orders in pending status can be updated.

POST/api/v1/create_payout_orderCreate PayOut Order

Queue a disbursement. Funds leave a connected agent wallet and land on the customer's mobile number.

FieldTypeRequiredDescription
keystringYesYour API key.
client_txn_idstringYesUnique reference, max 36 chars. Reusable after 24 hours.
mobile_nostringYesRecipient mobile number (7–15 digits).
amountnumberYesPayout amount (positive).
bank_idintegerYesTarget channel: 3 bKash Personal, 2 bKash Agent, 1 Nagad Personal/Agent, 4 Nagad Merchant (non-API, payin only). See channel limits.
curl request
curl -X POST 'https://xpaybd.com/api/v1/create_payout_order' \
  -H 'Content-Type: application/json' \
  -d '{
    "key": "YOUR_API_KEY",
    "client_txn_id": "order-001",
    "mobile_no": "01712345678",
    "amount": 1000,
    "bank_id": 1
  }'
201 — Queued
response
{
  "status_code": 201,
  "message": "Payout order created and queued successfully",
  "status": "success",
  "data": {
    "client_txn_id": "order-001",
    "amount": 1000,
    "mobile_no": "01712345678",
    "status": "queue",
    "agent_user_id": 5
  }
}
201 — Insufficient balance
response
{
  "status_code": 201,
  "message": "Payout order failed — insufficient balance (need 1010, have 500)",
  "status": "error",
  "data": {
    "client_txn_id": "order-001",
    "status": "failed",
    "agent_user_id": null,
    "fail_reason": "Insufficient balance (including charges)"
  }
}
409 — Duplicate
response
{
  "status_code": 409,
  "message": "client_txn_id already exists",
  "status": "error"
}
400 — Bank unavailable
response
{
  "status_code": 400,
  "message": "Bank is currently not active",
  "status": "error"
}

Note — Charge slabs are applied automatically — your wallet is debited amount + charges. If the balance falls short, the order is created with a failed status instead of being rejected.

POST/api/v1/fail_payout_orderFail PayOut Order

Cancel a payout that is still sitting in the queue. Once the dispatcher picks it up it can no longer be failed.

FieldTypeRequiredDescription
keystringYesYour API key.
client_txn_idstringOne ofYour reference — only orders from the last 24 hours are searched.
order_idintegerOne ofSystem order ID.
curl request
curl -X POST 'https://xpaybd.com/api/v1/fail_payout_order' \
  -H 'Content-Type: application/json' \
  -d '{
    "key": "YOUR_API_KEY",
    "client_txn_id": "order-001"
  }'
200 — Failed
response
{
  "status_code": 200,
  "status": "success",
  "message": "Payout order failed successfully",
  "data": {
    "order_id": 123,
    "client_txn_id": "order-001",
    "status": "failed"
  }
}
400 — Already processing
response
{
  "status_code": 400,
  "message": "Order is already processing",
  "status": "error"
}
422 — Validation
response
{
  "status_code": 422,
  "message": "Either client_txn_id or order_id is required",
  "status": "error"
}

Note — Only queued payouts that are not being processed can be failed. A failed order is never dispatched afterwards.

POST/api/v1/order_statusOrder Status

Poll the current state of any order — payin or payout.

FieldTypeRequiredDescription
keystringYesYour API key.
client_txn_idstringOne ofYour reference.
order_idintegerOne ofSystem order ID.
curl request
curl -X POST 'https://xpaybd.com/api/v1/order_status' \
  -H 'Content-Type: application/json' \
  -d '{
    "key": "YOUR_API_KEY",
    "client_txn_id": "payin-001"
  }'
200 — OK
response
{
  "status_code": 200,
  "status": "success",
  "data": {
    "order_id": 42,
    "order_type": "payout",
    "client_txn_id": "payout-001",
    "bank_txn_id": "D9Q8UI5",
    "paid_amount": 500,
    "requested_amount": 500,
    "status": "success",
    "reason": null,
    "api_user_fee": 10,
    "created_at": "2026-02-19T09:00:00.000Z",
    "updated_at": "2026-02-19T09:05:00.000Z",
    "sender_acc": "01923043979",
    "receiver_acc": "01712345678"
  }
}
404 — Not found
response
{
  "status_code": 404,
  "message": "Order not found",
  "status": "error"
}
POST/api/v1/wallet_balanceWallet Balance

Read the live wallet balance of the authenticated API user.

FieldTypeRequiredDescription
keystringYesYour API key.
curl request
curl -X POST 'https://xpaybd.com/api/v1/wallet_balance' \
  -H 'Content-Type: application/json' \
  -d '{ "key": "YOUR_API_KEY" }'
200 — OK
response
{
  "status_code": 200,
  "status": "success",
  "data": { "balance": 1500.00 }
}
401 — Unauthorized
response
{
  "status_code": 401,
  "message": "Unauthorized Access",
  "status": "error"
}
POST/api/v1/available_limitsAvailable Limits

Vault capacity across every active agent wallet — payin headroom, payout balance and limit, plus a per-gateway breakdown. Call it before sizing a batch.

FieldTypeRequiredDescription
keystringYesYour API key.
curl request
curl -X POST 'https://xpaybd.com/api/v1/available_limits' \
  -H 'Content-Type: application/json' \
  -d '{ "key": "YOUR_API_KEY" }'
200 — OK
response
{
  "status_code": 200,
  "status": "success",
  "message": "Vault capacity fetched successfully",
  "data": {
    "currency": "BDT",
    "payin": {
      "status": "available",
      "available_capacity": 2500000,
      "minimum_order_amount": 500,
      "maximum_order_amount": 100000
    },
    "payout": {
      "status": "available",
      "available_balance": 1800000,
      "available_limit": 1500000,
      "minimum_order_amount": 500,
      "maximum_order_amount": 100000
    },
    "gateway_capacity": [
      {
        "gateway_type": "bkash_agent",
        "payin_available": 1000000,
        "payout_available_balance": 700000,
        "payout_available_limit": 600000,
        "status": "available"
      }
    ],
    "updated_at": "2026-08-13T23:42:00+06:00"
  }
}

Webhooks

Set your webhook URL in the portal (leave it empty to disable). Every final order state is pushed as JSON with an X-Signature header — HMAC-SHA256 of the raw request payload keyed with your salt. Verify it before you trust the payload, and always respond 200 quickly.

payin_success
{
  "event": "payin_success",
  "data": {
    "order_id": 1,
    "order_type": "payin",
    "client_txn_id": "payin-001",
    "bank_txn_id": "DXXXXXXXX2",
    "paid_amount": 5000,
    "requested_amount": 5100,
    "reason": null,
    "status": "success",
    "api_user_fee": 101,
    "created_at": "2026-08-03T18:25:24.000Z",
    "updated_at": "2026-08-03T18:25:24.000Z",
    "sender_acc": "01712345678",
    "receiver_acc": "01987262113"
  }
}
signature verification — node.js
// Node.js — verify the X-Signature header on every webhook
import crypto from "node:crypto";

app.post("/webhooks/pay2capital", express.raw({ type: "*/*" }), (req, res) => {
  const raw = req.body;                       // the RAW request payload, not JSON.parse'd
  const expected = crypto
    .createHmac("sha256", process.env.PAY2CAPITAL_SALT)
    .update(raw)
    .digest("hex");

  const received = req.get("X-Signature") ?? "";
  const ok =
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));

  if (!ok) return res.status(401).send("bad signature");

  const event = JSON.parse(raw.toString("utf8"));
  // handle event.event: payin_success | payin_failed | payout_success | payout_failed
  res.sendStatus(200);
});

Order states

pending

PayIn created, waiting for the customer to pay and submit a TrxID.

queue

PayOut accepted, waiting for the dispatcher to pick a wallet.

processing

The dispatcher is executing the transfer — no longer cancellable.

success

Matched and settled. A webhook is fired.

failed

Rejected or unmatched. reason carries the cause.

expired

PayIn window closed (1 hour by default) with no matching payment.

Common error codes

401Unauthorized — invalid or missing API key.
403Account deactivated — contact customer care.
404Resource not found (bank or order).
409Conflict — duplicate client_txn_id.
422Validation error — read the message for the offending field.
500Internal server error — retry with the same client_txn_id.

Retries are safe: reuse the same client_txn_id and a duplicate returns 409 instead of creating a second order. The same reference becomes reusable again after 24 hours.

Go-live checklist

Tick every line before you switch real traffic on. These eight points cover every support ticket we normally see in week one.

  • Key and salt stored server-side in a vault — never in front-end code.
  • All backend outbound IPs whitelisted in the portal.
  • client_txn_id is unique per order and stored before you call us.
  • Webhook endpoint is public, responds 200 fast, and verifies X-Signature on the raw body.
  • Webhook handler is idempotent — the same event may arrive more than once.
  • Amounts validated against the channel min/max before you call the API.
  • order_status polling wired as a fallback for missed webhooks.
  • Errors 401 / 409 / 422 / 400 mapped to your own user-facing messages.

Need a sandbox key or help going live?

Our integration team reviews your flow and whitelists your servers the same day.

Talk to integration team