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

Every request must include your secret API key in the X-API-Key header. Requests without a valid key are rejected with 401 Unauthorized. Keep this key server-side โ€” never expose it in browser or mobile clients.

Your Credentials

Your account manager will provision the following. None of them are set in the API request itself:

  • X-API-Key โ€” secret key sent on every request
  • client_id โ€” your client UUID, sent in request bodies / query params
  • postback_url + client_postback_key โ€” receive & verify pay-in webhooks
  • withdrawal_postback_url + withdrawal_postback_key โ€” receive & verify payout webhooks

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 API key ยท 403 not your resource ยท 404 not found ยท 422 validation error (including a duplicate client_transaction_id / client_withdrawal_id).

Base URL
{{API_BASE}}/api/v1/external
Authenticated 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"
  }'
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)
2client_postback_key โ€” Secret key used to verify transaction webhook signatures
3withdrawal_postback_url โ€” Your server endpoint that receives withdrawal webhooks (POST)
4withdrawal_postback_key โ€” Secret key used to verify withdrawal webhook signatures

Headers

NameRequiredDescription
X-API-KeyRequiredYour external API key
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-API-KeyRequiredYour external API key

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
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-API-KeyRequiredYour external API key
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 and client_postback_key in client settings. 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).

Signature Verification

Every webhook request includes a sign field. You must verify this signature to ensure the request is authentic.

1Extract and remove the sign field from the received JSON payload
2Add your client_postback_key to the data as: {"client_postback_key": "your_key_here"}
3Sort all keys alphabetically, then URL-encode the key-value pairs
4Compute the MD5 hash of the URL-encoded string
5Compare the computed hash with the received sign โ€” if they match, the request is authentic

Python Example

import hashlib
from urllib.parse import urlencode
from flask import Flask, request, jsonify

app = Flask(__name__)
CLIENT_POSTBACK_KEY = "your_client_postback_key"

def verify_signature(payload, key):
    received_sign = payload.get("sign")
    if not received_sign:
        return False
    data = {k: v for k, v in payload.items() if k != "sign"}
    data["client_postback_key"] = key
    # IMPORTANT: Sort keys alphabetically
    expected_sign = hashlib.md5(
        urlencode(sorted(data.items())).encode("utf-8")
    ).hexdigest()
    return expected_sign == received_sign

@app.route("/webhook/transactions", methods=["POST"])
def handle_webhook():
    payload = request.get_json()
    if not verify_signature(payload, CLIENT_POSTBACK_KEY):
        return jsonify({"status": 500, "message": "Invalid signature"}), 500
    # Process transaction...
    for txn in payload.get("transactions", []):
        txn_id = txn["transaction_id"]
        amount = txn["transaction_amount"]
        print(f"Transaction {txn_id}: {amount}")
    return jsonify({"status": 200, "message": "OK"}), 200

Node.js Example

const express = require("express");
const crypto = require("crypto");

const app = express();
// Preserve raw body for accurate float detection
app.use(express.json({
  verify: (req, res, buf) => { req.rawBody = buf.toString("utf-8"); }
}));
const CLIENT_POSTBACK_KEY = "your_client_postback_key";

// Match Python's quote_plus encoding (differs from encodeURIComponent)
function quotePlus(s) {
  return encodeURIComponent(String(s))
    .replace(/%20/g, "+")
    .replace(/'/g, "%27")
    .replace(/!/g, "%21")
    .replace(/\*/g, "%2A")
    .replace(/\(/g, "%28")
    .replace(/\)/g, "%29");
}

// Detect numeric fields that had decimal points in the raw JSON
// (JavaScript's JSON.parse loses float vs int distinction: 5000.0 -> 5000)
function detectFloatKeys(rawJson) {
  const floatKeys = new Set();
  const regex = /"(\w+)"\s*:\s*-?\d+\.\d+/g;
  let m;
  while ((m = regex.exec(rawJson)) !== null) {
    floatKeys.add(m[1]);
  }
  return floatKeys;
}

// Convert a JS value to Python repr() style (with quotes for strings)
function pythonRepr(value, floatKeys, key) {
  if (value === null || value === undefined) return "None";
  if (value === true) return "True";
  if (value === false) return "False";
  if (typeof value === "number") {
    if (Number.isInteger(value) && floatKeys && floatKeys.has(key))
      return value + ".0";
    return String(value);
  }
  if (typeof value === "string") return "\'" + value + "\'";
  if (Array.isArray(value)) {
    return "[" + value.map(v => pythonRepr(v, floatKeys, null)).join(", ") + "]";
  }
  if (typeof value === "object") {
    const items = Object.entries(value)
      .map(([k, v]) => "\'" + k + "\': " + pythonRepr(v, floatKeys, k));
    return "{" + items.join(", ") + "}";
  }
  return String(value);
}

// Convert a JS value to Python str() (no quotes for strings)
function pythonStr(value, floatKeys, key) {
  if (typeof value === "string") return value;
  return pythonRepr(value, floatKeys, key);
}

function verifySignature(payload, rawBody, key) {
  const receivedSign = payload.sign;
  if (!receivedSign) return false;
  const floatKeys = detectFloatKeys(rawBody);
  const data = { ...payload };
  delete data.sign;
  data.client_postback_key = key;
  // Sort keys alphabetically, build URL-encoded string
  const sortedKeys = Object.keys(data).sort();
  const parts = sortedKeys.map(k =>
    quotePlus(k) + "=" + quotePlus(pythonStr(data[k], floatKeys, k))
  );
  const str2hash = parts.join("&");
  const expectedSign = crypto
    .createHash("md5")
    .update(str2hash)
    .digest("hex");
  return expectedSign === receivedSign;
}

app.post("/webhook/transactions", (req, res) => {
  const payload = req.body;
  if (!verifySignature(payload, req.rawBody, CLIENT_POSTBACK_KEY)) {
    return res.status(500).json({
      status: 500, message: "Invalid signature"
    });
  }
  // Process transactions...
  payload.transactions.forEach(txn => {
    console.log(txn.transaction_id, txn.transaction_amount);
  });
  return res.status(200).json({ status: 200, message: "OK" });
});

app.listen(5000);

PHP Example

<?php
$client_postback_key = "your_client_postback_key";
$payload = json_decode(file_get_contents("php://input"), true);

$received_sign = $payload["sign"];
unset($payload["sign"]);
$payload["client_postback_key"] = $client_postback_key;

// Convert PHP value to Python repr (with quotes for strings)
function python_repr($value) {
    if (is_null($value)) return "None";
    if (is_bool($value)) return $value ? "True" : "False";
    if (is_int($value)) return (string)$value;
    if (is_float($value)) {
        $s = (string)$value;
        if (strpos($s, ".") === false && strpos($s, "E") === false) $s .= ".0";
        return $s;
    }
    if (is_string($value)) return "\'" . $value . "\'";
    if (is_array($value)) {
        if (array_values($value) === $value) {
            // Sequential array -> Python list
            $items = array_map("python_repr", $value);
            return "[" . implode(", ", $items) . "]";
        } else {
            // Associative array -> Python dict
            $items = [];
            foreach ($value as $k => $v) {
                $items[] = "\'" . $k . "\': " . python_repr($v);
            }
            return "{" . implode(", ", $items) . "}";
        }
    }
    return (string)$value;
}

// Convert PHP value to Python str (no quotes for strings)
function python_str($value) {
    if (is_string($value)) return $value;
    return python_repr($value);
}

// Sort keys alphabetically, URL-encode matching Python urlencode
ksort($payload);
$parts = [];
foreach ($payload as $key => $value) {
    $parts[] = urlencode((string)$key) . "=" . urlencode(python_str($value));
}
$str2hash = implode("&", $parts);
$expected_sign = md5($str2hash);

if ($expected_sign === $received_sign) {
    foreach ($payload["transactions"] as $txn) {
        // Process each transaction
    }
    http_response_code(200);
    echo json_encode(["status" => 200, "message" => "OK"]);
} else {
    http_response_code(500);
    echo json_encode(["status" => 500, "message" => "Invalid signature"]);
}
?>

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.
signstringRequiredMD5 signature for verification. Computed using client_postback_key (see verification steps below)

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/Payment System for your client (case-insensitive). Custom fields vary by bank - check your configured banks for available fields.

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-API-KeyRequiredYour external API key
Content-TypeRequiredRequest content type

Parameters

NameTypeRequiredDescription
parent_typestringRequiredbank or payment_system
parent_namestringRequiredExact name of configured bank (e.g., IMPS, UPI)
typestringRequiredimps or bkash
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.
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 / payment system with that parent_name for your client
{
  "detail": "Bank with name \"IMPS\" not found for this client"
}
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-API-KeyRequiredYour external API key

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:
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 and withdrawal_postback_key in client settings. 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

Every webhook request includes a sign field. You must verify this signature to ensure the request is authentic.

1Extract and remove the sign field from the received JSON payload
2Add your withdrawal_postback_key to the data as: {"withdrawal_postback_key": "your_key_here"}
3URL-encode all key-value pairs (do NOT sort the keys)
4Compute the MD5 hash of the URL-encoded string
5Compare the computed hash with the received sign โ€” if they match, the request is authentic

Python Example

import hashlib
from urllib.parse import urlencode
from flask import Flask, request, jsonify

app = Flask(__name__)
WITHDRAWAL_POSTBACK_KEY = "your_withdrawal_postback_key"

def verify_signature(payload, key):
    received_sign = payload.get("sign")
    if not received_sign:
        return False
    data = {k: v for k, v in payload.items() if k != "sign"}
    data["withdrawal_postback_key"] = key
    expected_sign = hashlib.md5(
        urlencode(data).encode("utf-8")
    ).hexdigest()
    return expected_sign == received_sign

@app.route("/webhook/payouts", methods=["POST"])
def handle_webhook():
    payload = request.get_json()
    if not verify_signature(payload, WITHDRAWAL_POSTBACK_KEY):
        return jsonify({"status": 500, "message": "Invalid signature"}), 500
    # Process withdrawal...
    withdrawal_id = payload["withdrawal_id"]
    status = payload["status"]  # "success" or "failed"
    return jsonify({"status": 200, "message": "OK"}), 200

Node.js Example

const express = require("express");
const crypto = require("crypto");

const app = express();
// Preserve raw body for accurate float detection
app.use(express.json({
  verify: (req, res, buf) => { req.rawBody = buf.toString("utf-8"); }
}));
const WITHDRAWAL_POSTBACK_KEY = "your_withdrawal_postback_key";

// Match Python's quote_plus encoding (differs from encodeURIComponent)
function quotePlus(s) {
  return encodeURIComponent(String(s))
    .replace(/%20/g, "+")
    .replace(/'/g, "%27")
    .replace(/!/g, "%21")
    .replace(/\*/g, "%2A")
    .replace(/\(/g, "%28")
    .replace(/\)/g, "%29");
}

// Detect numeric fields that had decimal points in the raw JSON
function detectFloatKeys(rawJson) {
  const floatKeys = new Set();
  const regex = /"(\w+)"\s*:\s*-?\d+\.\d+/g;
  let m;
  while ((m = regex.exec(rawJson)) !== null) {
    floatKeys.add(m[1]);
  }
  return floatKeys;
}

function pythonRepr(value, floatKeys, key) {
  if (value === null || value === undefined) return "None";
  if (value === true) return "True";
  if (value === false) return "False";
  if (typeof value === "number") {
    if (Number.isInteger(value) && floatKeys && floatKeys.has(key))
      return value + ".0";
    return String(value);
  }
  if (typeof value === "string") return "\'" + value + "\'";
  if (Array.isArray(value)) {
    return "[" + value.map(v => pythonRepr(v, floatKeys, null)).join(", ") + "]";
  }
  if (typeof value === "object") {
    const items = Object.entries(value)
      .map(([k, v]) => "\'" + k + "\': " + pythonRepr(v, floatKeys, k));
    return "{" + items.join(", ") + "}";
  }
  return String(value);
}

function pythonStr(value, floatKeys, key) {
  if (typeof value === "string") return value;
  return pythonRepr(value, floatKeys, key);
}

function verifySignature(payload, rawBody, key) {
  const receivedSign = payload.sign;
  if (!receivedSign) return false;
  const floatKeys = detectFloatKeys(rawBody);
  const data = { ...payload };
  delete data.sign;
  data.withdrawal_postback_key = key;
  // URL-encode key-value pairs (do NOT sort keys)
  const parts = Object.entries(data).map(([k, v]) =>
    quotePlus(k) + "=" + quotePlus(pythonStr(v, floatKeys, k))
  );
  const str2hash = parts.join("&");
  const expectedSign = crypto
    .createHash("md5")
    .update(str2hash)
    .digest("hex");
  return expectedSign === receivedSign;
}

app.post("/webhook/payouts", (req, res) => {
  const payload = req.body;
  if (!verifySignature(payload, req.rawBody, WITHDRAWAL_POSTBACK_KEY)) {
    return res.status(500).json({
      status: 500, message: "Invalid signature"
    });
  }
  // Process withdrawal...
  console.log(payload.withdrawal_id, payload.status);
  return res.status(200).json({ status: 200, message: "OK" });
});

app.listen(5000);

PHP Example

<?php
$withdrawal_postback_key = "your_withdrawal_postback_key";
$payload = json_decode(file_get_contents("php://input"), true);

$received_sign = $payload["sign"];
unset($payload["sign"]);
$payload["withdrawal_postback_key"] = $withdrawal_postback_key;

// Convert PHP value to Python repr (with quotes for strings)
function python_repr($value) {
    if (is_null($value)) return "None";
    if (is_bool($value)) return $value ? "True" : "False";
    if (is_int($value)) return (string)$value;
    if (is_float($value)) {
        $s = (string)$value;
        if (strpos($s, ".") === false && strpos($s, "E") === false) $s .= ".0";
        return $s;
    }
    if (is_string($value)) return "\'" . $value . "\'";
    if (is_array($value)) {
        if (array_values($value) === $value) {
            $items = array_map("python_repr", $value);
            return "[" . implode(", ", $items) . "]";
        } else {
            $items = [];
            foreach ($value as $k => $v) {
                $items[] = "\'" . $k . "\': " . python_repr($v);
            }
            return "{" . implode(", ", $items) . "}";
        }
    }
    return (string)$value;
}

function python_str($value) {
    if (is_string($value)) return $value;
    return python_repr($value);
}

// URL-encode key-value pairs (do NOT sort keys)
$parts = [];
foreach ($payload as $key => $value) {
    $parts[] = urlencode((string)$key) . "=" . urlencode(python_str($value));
}
$str2hash = implode("&", $parts);
$expected_sign = md5($str2hash);

if ($expected_sign === $received_sign) {
    http_response_code(200);
    echo json_encode(["status" => 200, "message" => "OK"]);
} else {
    http_response_code(500);
    echo json_encode(["status" => 500, "message" => "Invalid signature"]);
}
?>

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)
signstringRequiredMD5 signature for verification. Computed using withdrawal_postback_key (see verification steps above)
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" }