{{BRAND}} Payments API
{{API_BASE}}/api/v1/externalThe {{BRAND}} API lets you accept payments (pay-ins) and send payouts (withdrawals) programmatically. All endpoints are JSON over HTTPS and share the base URL above.
Authentication
Your account is set up with one of the following. Your account manager will confirm which applies to you:
- Signature authentication (used for new integrations) โ every request carries an
X-SignatureHMAC-SHA256 digest over the request body, and no API key is sent. See Request Signing. - API key โ a secret sent in the
X-API-Keyheader. Requests without a valid key are rejected with401 Unauthorized.
Where signature authentication is enabled for your account, an API key alone will not authenticate a request. Keep every credential server-side โ never expose one in browser or mobile clients.
Your account can also be restricted to an IP allowlist, so requests are only accepted from IPs you nominate. Send your egress IPs to your account manager to enable it.
Your Credentials
Your account manager will provision the following. None of them are set in the API request itself:
- Signing key โ used to compute
X-Signature. Never sent in a request. Signature accounts only. X-API-Keyโ secret key sent on every request. API key accounts only.client_idโ your client UUID, sent in request bodies / query paramspostback_urlโ receive pay-in webhooks (verified with yoursigning_key)withdrawal_postback_urlโ receive payout webhooks (verified with yoursigning_key)
Every endpoint below lists both header sets. Send the one your account is configured for โ the request examples show the signed form.
Typical Flow
- Pay-in: create a transaction โ redirect the customer to
payment_page_urlโ get notified via webhook when it isactivated. - Payout: create a withdrawal โ poll its status or wait for the withdrawal webhook to fire
success/failed.
Response Codes
200/201 success ยท 400 bad request ยท 401 invalid credentials (bad API key, or missing/invalid/stale signature) ยท 403 not your resource, or an IP that is not on your allowlist ยท 404 not found ยท 422 validation error (including a duplicate client_transaction_id / client_withdrawal_id).
{{API_BASE}}/api/v1/externalPATH="/api/v1/external/payin"
BODY='{"amount":1000.50,"currency":"INR","client_user":"user123","client_id":"545XXXXXXXXXXXXXXXXXXXXXXXXXXUI","client_transaction_id":"TXN20231106001","payment_option_name":"UPI"}'
TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')
SIG=$(printf '%s\n%s\n%s\n%s\n%s' "POST" "$PATH" "$TS" "$NONCE" "$BODY_HASH" \
| openssl dgst -sha256 -hmac "$SIGNING_KEY" -hex | awk '{print $NF}')
curl -X POST "{{API_BASE}}$PATH" \
-H "Content-Type: application/json" \
-H "X-Timestamp: $TS" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIG" \
-d "$BODY"curl -X POST {{API_BASE}}/api/v1/external/payin \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"amount": 1000.50,
"currency": "INR",
"client_user": "user123",
"client_id": "545XXXXXXXXXXXXXXXXXXXXXXXXXXUI",
"client_transaction_id": "TXN20231106001",
"payment_option_name": "UPI"
}'Request Signing & Security
HMAC-SHA256Sign every API request with your signing key. The key itself is never transmitted โ only the digest โ so an intercepted request cannot be replayed or modified.
How to sign a request
\n):METHOD ยท PATH (including the query string) ยท TIMESTAMP ยท NONCE ยท SHA256_HEX(body)HMAC-SHA256(signing key, canonical string) and hex-encode it (lowercase).X-Signature, along with X-Timestamp (unix seconds) and X-Nonce (unique per request).Headers
| Name | Required | Description |
|---|---|---|
| X-Signature | Required | Lowercase hex HMAC-SHA256 digest of the canonical string |
| X-Timestamp | Required | Unix seconds. Rejected if more than 5 minutes from server time |
| X-Nonce | Required | Unique random value per request (max 128 chars). A repeat within the 5-minute window is rejected |
| X-Merchant-Id | Optional | Your client UUID. Only needed if the request carries no client_id |
Rejection reasons
401 missing or invalid signature, stale X-Timestamp, or a reused X-Nonce ยท 403 the request came from an IP that is not on your allowlist.
Webhook signatures
Webhooks sent to you are signed the same way. Each callback carries X-Signature, X-Timestamp and X-Signature-Algorithm: HMAC-SHA256, where the canonical string is TIMESTAMP + \n + SHA256_HEX(raw_body), keyed by your postback key. Always verify against the raw body bytes exactly as received, before parsing them.
import hashlib, hmac, json, secrets, time
import requests
SIGNING_KEY = "your-signing-key"
BASE = "{{API_BASE}}"
def signed_post(path, payload):
body = json.dumps(payload).encode()
ts = str(int(time.time()))
nonce = secrets.token_hex(16)
canonical = "\n".join([
"POST", path, ts, nonce,
hashlib.sha256(body).hexdigest(),
])
signature = hmac.new(
SIGNING_KEY.encode(), canonical.encode(), hashlib.sha256
).hexdigest()
return requests.post(
BASE + path,
data=body, # send the exact bytes you signed
headers={
"Content-Type": "application/json",
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Signature": signature,
},
)
signed_post("/api/v1/external/payin", {
"amount": 1000.50,
"currency": "INR",
"client_user": "user123",
"client_id": "545XXXXXXXXXXXXXXXXXXXXXXXXXXUI",
"client_transaction_id": "TXN20231106001",
"payment_option_name": "UPI",
})const crypto = require("crypto");
const SIGNING_KEY = "your-signing-key";
const BASE = "{{API_BASE}}";
async function signedPost(path, payload) {
const body = Buffer.from(JSON.stringify(payload));
const ts = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomBytes(16).toString("hex");
const canonical = [
"POST", path, ts, nonce,
crypto.createHash("sha256").update(body).digest("hex"),
].join("\n");
const signature = crypto
.createHmac("sha256", SIGNING_KEY)
.update(canonical)
.digest("hex");
return fetch(BASE + path, {
method: "POST",
body, // the exact bytes that were signed
headers: {
"Content-Type": "application/json",
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Signature": signature,
},
});
}import hashlib, hmac, time
SIGNING_KEY = "your_signing_key"
def verify(raw_body: bytes, headers) -> bool:
ts = headers.get("X-Timestamp", "")
if abs(time.time() - int(ts)) > 300: # reject stale callbacks
return False
canonical = f"{ts}\n{hashlib.sha256(raw_body).hexdigest()}"
expected = hmac.new(
SIGNING_KEY.encode(), canonical.encode(), hashlib.sha256
).hexdigest()
# constant-time comparison
return hmac.compare_digest(expected, headers.get("X-Signature", ""))# PATH includes the query string, body is empty
path = "/api/v1/external/transactions/check?client_id=545XXX&client_transaction_id=TXN20231106001"
canonical = "\n".join([
"GET", path, ts, nonce,
hashlib.sha256(b"").hexdigest(),
])Create Pay-In Transaction
{{API_BASE}}/api/v1/external/payinCreates a new pay-in transaction. Returns payment details and a payment_page_url to redirect your customer for payment.
How to Redirect Customers After Payment
Append redirect_success_url to the payment_page_url to redirect the customer back to your site after successful payment:
// Append redirect_success_url before sending user to payment page
const paymentUrl = response.payment_page_url + "&redirect_success_url=https://yoursite.com/payment/success";
// Then redirect user:
window.location.href = paymentUrl;Webhook URL & Signing Key Configuration
Webhook URLs and signing keys are configured per-client in the system settings (not in the API request). Contact your account manager to configure:
Headers
| Name | Required | Description |
|---|---|---|
| X-Signature | Signature accounts | HMAC-SHA256 digest of this request โ see Request Signing |
| X-Timestamp | Signature accounts | Unix seconds, within 5 minutes of server time |
| X-Nonce | Signature accounts | Unique random value per request |
| X-API-Key | API key accounts | Your external API key. Not accepted on signature accounts |
| Content-Type | Required | Request content type |
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| amount | float | Required | Transaction amount |
| currency | string | Required | 3-letter currency code (e.g., INR) |
| client_user | string | Required | User identifier |
| client_id | UUID | Required | Your client UUID |
| client_transaction_id | string | Required | Your unique transaction ID |
| payment_option_name | string | Required | Payment method (UPI, IMPS, etc.) |
Response Fields
| Name | Type | Description |
|---|---|---|
| payin_id | string | Unique pay-in identifier |
| transaction_id | string | Transaction ID for status checks and webhook matching |
| qr_link | string | null | UPI QR code link. null for non-UPI methods (IMPS, etc.) |
| payment_details | object | null | UPI: {upi_id, account_name} ยท IMPS: {account_name, account_number, ifsc_code} |
| payment_option_name | string | Payment method used (UPI, IMPS, etc.) |
| client_transaction_id | string | Your transaction ID echoed back |
| client_user | string | Your user identifier echoed back |
| payment_page_url | string | Redirect your customer to this URL to complete payment. To redirect back to your site after payment, append: ?redirect_success_url=https://yoursite.com/success |
| expiry_time | integer | Unix timestamp โ transaction expires 10 minutes after creation |
| parsing_type | integer | null | Internal routing hint for how the account is verified. You can ignore it. |
{
"amount": 1000.50,
"currency": "INR",
"client_user": "user123",
"client_id": "545XXXXXXXXXXXXXXXXXXXXXXXXXXUI",
"client_transaction_id": "TXN20231106001",
"payment_option_name": "UPI"
}{
"payin_id": "pay-cfcbd95213647d81e5f6903f3eb2f747",
"transaction_id": "txn-pay-cfcbd95213647d81e5f6903f3eb2f747",
"qr_link": "upi://pay?pa=jeevajeeva31156@okaxis&am=1000.50",
"payment_details": {
"upi_id": "jeevajeeva31156@okaxis",
"account_name": "Jeeva Jeeva"
},
"payment_option_name": "UPI",
"client_transaction_id": "TXN20231106001",
"client_user": "user123",
"payment_page_url": "{{PAY_BASE}}?transaction_id=txn-pay-abc123",
"expiry_time": 1763577117,
"parsing_type": 1
}{
"detail": "No accounts found for payment option \"UPI\". Available payment options: IMPS, UPI"
}{
"detail": "Invalid API Key"
}{
"detail": "Payin with client_transaction_id: TXN20231106001 already exists"
}{
"detail": [
{
"loc": ["body", "currency"],
"msg": "string does not match regex \"[A-Z]{3}\"",
"type": "value_error.str.regex"
}
]
}Check Transaction Status
{{API_BASE}}/api/v1/external/transactions/checkCheck the current status of a transaction. Provide at least one of: client_id + client_transaction_id, transaction_id, or UTR. The status field in the response tells you the current state โ see Status Reference below.
Transaction Status Reference
The status field in the response (and in webhooks) can be one of the following values. Final statuses will not change โ do not poll again once received.
| Status | Final? | Description |
|---|---|---|
| activated | Final โ | Payment confirmed and verified. Safe to credit the user. |
| fake | Final โ | Transaction marked as fraudulent or invalid. Do NOT credit the user. |
| non_activated | Pending | Payment received but not yet verified by the system. Will transition to activated or fake. |
| non_paid | Pending | Awaiting payment from the customer (e.g., API gateway flow). Will transition once payment is made. |
๐ก Tip: Use webhooks (transaction-webhook) to get notified instantly instead of polling this endpoint.
Headers
| Name | Required | Description |
|---|---|---|
| X-Signature | Signature accounts | HMAC-SHA256 digest of this request โ see Request Signing |
| X-Timestamp | Signature accounts | Unix seconds, within 5 minutes of server time |
| X-Nonce | Signature accounts | Unique random value per request |
| X-API-Key | API key accounts | Your external API key. Not accepted on signature accounts |
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| client_id | UUID | Optional | Your client UUID (required with client_transaction_id) |
| client_transaction_id | string | Optional | Your unique transaction ID (required with client_id) |
| transaction_id | string | Optional | System transaction ID |
| UTR | string | Optional | Bank UTR reference number |
Response Fields
| Name | Type | Description |
|---|---|---|
| status | string | Current transaction status โ see Status Reference above |
| transaction_id | string | System transaction ID |
| amount | float | Transaction amount |
| currency | string | Currency code (e.g., INR) |
| creation_timestamp | integer | Unix timestamp when transaction was created |
| add_timestamp | integer | Unix timestamp when transaction was registered in system |
| activation_timestamp | integer | null | Unix timestamp when payment was confirmed. null if not yet activated |
| client_user | string | Your user identifier |
GET {{API_BASE}}/api/v1/external/transactions/check?client_id=545XXXXXXXXXXXXXXXXXXXXXXXXXXUI&client_transaction_id=TXN20231106001
OR
GET {{API_BASE}}/api/v1/external/transactions/check?transaction_id=txn-pay-cfcbd95213647d81e5f6903f3eb2f747
OR
GET {{API_BASE}}/api/v1/external/transactions/check?UTR=HDFC123456789
Headers (signature accounts โ sign the full path INCLUDING the query string,
over an empty body):
X-Timestamp: 1730880000
X-Nonce: 8f14e45fceea167a5a36dedd4bea2543
X-Signature: <hex hmac-sha256 digest>
Headers (API key accounts):
X-API-Key: your-api-key{
"status": "activated",
"transaction_id": "txn-pay-cfcbd95213647d81e5f6903f3eb2f747",
"amount": 1000.50,
"currency": "INR",
"creation_timestamp": 1699268400,
"add_timestamp": 1699268450,
"activation_timestamp": 1699268500,
"client_user": "user123"
}{
"detail": "One of the parameters: transaction_id, UTR, client_transaction_id must be specified"
}{
"detail": "Could not find transaction"
}UTR
{{API_BASE}}/api/v1/external/transactions/utrStore a UTR (Unique Transaction Reference) for a transaction without activating it. The UTR will be used for transaction verification and matching. Provide either transaction_id or client_transaction_id to identify the transaction โ exactly one must be specified.
Headers
| Name | Required | Description |
|---|---|---|
| X-Signature | Signature accounts | HMAC-SHA256 digest of this request โ see Request Signing |
| X-Timestamp | Signature accounts | Unix seconds, within 5 minutes of server time |
| X-Nonce | Signature accounts | Unique random value per request |
| X-API-Key | API key accounts | Your external API key. Not accepted on signature accounts |
| Content-Type | Required | Request content type |
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| client_id | UUID | Required | Your client UUID |
| transaction_id | string | Optional | System transaction ID (TXN-PAY-...). Required if client_transaction_id is not provided |
| client_transaction_id | string | Optional | Your unique transaction ID (from pay-in). Required if transaction_id is not provided |
| utr | string | Required | UTR / merchantOrderId / bank reference number to store |
// Option 1: Using transaction_id
{
"client_id": "545XXXXXXXXXXXXXXXXXXXXXXXXXXUI",
"transaction_id": "txn-pay-cfcbd95213647d81e5f6903f3eb2f747",
"utr": "HDFC123456789"
}
// Option 2: Using client_transaction_id
{
"client_id": "545XXXXXXXXXXXXXXXXXXXXXXXXXXUI",
"client_transaction_id": "TXN20231106001",
"utr": "HDFC123456789"
}{
"success": true,
"message": "UTR stored successfully. Transaction will be verified automatically.",
"transaction_id": "txn-pay-cfcbd95213647d81e5f6903f3eb2f747"
}{
"detail": "Invalid API Key"
}{
"detail": "Transaction does not belong to this client"
}{
"detail": "Transaction not found by transaction_id=txn-pay-abc123"
}{
"detail": "UTR can only be stored for incoming (deposit) transactions"
}{
"detail": [
{
"loc": ["body"],
"msg": "Either transaction_id or client_transaction_id must be provided",
"type": "value_error"
}
]
}Transaction Webhook (Postback)
Your Webhook URLSent when a pay-in transaction is activated (payment confirmed) or marked as fake. Configure your postback_url in client settings; callbacks are signed with your signing_key. Return HTTP 200 to acknowledge โ any non-200 (or a network error) triggers a retry. Defaults: up to 5 attempts, ~10s apart (configurable per client).
Status in Pay-in Callbacks
This callback has no status field. Unlike the withdrawal webhook, the outcome is carried by the boolean postback_is_fake. Map it to the pay-in status values as follows:
| Callback field | Equivalent status | What to do |
|---|---|---|
| postback_is_fake: false | activated | Payment confirmed โ credit the user. |
| postback_is_fake: true | fake | Marked fake/fraudulent โ do NOT credit the user. Reverse it if you already credited on an earlier callback. |
Callbacks are sent only for these two terminal outcomes. The remaining pay-in statuses โ pending, non_activated and non_paid โ are never pushed as callbacks; they are observable only by polling Check Transaction Status. If you need to detect an abandoned or unpaid transaction, poll for it โ no webhook will arrive.
Signature Verification (HMAC-SHA256)
Every callback is signed with HMAC-SHA256 using your signing_key โ the same secret you sign API requests with. The signature is in the headers, not the body:
X-Timestamp and X-Signature headersX-Timestamp is more than 5 minutes from your own clockTIMESTAMP + \n + SHA256_HEX(raw_body)HMAC-SHA256(signing_key, canonical string), hex-encode it, and compare against X-Signature using a constant-time comparisonPython Example (HMAC-SHA256)
import hashlib, hmac, time
from flask import Flask, request, jsonify
app = Flask(__name__)
SIGNING_KEY = "your_signing_key"
@app.route("/webhook/transactions", methods=["POST"])
def handle_webhook():
raw = request.get_data() # raw bytes, BEFORE parsing
ts = request.headers.get("X-Timestamp", "")
try:
if abs(time.time() - int(ts)) > 300:
return jsonify({"status": 401, "message": "Stale callback"}), 401
except ValueError:
return jsonify({"status": 401, "message": "Bad timestamp"}), 401
canonical = f"{ts}\n{hashlib.sha256(raw).hexdigest()}"
expected = hmac.new(
SIGNING_KEY.encode(), canonical.encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Signature", "")):
return jsonify({"status": 401, "message": "Invalid signature"}), 401
payload = request.get_json() # safe to parse once verified
for txn in payload.get("transactions", []):
print(txn["transaction_id"], txn["transaction_amount"])
return jsonify({"status": 200, "message": "OK"}), 200Node.js Example (HMAC-SHA256)
const express = require("express");
const crypto = require("crypto");
const app = express();
const SIGNING_KEY = "your_signing_key";
// raw body is required โ the signature covers the exact bytes we sent
app.post("/webhook/transactions", express.raw({ type: "*/*" }), (req, res) => {
const ts = req.get("X-Timestamp") || "";
if (Math.abs(Date.now() / 1000 - parseInt(ts, 10)) > 300) {
return res.status(401).json({ status: 401, message: "Stale callback" });
}
const bodyHash = crypto.createHash("sha256").update(req.body).digest("hex");
const expected = crypto
.createHmac("sha256", SIGNING_KEY)
.update(`${ts}\n${bodyHash}`)
.digest("hex");
const received = req.get("X-Signature") || "";
if (
expected.length !== received.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
) {
return res.status(401).json({ status: 401, message: "Invalid signature" });
}
const payload = JSON.parse(req.body); // safe to parse once verified
for (const txn of payload.transactions || []) {
console.log(txn.transaction_id, txn.transaction_amount);
}
res.status(200).json({ status: 200, message: "OK" });
});PHP Example (HMAC-SHA256)
<?php
$signingKey = "your_signing_key";
$raw = file_get_contents("php://input"); // raw bytes, BEFORE parsing
$ts = $_SERVER["HTTP_X_TIMESTAMP"] ?? "";
$sig = $_SERVER["HTTP_X_SIGNATURE"] ?? "";
if (abs(time() - intval($ts)) > 300) {
http_response_code(401);
exit(json_encode(["status" => 401, "message" => "Stale callback"]));
}
$canonical = $ts . "\n" . hash("sha256", $raw);
$expected = hash_hmac("sha256", $canonical, $signingKey);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit(json_encode(["status" => 401, "message" => "Invalid signature"]));
}
$payload = json_decode($raw, true); // safe to parse once verified
foreach ($payload["transactions"] as $txn) { /* credit user */ }
http_response_code(200);
echo json_encode(["status" => 200, "message" => "OK"]);Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| postback_id | integer | Required | Unique postback identifier |
| postback_type | integer | Required | 1 = Payin transaction |
| postback_is_fake | boolean | Required | false = payment confirmed (activated). true = transaction marked as fake/fraudulent โ do NOT credit the user |
| creation_type | string | Required | "auto" = activated by parser/system. "manual" = activated by admin |
| client_user | string | Required | Your customer/user identifier |
| transactions | array | Required | Always contains exactly ONE transaction object (single-element array). See Transaction Fields below. |
| sign | string | Required | Deprecated โ no longer sent. Verify the X-Signature header instead (see verification steps) |
Response Fields
| Name | Type | Description |
|---|---|---|
| transactions[].transaction_id | string | System transaction ID (e.g., txn-pay-xxx) |
| transactions[].client_transaction_id | string | Your transaction ID (if provided during pay-in creation) |
| transactions[].transaction_amount | float | Transaction amount |
| transactions[].transaction_currency_code | string | Currency code (e.g., INR) |
| transactions[].creation_timestamp | integer | Unix timestamp of transaction creation |
| transactions[].payment_method_id | integer | Payment method ID |
| transactions[].payment_details | string | Payment details (UPI ID, account number, etc.) |
| transactions[].payment_method_name | string | Payment method name (UPI, IMPS, etc.) |
| transactions[].transaction_custom_fields | array | Custom fields provided during transaction creation |
// Webhook Payload (POST JSON to your URL)
{
"postback_id": 27,
"postback_type": 1,
"postback_is_fake": false,
"creation_type": "auto",
"client_user": "user123",
"transactions": [
{
"transaction_id": "txn-pay-ed5910073cfed2a828f606f6050eb501",
"client_transaction_id": "TEST_TXN_1767079115",
"transaction_amount": 2000,
"transaction_currency_code": "INR",
"creation_timestamp": 1767079116,
"payment_method_id": 117,
"payment_details": "sundarrajan@okaxi",
"payment_method_name": "UPI",
"transaction_custom_fields": []
}
],
"sign": "ab0d339ef0cb649bf83f57f42e1766c9"
}// Your endpoint must return HTTP 200:
{
"status": 200,
"message": "OK"
}{ "status": 200, "message": "OK" }{ "status": 500, "message": "Invalid webhook signature" }Create Withdrawal
{{API_BASE}}/api/v1/external/withdrawalsInitiate a payout to a user bank account. The parent_name must match an existing Bank or Payment System by name (case-insensitive). Banks are shared across clients; Payment Systems are scoped to your client. Custom fields vary by bank โ call GET /api/v1/external/banks to list the banks available to you, and check your configured banks for the required field keys.
Withdrawal Status Reference
| Status | Final? | Description |
|---|---|---|
| new | Pending | Withdrawal created, queued for processing. |
| in_progress | Pending | Withdrawal is being processed by the payment provider. |
| success | Final โ | Payout completed successfully. UTR will be present in the response. |
| failed | Final โ | Payout failed. The amount will not be debited. |
Headers
| Name | Required | Description |
|---|---|---|
| X-Signature | Signature accounts | HMAC-SHA256 digest of this request โ see Request Signing |
| X-Timestamp | Signature accounts | Unix seconds, within 5 minutes of server time |
| X-Nonce | Signature accounts | Unique random value per request |
| X-API-Key | API key accounts | Your external API key. Not accepted on signature accounts |
| Content-Type | Required | Request content type |
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| parent_type | string | Required | Exactly bank or payment_system โ lowercase. Any other value returns 422 |
| parent_name | string | Required | Name of a Bank or Payment System (e.g., IMPS, UPI). Matched case-insensitively. Unknown name returns 404. List available banks via GET /api/v1/external/banks |
| type | string | Required | Exactly imps or bkash โ lowercase. Any other value returns 422 |
| client_id | UUID | Required | Your client UUID |
| client_user | string | Required | User identifier |
| amount | float | Required | Withdrawal amount |
| creation_timestamp | integer | Required | Unix timestamp |
| custom_field_values | array | Optional | Array of {field_key, field_value} objects. Keys must match the bank's configured fields (e.g. account_number, ifsc_code, account_holder_name). Defaults to empty โ but real payouts need the bank's fields. An unrecognised key returns 422 listing the valid keys for that bank; sending values to a bank with no custom fields configured also returns 422. |
| client_withdrawal_id | string | Optional | Your unique withdrawal ID โ alphanumeric, max 50 chars |
{
"parent_type": "bank",
"parent_name": "IMPS",
"type": "imps",
"client_id": "545XXXXXXXXXXXXXXXXXXXXXXXXXXUI",
"client_user": "user123",
"amount": 5000.00,
"creation_timestamp": 1763359236,
"custom_field_values": [
{
"field_key": "account_number",
"field_value": "123456789012"
},
{
"field_key": "ifsc_code",
"field_value": "HDFC0001234"
},
{
"field_key": "account_holder_name",
"field_value": "John Doe"
}
],
"client_withdrawal_id": "WD20231106001"
}{
"withdrawal_id": "plw-cd0c54210e09823b8103e502f40ea0f9",
"status": "new",
"amount": 5000.0,
"currency_code": "INR",
"parent_name": "IMPS",
"type": "imps",
"client_user": "user123",
"creation_timestamp": 1763359236,
"add_timestamp": 1763390633,
"completion_timestamp": null,
"utr": null,
"client_withdrawal_id": "WD20231106001",
"custom_field_values": [
{
"field_key": "account_number",
"field_value": "123456789012"
},
{
"field_key": "ifsc_code",
"field_value": "HDFC0001234"
},
{
"field_key": "account_holder_name",
"field_value": "John Doe"
}
]
}{
"detail": "Invalid API Key"
}{
"detail": "Bank with name \"IMPS\" not found for this client"
}{
"detail": "Bank name \"imps\" is ambiguous โ it matches 2 entries differing only by case. Use the exact name."
}{
"detail": "Invalid custom field keys: ['acount_number']. Available fields for Bank \"IMPS\": account_number, ifsc_code, account_holder_name"
}{
"detail": "Withdrawal with client_withdrawal_id: WD20231106001 already exists"
}Get Withdrawal Details
{{API_BASE}}/api/v1/external/withdrawals/{withdrawal_id}Retrieve detailed information about a specific withdrawal using the withdrawal_id. Returns all withdrawal details including status, amount, custom fields, timestamps, and UTR (if completed).
Withdrawal Status Reference
| Status | Final? | Description |
|---|---|---|
| new | Pending | Withdrawal created, queued for processing. |
| in_progress | Pending | Withdrawal is being processed by the payment provider. |
| success | Final โ | Payout completed successfully. UTR will be present in the response. |
| failed | Final โ | Payout failed. The amount will not be debited. |
Headers
| Name | Required | Description |
|---|---|---|
| X-Signature | Signature accounts | HMAC-SHA256 digest of this request โ see Request Signing |
| X-Timestamp | Signature accounts | Unix seconds, within 5 minutes of server time |
| X-Nonce | Signature accounts | Unique random value per request |
| X-API-Key | API key accounts | Your external API key. Not accepted on signature accounts |
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| withdrawal_id | string | Required | System-generated withdrawal ID (e.g., plw-cd0c54210e09823b8103e502f40ea0f9) |
| client_id | UUID | Required | Your client UUID (query parameter) |
Response Fields
| Name | Type | Description |
|---|---|---|
| withdrawal_id | string | Unique system-generated withdrawal identifier (e.g., plw-cd0c54210e09823b8103e502f40ea0f9) |
| status | string | Withdrawal status: new, in_progress, success, or failed |
| amount | float | Withdrawal amount in specified currency |
| currency_code | string | ISO currency code (e.g., INR, USD) |
| parent_name | string | Name of the bank or payment system (e.g., IMPS, UPI) |
| type | string | Withdrawal type: imps or bkash |
| client_user | string | User identifier provided during withdrawal creation |
| creation_timestamp | integer | Unix timestamp when withdrawal was initiated by client |
| add_timestamp | integer | Unix timestamp when withdrawal was added to system |
| completion_timestamp | integer | null | Unix timestamp when withdrawal was completed (null if pending) |
| utr | string | null | Bank UTR/reference number (available after successful completion) |
| client_withdrawal_id | string | null | Your custom withdrawal ID if provided during creation |
| custom_field_values | array | Array of custom field objects containing bank-specific details (account_number, ifsc_code, account_holder_name, etc.) |
GET {{API_BASE}}/api/v1/external/withdrawals/plw-cd0c54210e09823b8103e502f40ea0f9?client_id=545XXXXXXXXXXXXXXXXXXXXXXXXXXUI
Headers (signature accounts โ sign the full path INCLUDING the query string):
X-Timestamp: 1730880000
X-Nonce: 8f14e45fceea167a5a36dedd4bea2543
X-Signature: <hex hmac-sha256 digest>
Headers (API key accounts):
X-API-Key: your-api-key{
"withdrawal_id": "plw-cd0c54210e09823b8103e502f40ea0f9",
"status": "success",
"amount": 5000.0,
"currency_code": "INR",
"parent_name": "IMPS",
"type": "imps",
"client_user": "user123",
"creation_timestamp": 1763359236,
"add_timestamp": 1763390633,
"completion_timestamp": 1763391200,
"utr": "HDFC123456789",
"client_withdrawal_id": "WD20231106001",
"custom_field_values": [
{
"field_key": "account_number",
"field_value": "123456789012"
},
{
"field_key": "ifsc_code",
"field_value": "HDFC0001234"
},
{
"field_key": "account_holder_name",
"field_value": "John Doe"
}
]
}{
"detail": "Invalid API Key"
}{
"detail": "Could not find withdrawal"
}{
"detail": [
{
"loc": ["query", "client_id"],
"msg": "value is not a valid uuid",
"type": "type_error.uuid"
}
]
}Withdrawal Webhook (Postback)
Your Webhook URLReceive real-time withdrawal status updates when a withdrawal is completed or failed. Configure your withdrawal_postback_url in client settings; callbacks are signed with your signing_key. Your endpoint must return HTTP 200 to acknowledge receipt โ any non-200 (or network error) triggers a retry. Defaults: up to 5 attempts, ~10s apart (configurable per client).
Signature Verification (HMAC-SHA256)
Every callback is signed with HMAC-SHA256 using your signing_key โ the same secret you sign API requests with. The signature is in the headers, not the body:
X-Timestamp and X-Signature headersX-Timestamp is more than 5 minutes from your own clockTIMESTAMP + \n + SHA256_HEX(raw_body)HMAC-SHA256(signing_key, canonical string), hex-encode it, and compare against X-Signature using a constant-time comparisonIdentical to the pay-in webhook scheme, keyed by the same signing_key. See Request Signing for a ready-made Python verifier.
Node.js Example (HMAC-SHA256)
const express = require("express");
const crypto = require("crypto");
const app = express();
const SIGNING_KEY = "your_signing_key";
// raw body is required โ the signature covers the exact bytes we sent
app.post("/webhook/withdrawals", express.raw({ type: "*/*" }), (req, res) => {
const ts = req.get("X-Timestamp") || "";
if (Math.abs(Date.now() / 1000 - parseInt(ts, 10)) > 300) {
return res.status(401).json({ status: 401, message: "Stale callback" });
}
const bodyHash = crypto.createHash("sha256").update(req.body).digest("hex");
const expected = crypto
.createHmac("sha256", SIGNING_KEY)
.update(`${ts}\n${bodyHash}`)
.digest("hex");
const received = req.get("X-Signature") || "";
if (
expected.length !== received.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
) {
return res.status(401).json({ status: 401, message: "Invalid signature" });
}
const payload = JSON.parse(req.body); // safe to parse once verified
console.log(payload.withdrawal_id, payload.status);
res.status(200).json({ status: 200, message: "OK" });
});PHP Example (HMAC-SHA256)
<?php
$signingKey = "your_signing_key";
$raw = file_get_contents("php://input"); // raw bytes, BEFORE parsing
$ts = $_SERVER["HTTP_X_TIMESTAMP"] ?? "";
$sig = $_SERVER["HTTP_X_SIGNATURE"] ?? "";
if (abs(time() - intval($ts)) > 300) {
http_response_code(401);
exit(json_encode(["status" => 401, "message" => "Stale callback"]));
}
$canonical = $ts . "\n" . hash("sha256", $raw);
$expected = hash_hmac("sha256", $canonical, $signingKey);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit(json_encode(["status" => 401, "message" => "Invalid signature"]));
}
$payload = json_decode($raw, true); // safe to parse once verified
// $payload["status"] is "success" or "failed"
http_response_code(200);
echo json_encode(["status" => 200, "message" => "OK"]);Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| withdrawal_id | string | Required | System withdrawal ID (e.g., plw-xxx) |
| client_withdrawal_id | string | Optional | Your withdrawal ID (if provided during creation) |
| status | string | Required | Withdrawal status: success or failed |
| amount | float | Required | Withdrawal amount |
| currency_code | string | Required | Currency code (e.g., INR) |
| parent_name | string | Required | Bank/Payment system name (e.g., IMPS) |
| type | string | Required | Payment type (e.g., imps) |
| client_user | string | Required | User identifier |
| creation_timestamp | integer | Required | Unix timestamp of withdrawal creation |
| utr | string | Optional | Bank UTR reference number (present when completed) |
| custom_fields_values | array | Required | Array of custom field key-value pairs (e.g., account_number, ifsc_code, account_holder_name) |
| sign | string | Required | Deprecated โ no longer sent. Verify the X-Signature header instead (see verification steps) |
// Webhook Payload (POST JSON to your URL)
{
"withdrawal_id": "plw-cd0c54210e09823b8103e502f40ea0f9",
"client_withdrawal_id": "WD20231106001",
"status": "success",
"amount": 5000.00,
"currency_code": "INR",
"parent_name": "IMPS",
"type": "imps",
"client_user": "user123",
"creation_timestamp": 1763359236,
"utr": "HDFC123456789",
"custom_fields_values": [
{"account_number": "123456789012"},
{"ifsc_code": "HDFC0001234"},
{"account_holder_name": "John Doe"}
],
"sign": "8f3e7a6b2c9d1f4e5a7b3c6d9e2f1a4b"
}// Your endpoint must return HTTP 200:
{
"status": 200,
"message": "OK"
}{ "status": 200, "message": "OK" }{ "status": 500, "message": "Invalid webhook signature" }