{{BRAND}} Payments API

{{API_BASE}}/api/v1/external

The {{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-Signature HMAC-SHA256 digest over the request body, and no API key is sent. See Request Signing.
  • API key โ€” a secret sent in the X-API-Key header. Requests without a valid key are rejected with 401 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 params
  • postback_url โ€” receive pay-in webhooks (verified with your signing_key)
  • withdrawal_postback_url โ€” receive payout webhooks (verified with your signing_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 is activated.
  • 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).

Base URL
{{API_BASE}}/api/v1/external
Signed Request (cURL)
PATH="/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"
API Key Request (cURL)
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-SHA256

Sign 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

1Serialize your JSON body exactly as you will send it. Sign the raw bytes โ€” do not re-serialize after signing. For GET requests the body is empty.
2Build the canonical string, joined by newlines (\n):
METHOD ยท PATH (including the query string) ยท TIMESTAMP ยท NONCE ยท SHA256_HEX(body)
3Compute HMAC-SHA256(signing key, canonical string) and hex-encode it (lowercase).
4Send it as X-Signature, along with X-Timestamp (unix seconds) and X-Nonce (unique per request).

Headers

NameRequiredDescription
X-SignatureRequiredLowercase hex HMAC-SHA256 digest of the canonical string
X-TimestampRequiredUnix seconds. Rejected if more than 5 minutes from server time
X-NonceRequiredUnique random value per request (max 128 chars). A repeat within the 5-minute window is rejected
X-Merchant-IdOptionalYour 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.

Python
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",
})
Node.js
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,
    },
  });
}
Verify a webhook (Python)
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", ""))
Signed GET
# 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(),
])
POST

Create Pay-In Transaction

{{API_BASE}}/api/v1/external/payin

Creates 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:

1postback_url โ€” Your server endpoint that receives transaction webhooks (POST)
2signing_key โ€” One secret: signs your API requests AND verifies every webhook we send you
3withdrawal_postback_url โ€” Your server endpoint that receives withdrawal webhooks (POST)
4withdrawal_postback_url โ€” Where withdrawal webhooks are delivered

Headers

NameRequiredDescription
X-SignatureSignature accountsHMAC-SHA256 digest of this request โ€” see Request Signing
X-TimestampSignature accountsUnix seconds, within 5 minutes of server time
X-NonceSignature accountsUnique random value per request
X-API-KeyAPI key accountsYour external API key. Not accepted on signature accounts
Content-TypeRequiredRequest content type

Parameters

NameTypeRequiredDescription
amountfloatRequiredTransaction amount
currencystringRequired3-letter currency code (e.g., INR)
client_userstringRequiredUser identifier
client_idUUIDRequiredYour client UUID
client_transaction_idstringRequiredYour unique transaction ID
payment_option_namestringRequiredPayment method (UPI, IMPS, etc.)

Response Fields

NameTypeDescription
payin_idstringUnique pay-in identifier
transaction_idstringTransaction ID for status checks and webhook matching
qr_linkstring | nullUPI QR code link. null for non-UPI methods (IMPS, etc.)
payment_detailsobject | nullUPI: {upi_id, account_name} ยท IMPS: {account_name, account_number, ifsc_code}
payment_option_namestringPayment method used (UPI, IMPS, etc.)
client_transaction_idstringYour transaction ID echoed back
client_userstringYour user identifier echoed back
payment_page_urlstringRedirect 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_timeintegerUnix timestamp โ€” transaction expires 10 minutes after creation
parsing_typeinteger | nullInternal routing hint for how the account is verified. You can ignore it.
Request Example
{
  "amount": 1000.50,
  "currency": "INR",
  "client_user": "user123",
  "client_id": "545XXXXXXXXXXXXXXXXXXXXXXXXXXUI",
  "client_transaction_id": "TXN20231106001",
  "payment_option_name": "UPI"
}
Response Example
{
  "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
}
Error Responses
400Bad Request - Client not found, or no account available for the requested payment option / amount
{
  "detail": "No accounts found for payment option \"UPI\". Available payment options: IMPS, UPI"
}
401Unauthorized - Invalid or missing API key
{
  "detail": "Invalid API Key"
}
422Unprocessable Entity - Duplicate client_transaction_id (unique per client)
{
  "detail": "Payin with client_transaction_id: TXN20231106001 already exists"
}
422Unprocessable Entity - Request body validation (missing field, bad UUID, currency not 3 uppercase letters)
{
  "detail": [
    {
      "loc": ["body", "currency"],
      "msg": "string does not match regex \"[A-Z]{3}\"",
      "type": "value_error.str.regex"
    }
  ]
}
GET

Check Transaction Status

{{API_BASE}}/api/v1/external/transactions/check

Check 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.

StatusFinal?Description
activatedFinal โœ“Payment confirmed and verified. Safe to credit the user.
fakeFinal โœ“Transaction marked as fraudulent or invalid. Do NOT credit the user.
non_activatedPendingPayment received but not yet verified by the system. Will transition to activated or fake.
non_paidPendingAwaiting 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

NameRequiredDescription
X-SignatureSignature accountsHMAC-SHA256 digest of this request โ€” see Request Signing
X-TimestampSignature accountsUnix seconds, within 5 minutes of server time
X-NonceSignature accountsUnique random value per request
X-API-KeyAPI key accountsYour external API key. Not accepted on signature accounts

Parameters

NameTypeRequiredDescription
client_idUUIDOptionalYour client UUID (required with client_transaction_id)
client_transaction_idstringOptionalYour unique transaction ID (required with client_id)
transaction_idstringOptionalSystem transaction ID
UTRstringOptionalBank UTR reference number

Response Fields

NameTypeDescription
statusstringCurrent transaction status โ€” see Status Reference above
transaction_idstringSystem transaction ID
amountfloatTransaction amount
currencystringCurrency code (e.g., INR)
creation_timestampintegerUnix timestamp when transaction was created
add_timestampintegerUnix timestamp when transaction was registered in system
activation_timestampinteger | nullUnix timestamp when payment was confirmed. null if not yet activated
client_userstringYour user identifier
Request Example
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
Response Example
{
  "status": "activated",
  "transaction_id": "txn-pay-cfcbd95213647d81e5f6903f3eb2f747",
  "amount": 1000.50,
  "currency": "INR",
  "creation_timestamp": 1699268400,
  "add_timestamp": 1699268450,
  "activation_timestamp": 1699268500,
  "client_user": "user123"
}
Error Responses
422Unprocessable Entity - No lookup parameter provided, or client_id missing when searching by UTR / client_transaction_id
{
  "detail": "One of the parameters: transaction_id, UTR, client_transaction_id must be specified"
}
404Not Found - No matching transaction
{
  "detail": "Could not find transaction"
}
POST

UTR

{{API_BASE}}/api/v1/external/transactions/utr

Store 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

NameRequiredDescription
X-SignatureSignature accountsHMAC-SHA256 digest of this request โ€” see Request Signing
X-TimestampSignature accountsUnix seconds, within 5 minutes of server time
X-NonceSignature accountsUnique random value per request
X-API-KeyAPI key accountsYour external API key. Not accepted on signature accounts
Content-TypeRequiredRequest content type

Parameters

NameTypeRequiredDescription
client_idUUIDRequiredYour client UUID
transaction_idstringOptionalSystem transaction ID (TXN-PAY-...). Required if client_transaction_id is not provided
client_transaction_idstringOptionalYour unique transaction ID (from pay-in). Required if transaction_id is not provided
utrstringRequiredUTR / merchantOrderId / bank reference number to store
Request Example
// 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"
}
Response Example
{
  "success": true,
  "message": "UTR stored successfully. Transaction will be verified automatically.",
  "transaction_id": "txn-pay-cfcbd95213647d81e5f6903f3eb2f747"
}
Error Responses
401Unauthorized - Invalid or missing API key
{
  "detail": "Invalid API Key"
}
403Forbidden - Transaction does not belong to this client
{
  "detail": "Transaction does not belong to this client"
}
404Not Found - Transaction not found
{
  "detail": "Transaction not found by transaction_id=txn-pay-abc123"
}
400Bad Request - Transaction is not an incoming deposit
{
  "detail": "UTR can only be stored for incoming (deposit) transactions"
}
422Unprocessable Entity - Validation errors (must provide exactly one identifier)
{
  "detail": [
    {
      "loc": ["body"],
      "msg": "Either transaction_id or client_transaction_id must be provided",
      "type": "value_error"
    }
  ]
}
POST

Transaction Webhook (Postback)

Your Webhook URL

Sent 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 fieldEquivalent statusWhat to do
postback_is_fake: falseactivatedPayment confirmed โ€” credit the user.
postback_is_fake: truefakeMarked 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:

1Read the raw body bytes before parsing the JSON, plus the X-Timestamp and X-Signature headers
2Reject the callback if X-Timestamp is more than 5 minutes from your own clock
3Build the canonical string TIMESTAMP + \n + SHA256_HEX(raw_body)
4Compute HMAC-SHA256(signing_key, canonical string), hex-encode it, and compare against X-Signature using a constant-time comparison

Python 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"}), 200

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/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

NameTypeRequiredDescription
postback_idintegerRequiredUnique postback identifier
postback_typeintegerRequired1 = Payin transaction
postback_is_fakebooleanRequiredfalse = payment confirmed (activated). true = transaction marked as fake/fraudulent โ€” do NOT credit the user
creation_typestringRequired"auto" = activated by parser/system. "manual" = activated by admin
client_userstringRequiredYour customer/user identifier
transactionsarrayRequiredAlways contains exactly ONE transaction object (single-element array). See Transaction Fields below.
signstringRequiredDeprecated โ€” no longer sent. Verify the X-Signature header instead (see verification steps)

Response Fields

NameTypeDescription
transactions[].transaction_idstringSystem transaction ID (e.g., txn-pay-xxx)
transactions[].client_transaction_idstringYour transaction ID (if provided during pay-in creation)
transactions[].transaction_amountfloatTransaction amount
transactions[].transaction_currency_codestringCurrency code (e.g., INR)
transactions[].creation_timestampintegerUnix timestamp of transaction creation
transactions[].payment_method_idintegerPayment method ID
transactions[].payment_detailsstringPayment details (UPI ID, account number, etc.)
transactions[].payment_method_namestringPayment method name (UPI, IMPS, etc.)
transactions[].transaction_custom_fieldsarrayCustom fields provided during transaction creation
Request Example
// 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"
}
Response Example
// Your endpoint must return HTTP 200:
{
  "status": 200,
  "message": "OK"
}
Error Responses
200OK - Webhook received successfully. Always return 200 to stop retries.
{ "status": 200, "message": "OK" }
500Any non-200 triggers a retry (defaults: up to 5 attempts, ~10s apart).
{ "status": 500, "message": "Invalid webhook signature" }
POST

Create Withdrawal

{{API_BASE}}/api/v1/external/withdrawals

Initiate 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

StatusFinal?Description
newPendingWithdrawal created, queued for processing.
in_progressPendingWithdrawal is being processed by the payment provider.
successFinal โœ“Payout completed successfully. UTR will be present in the response.
failedFinal โœ“Payout failed. The amount will not be debited.

Headers

NameRequiredDescription
X-SignatureSignature accountsHMAC-SHA256 digest of this request โ€” see Request Signing
X-TimestampSignature accountsUnix seconds, within 5 minutes of server time
X-NonceSignature accountsUnique random value per request
X-API-KeyAPI key accountsYour external API key. Not accepted on signature accounts
Content-TypeRequiredRequest content type

Parameters

NameTypeRequiredDescription
parent_typestringRequiredExactly bank or payment_system โ€” lowercase. Any other value returns 422
parent_namestringRequiredName 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
typestringRequiredExactly imps or bkash โ€” lowercase. Any other value returns 422
client_idUUIDRequiredYour client UUID
client_userstringRequiredUser identifier
amountfloatRequiredWithdrawal amount
creation_timestampintegerRequiredUnix timestamp
custom_field_valuesarrayOptionalArray 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_idstringOptionalYour unique withdrawal ID โ€” alphanumeric, max 50 chars
Request Example
{
  "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"
}
Response Example
{
  "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"
    }
  ]
}
Error Responses
401Unauthorized - Invalid API key
{
  "detail": "Invalid API Key"
}
404Not Found - No Bank (any client) or Payment System (yours) matches that parent_name
{
  "detail": "Bank with name \"IMPS\" not found for this client"
}
409Conflict - parent_name matches several entries differing only by case. Send the exact name.
{
  "detail": "Bank name \"imps\" is ambiguous โ€” it matches 2 entries differing only by case. Use the exact name."
}
422Unprocessable Entity - Unknown custom field key(s) for this bank
{
  "detail": "Invalid custom field keys: ['acount_number']. Available fields for Bank \"IMPS\": account_number, ifsc_code, account_holder_name"
}
422Unprocessable Entity - Duplicate client_withdrawal_id (unique per client)
{
  "detail": "Withdrawal with client_withdrawal_id: WD20231106001 already exists"
}
GET

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

StatusFinal?Description
newPendingWithdrawal created, queued for processing.
in_progressPendingWithdrawal is being processed by the payment provider.
successFinal โœ“Payout completed successfully. UTR will be present in the response.
failedFinal โœ“Payout failed. The amount will not be debited.

Headers

NameRequiredDescription
X-SignatureSignature accountsHMAC-SHA256 digest of this request โ€” see Request Signing
X-TimestampSignature accountsUnix seconds, within 5 minutes of server time
X-NonceSignature accountsUnique random value per request
X-API-KeyAPI key accountsYour external API key. Not accepted on signature accounts

Parameters

NameTypeRequiredDescription
withdrawal_idstringRequiredSystem-generated withdrawal ID (e.g., plw-cd0c54210e09823b8103e502f40ea0f9)
client_idUUIDRequiredYour client UUID (query parameter)

Response Fields

NameTypeDescription
withdrawal_idstringUnique system-generated withdrawal identifier (e.g., plw-cd0c54210e09823b8103e502f40ea0f9)
statusstringWithdrawal status: new, in_progress, success, or failed
amountfloatWithdrawal amount in specified currency
currency_codestringISO currency code (e.g., INR, USD)
parent_namestringName of the bank or payment system (e.g., IMPS, UPI)
typestringWithdrawal type: imps or bkash
client_userstringUser identifier provided during withdrawal creation
creation_timestampintegerUnix timestamp when withdrawal was initiated by client
add_timestampintegerUnix timestamp when withdrawal was added to system
completion_timestampinteger | nullUnix timestamp when withdrawal was completed (null if pending)
utrstring | nullBank UTR/reference number (available after successful completion)
client_withdrawal_idstring | nullYour custom withdrawal ID if provided during creation
custom_field_valuesarrayArray of custom field objects containing bank-specific details (account_number, ifsc_code, account_holder_name, etc.)
Request Example
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
Response Example
{
  "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"
    }
  ]
}
Error Responses
401Unauthorized - Invalid API key
{
  "detail": "Invalid API Key"
}
404Not Found - No withdrawal matches this withdrawal_id for your client_id (a wrong client_id returns 404, not 403)
{
  "detail": "Could not find withdrawal"
}
422Unprocessable Entity - Missing or malformed client_id query parameter (must be a valid UUID)
{
  "detail": [
    {
      "loc": ["query", "client_id"],
      "msg": "value is not a valid uuid",
      "type": "type_error.uuid"
    }
  ]
}
POST

Withdrawal Webhook (Postback)

Your Webhook URL

Receive 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:

1Read the raw body bytes before parsing the JSON, plus the X-Timestamp and X-Signature headers
2Reject the callback if X-Timestamp is more than 5 minutes from your own clock
3Build the canonical string TIMESTAMP + \n + SHA256_HEX(raw_body)
4Compute HMAC-SHA256(signing_key, canonical string), hex-encode it, and compare against X-Signature using a constant-time comparison

Identical 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

NameTypeRequiredDescription
withdrawal_idstringRequiredSystem withdrawal ID (e.g., plw-xxx)
client_withdrawal_idstringOptionalYour withdrawal ID (if provided during creation)
statusstringRequiredWithdrawal status: success or failed
amountfloatRequiredWithdrawal amount
currency_codestringRequiredCurrency code (e.g., INR)
parent_namestringRequiredBank/Payment system name (e.g., IMPS)
typestringRequiredPayment type (e.g., imps)
client_userstringRequiredUser identifier
creation_timestampintegerRequiredUnix timestamp of withdrawal creation
utrstringOptionalBank UTR reference number (present when completed)
custom_fields_valuesarrayRequiredArray of custom field key-value pairs (e.g., account_number, ifsc_code, account_holder_name)
signstringRequiredDeprecated โ€” no longer sent. Verify the X-Signature header instead (see verification steps)
Request Example
// 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"
}
Response Example
// Your endpoint must return HTTP 200:
{
  "status": 200,
  "message": "OK"
}
Error Responses
200OK - Webhook received successfully. Always return 200 to stop retries.
{ "status": 200, "message": "OK" }
500Any non-200 triggers a retry (defaults: up to 5 attempts, ~10s apart).
{ "status": 500, "message": "Invalid webhook signature" }