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_idChannelGatewayPayInPayOutMinMax
3bKash Personalbkash Yes Yes৳200৳50,000
2bKash Agentbkash_agent Yes Yes৳200৳50,000
1Nagad (Agent)nagad Yes Yes৳200৳50,000
5Nagad Personalnagad_personal Yes Yes৳200৳50,000
4Nagad Merchant (non-API)nagad_merchant Yes— No৳200৳100,000
DirectionEndpointAccepted bank_id
PayIn/create_payin_order
3bKash Personal2bKash Agent1Nagad (Agent)5Nagad Personal4Nagad Merchant (non-API)
PayOut/create_payout_order
3bKash Personal2bKash Agent1Nagad (Agent)5Nagad Personal

Nagad Merchant (4) is a non-API collection channel — sending it to /create_payout_order returns 400.

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 (must support payin): 3 bKash Personal, 2 bKash Agent, 1 Nagad, 5 Nagad Personal, 4 Nagad Merchant (non-API). See the bank_id map.

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"
}'

Signature & key tools

Runs in your browser

Build a webhook signature from any payload, compare it with one you received, and confirm your credentials work before you write a single line of code.

Webhook signature generator & verifier

Salt (webhook secret)
Raw request body (exact bytes we send)
Compare with a received signature

Everything here is computed inside your browser with WebCrypto. Your salt never leaves this tab.

API key checker

API key

The check calls Wallet Balance — a read-only endpoint — so nothing is created or charged. The key is used for that single request and never stored.

Ready-made client

Zero dependencies

One file that wraps every endpoint plus webhook signature verification. Drop it into your project, set your key from an environment variable, and you are integrated.

pay2capital.js
// pay2capital.js — generated client for https://xpaybd.com/api/v1
// Usage:  const p2c = createClient({ key: process.env.P2C_KEY, salt: process.env.P2C_SALT });
import { createHmac, timingSafeEqual } from "node:crypto";

const BASE = "https://xpaybd.com/api/v1";

export function createClient({ key, base = BASE, timeoutMs = 20000 }) {
  if (!key) throw new Error("API key is required");

  async function call(path, payload = {}) {
    const res = await fetch(base + path, {
      method: "POST",
      headers: { "content-type": "application/json", accept: "application/json" },
      body: JSON.stringify({ key, ...payload }),
      signal: AbortSignal.timeout(timeoutMs),
    });
    const text = await res.text();
    let data;
    try {
      data = JSON.parse(text);
    } catch {
      data = { raw: text };
    }
    if (!res.ok) {
      const err = new Error(data?.message || `Request failed with ${res.status}`);
      err.status = res.status;
      err.body = data;
      throw err;
    }
    return data;
  }

  return {
    call,
    /** POST /create_payin_order — Create PayIn Order. Fields: client_txn_id, amount, bank_id, redirect_url */
    createPayinOrder: (payload) => call("/create_payin_order", payload),
    /** POST /update_payin_order — Update PayIn Order. Fields: client_txn_id, order_id, txn_id, mobile_no */
    updatePayinOrder: (payload) => call("/update_payin_order", payload),
    /** POST /create_payout_order — Create PayOut Order. Fields: client_txn_id, mobile_no, amount, bank_id */
    createPayoutOrder: (payload) => call("/create_payout_order", payload),
    /** POST /fail_payout_order — Fail PayOut Order. Fields: client_txn_id, order_id */
    failPayoutOrder: (payload) => call("/fail_payout_order", payload),
    /** POST /order_status — Order Status. Fields: client_txn_id, order_id */
    orderStatus: (payload) => call("/order_status", payload),
    /** POST /wallet_balance — Wallet Balance. Fields: none */
    walletBalance: (payload) => call("/wallet_balance", payload),
    /** POST /available_limits — Available Limits. Fields: none */
    availableLimits: (payload) => call("/available_limits", payload),
  };
}

/** Verify the X-Signature header of an incoming webhook against the RAW body string. */
export function verifyWebhook(rawBody, signature, salt) {
  const expected = createHmac("sha256", salt).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(String(signature || ""));
  return a.length === b.length && timingSafeEqual(a, b);
}

Android SDK

API + Checkout UI Download SDK (.zip)

A complete Android Studio project: the sdk library module wrapping every endpoint, the hosted bKash / Nagad checkout screen, and a runnable sample app. Build an AAR with ./gradlew :sdk:assembleRelease or include the module directly.

Language
Kotlin 1.9
minSdk
21 (Android 5.0)
Delivery
Gradle module or AAR
Size
Zero HTTP deps
include(":sdk")
project(":sdk").projectDir = file("pay2capital-android-sdk/sdk")

// app/build.gradle.kts
dependencies { implementation(project(":sdk")) }

Never ship a live API key inside an APK. Create the order on your backend and pass only the checkout_url to the app. Treat a submitted TrxID as a claim — settle only after order_status or the webhook confirms it.

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 (must support payin): 3 bKash Personal, 2 bKash Agent, 1 Nagad, 5 Nagad Personal, 4 Nagad Merchant (non-API). See the bank_id map.
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
response
{
  "status_code": 400,
  "message": "Order cannot be updated in its current status: success",
  "status": "error",
  "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"
  }
}
400 — Expired
response
{
  "status_code": 400,
  "message": "Order has expired",
  "status": "error",
  "data": {
    "order_id": 42,
    "client_txn_id": "payin-001",
    "bank_txn_id": "A1B2C3D4E5",
    "paid_amount": 500,
    "requested_amount": 500,
    "status": "expired",
    "api_user_fee": 15.00,
    "created_at": "2026-08-04T10:00:00.000Z",
    "updated_at": "2026-08-04T10:10:00.000Z"
  }
}

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 (must support payout): 3 bKash Personal, 2 bKash Agent, 1 Nagad, 5 Nagad Personal. Nagad Merchant (4) is payin only. See the bank_id map.
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",
    "amount": 1000,
    "mobile_no": "01712345678",
    "status": "failed",
    "agent_user_id": null,
    "fail_reason": "Insufficient balance (including charges)"
  }
}
422 — Validation
response
{
  "status_code": 422,
  "message": "client_txn_id is required",
  "status": "error"
}
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"
}
400 — Wrong status
response
{
  "status_code": 400,
  "message": "Order cannot be failed in its current status: success",
  "status": "error"
}
404 — Not found
response
{
  "status_code": 404,
  "message": "Order not found",
  "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.

Live status & go-live checklist

Payments API

Tick every line before you switch real traffic on — your progress is saved in this browser. These points cover every support ticket we normally see in week one.

0/8

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