Receive Payout Result
Paydify calls back the Partner to implement the related business process.
Withdrawal Notification
- URL: the payout callback URL of your app (configured by Paydify for the app, not passed per order; contact Paydify to set or change it)
- Method: POST
- Content-Type: application/json
- Trigger: pushed when a withdraw order reaches a terminal state (
SUCCESS/FAILED), exactly once per withdraw. Every detail of a batch withdraw is also notified one by one through this callback; batch-level status is pull-only via Batch Withdraw Query
See the API reference Withdraw Notification for the full field definitions.
Callback Headers
| Header | Required | Description |
|---|---|---|
| content-type | Yes | Always application/json |
| x-api-key | Yes | Your appId |
| x-api-timestamp | Yes | Unix timestamp in milliseconds |
| x-api-signature | Yes | Signature generated by Paydify |
Callback Body
| Field Name | Type | Required | Description |
|---|---|---|---|
| appId | string | Yes | appId of the app the order belongs to |
| withdrawId | string | Yes | Paydify withdraw order ID |
| mchWithdrawId | string | Yes | Merchant withdraw order ID (the mchWithdrawId passed at apply) |
| status | string | Yes | Withdraw status; only terminal states are pushed: SUCCESS (on-chain transfer and settlement succeeded) / FAILED (funds returned to balance, see errorMsg) |
| currency | string | Yes | Withdraw currency |
| network | string | Yes | Chain network |
| amount | string | Yes | Withdraw amount, 0 when absent |
| gasFee | string | Yes | On-chain gas fee, 0 when absent |
| processingFee | string | Yes | Processing fee, 0 when absent |
| address | string | Yes | Destination address |
| txnHash | string | No | On-chain transaction hash, present when available |
| finishedTime | long | Yes | Completion timestamp in milliseconds, 0 when absent |
| errorCode | string | No | Error code, may be present only on failure |
| errorMsg | string | No | Failure reason, may be present only on failure |
Example
{ "appId": "A123456788", "withdrawId": "W5081643613805694001", "mchWithdrawId": "PAYOUT_20260610_001", "status": "SUCCESS", "currency": "USDT", "network": "TRON", "amount": "99.99000000", "gasFee": "0.01000000", "processingFee": "0.5", "address": "TX7dEjrN...", "txnHash": "0xabcdef...", "finishedTime": 1780971800000 }
Fiat Withdrawal Notification
- URL: the callback URL of your fiat withdraw app config (configured by Paydify for the app, not passed per order)
- Method: POST
- Content-Type: application/json
- Trigger: pushed when a fiat withdraw order reaches a terminal state (
COMPLETED/FAILED/CANCELLED/REJECTED); non-terminal states (PENDING/PROCESSING/SETTLING, etc.) are not pushed, exactly once per withdraw
Key fields: withdrawId (Paydify fiat withdraw order ID), mchWithdrawId (the mchOrderId passed at apply), status, orderAmount (arrival amount, currently in USD), amount (total deducted amount), withdrawFeeAmount (fee), quoteId, deductionSnapshot / bankAccountSnapshot (snapshots as JSON strings), finishedTime, errorMsg. See the API reference Fiat Withdraw Notification for the full field definitions.
Callbacks share the same signature mechanism as Payin. If no callback arrives for a long time, query proactively via /withdraw/detail or /fiat-withdraw/detail. For orders created with a partner credential, the callback signature is computed with the ordering credential apiSecret, while x-api-key remains the appId of the app the order belongs to.
Callback Response
Acknowledge with HTTP 200 and the plain text success (case-insensitive):
| Response Value | Description |
|---|---|
| success | Callback acknowledged successfully |
| Anything else | Treated as failure and triggers re-push (including fail, JSON objects, HTML error pages, and empty bodies) |
Callback Signature Verification
Use your secretKey to verify the callback before processing the business logic.
The signed content contains exactly these fields:
| Field | Value |
|---|---|
| apiPath | The path part of your webhook URL, for example /api/payout/webhook |
| body | The exact raw request body string |
| x-api-key | The value from the request header |
| x-api-timestamp | The value from the request header |
Verification rules:
- Use HMAC-SHA256
- Sort the field names in ascending order before JSON serialization
- JSON serialize the sorted content map
- Base64 encode the HMAC result
- Compare the generated value with
x-api-signature - Use the request path only; do not include scheme, domain, or query parameters
- Use the raw request body exactly as received; do not parse and rebuild it before verification
In practice, the object to be signed should look like this (the body below is truncated to two fields for illustration — it is not a complete callback payload; always sign against the complete raw request body string you received):
{ "apiPath": "/api/payout/webhook", "body": "{\"withdrawId\":\"W5081643613805694001\",\"mchWithdrawId\":\"PAYOUT_20260610_001\"}", "x-api-key": "A123456788", "x-api-timestamp": "1762513487000" }
Final signature:
Base64(HMAC_SHA256(jsonString, secretKey))
Recommended Verification Steps
- Read
x-api-key,x-api-timestamp, andx-api-signaturefrom the headers - Read the raw request body as a UTF-8 string
- Get the request path only, for example
/api/payout/webhook - Build the content map with
apiPath,body,x-api-key, andx-api-timestamp - Sort the keys in ascending order
- JSON serialize the sorted map
- Compute HMAC-SHA256 using your
secretKey - Base64 encode the result
- Compare the generated value with
x-api-signature - Process the webhook only after the signature matches
Node.js Example
const crypto = require("crypto"); function verifyPaydifyWebhook({ path, rawBody, apiKey, timestamp, receivedSignature, secretKey, }) { const content = { apiPath: path, body: rawBody || "", "x-api-key": apiKey, "x-api-timestamp": String(timestamp), }; const jsonString = JSON.stringify(content); const expectedSignature = crypto .createHmac("sha256", secretKey) .update(jsonString, "utf8") .digest("base64"); return expectedSignature === receivedSignature; }
Common Verification Failures
- Using plain SHA256 instead of HMAC-SHA256
- Rebuilding the JSON body before verification
- Using the full URL instead of the path only
- Including query parameters in the signature calculation
- Using a different timestamp value from the header
- Comparing hex output with a Base64 signature
Callback Retry Mechanism
If the callback does not receive the expected response value success, it will be re-pushed at intervals of 15s / 45s / 90s / 180s / 10m / 30m / 60m / 3h / 6h / 12h, up to 11 attempts including the first push:
| Request Number | Time Delay | Description |
|---|---|---|
| 1 | 0 | First callback, executed right after the terminal state is persisted |
| 2 | 15 seconds | 1st retry |
| 3 | 45 seconds | 2nd retry |
| 4 | 90 seconds | 3rd retry |
| 5 | 180 seconds | 4th retry |
| 6 | 10 minutes | 5th retry |
| 7 | 30 minutes | 6th retry |
| 8 | 60 minutes | 7th retry |
| 9 | 3 hours | 8th retry |
| 10 | 6 hours | 9th retry |
| 11 | 12 hours | 10th retry; delivery stops if it still fails |