{{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
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 requestclient_idโ your client UUID, sent in request bodies / query paramspostback_url+client_postback_keyโ receive & verify pay-in webhookswithdrawal_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 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 API key ยท 403 not your resource ยท 404 not found ยท 422 validation error (including a duplicate client_transaction_id / client_withdrawal_id).
{{API_BASE}}/api/v1/externalcurl -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"
}'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-API-Key | Required | Your external API key |
| 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-API-Key | Required | Your external API key |
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{
"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-API-Key | Required | Your external API key |
| 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 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.
sign field from the received JSON payloadclient_postback_key to the data as: {"client_postback_key": "your_key_here"}sign โ if they match, the request is authenticPython 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"}), 200Node.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
| 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 | MD5 signature for verification. Computed using client_postback_key (see verification steps below) |
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/Payment System for your client (case-insensitive). Custom fields vary by bank - check your configured banks for available fields.
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-API-Key | Required | Your external API key |
| Content-Type | Required | Request content type |
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| parent_type | string | Required | bank or payment_system |
| parent_name | string | Required | Exact name of configured bank (e.g., IMPS, UPI) |
| type | string | Required | imps or bkash |
| 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. |
| 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": "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-API-Key | Required | Your external API key |
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:
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 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.
sign field from the received JSON payloadwithdrawal_postback_key to the data as: {"withdrawal_postback_key": "your_key_here"}sign โ if they match, the request is authenticPython 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"}), 200Node.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
| 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 | MD5 signature for verification. Computed using withdrawal_postback_key (see verification steps above) |
// 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" }