Webhook
Webhooks allow your system to receive automatic real-time notifications from PayerScan. The system sends POST requests to the callback_url provided by the Merchant when creating an invoice.
Event Types
PayerScan has 2 main events returned in the status field:
completed— Payment successful: A matching transaction has been confirmed with the invoice (blockchain or Binance Pay).expired— Invoice expired: The invoice has exceeded the payment wait time.
Merchants need to expose a public URL (HTTPS recommended), receive POST requests, process the payload, and return HTTP 2xx within the specified timeout (e.g., 10 seconds).
Payload Structure
1. Webhook on Successful Payment (status: completed)
When a matching transaction is found (correct address, correct amount, within time window), the system updates the invoice to completed and sends a POST to callback_url with this payload:
Payload (JSON body)
{
"merchant_id": "MERCHANT_001",
"api_key": "YOUR_API_KEY",
"request_id": "order-1234",
"trans_id": "TID-ABC123DEF4567890",
"status": "completed",
"amount": "100",
"token_symbol": "USDT",
"token_price": "1",
"token_amount": "100.0026",
"network_symbol": "BSC",
"from_address": "0x8894e0a0c962cb723c1976a4421c95949be2d4e3",
"to_address": "0xc221460115e2CfCa5bF089A7e647b11cb9631efE",
"transaction_hash": "0xd346b32b83b35376d42a2464598fbf565fffb39e6569200f034a5e8342c532d7"
}
| Field | Description |
|---|---|
merchant_id | Store's merchant code. |
api_key | API Key (for your cross-reference if needed). |
request_id | Your order ID (For you to compare with your invoice). |
trans_id | Invoice code. Use to update the corresponding order. |
status | Always "completed". |
amount | USD amount (string). |
token_symbol | Coin/Token (e.g., USDT). |
token_price | Exchange rate at the time (string). May fallback to amount if unavailable. |
token_amount | Actual crypto amount transferred (string). May be null in rare cases. |
network_symbol | Network (e.g., BSC, TRC20, ETH, SOL, BINANCE_ID, OKX_UID, BYBIT_UID, ...). |
from_address | Sender's wallet address. |
to_address | Receiving wallet address (Store's). |
transaction_hash | Transaction hash on blockchain. |
Merchant Requirements
- Receive POST at the endpoint corresponding to
callback_url. - Parse JSON body.
- Verify (optional but recommended): check
merchant_idortrans_idagainst your internal data. - Update order: mark the order matching
trans_idorrequest_idas paid; savetransaction_hashif needed. - Return HTTP 2xx (e.g., 200 OK) within the timeout (e.g., 10 seconds). Response body is not required; the system only needs a 2xx status to consider the webhook successful.
If you return 4xx/5xx or timeout, the system marks the callback as failed and has a retry mechanism (number of retries and intervals depend on backend configuration).
2. Webhook on Invoice Expiry (status: expired)
When an invoice transitions to expired (past expires_at), the system sends a POST to callback_url with a simpler payload:
Payload (JSON body)
{
"merchant_id": "MERCHANT_001",
"request_id": "order-1234",
"trans_id": "TID-ABC123DEF4567890",
"status": "expired",
"amount": "100"
}
| Field | Description |
|---|---|
merchant_id | Store's merchant code. |
request_id | Your order ID (if provided when creating the invoice). |
trans_id | Invoice code. Use to update the corresponding order. |
status | Always "expired". |
amount | USD amount (string). |
You can use this to update the order as "expired", cancel reservations, etc. You should still return HTTP 2xx so the system doesn't treat it as an error.
3. Webhook Handler Example (Node.js)
app.post('/webhook/payment', express.json(), async (req, res) => {
const { merchant_id, api_key, trans_id, request_id, status, amount, transaction_hash } = req.body;
// Verify merchant_id (both completed and expired)
if (merchant_id !== process.env.PAYERSCAN_MERCHANT_ID) {
return res.status(401).send('Unauthorized');
}
// Verify api_key (completed only — expired does not include api_key)
if (status === 'completed' && api_key !== process.env.PAYERSCAN_API_KEY) {
return res.status(401).send('Unauthorized');
}
// Find order by request_id or trans_id
const order = await orderService.findByRequestId(request_id) || await orderService.findByTransId(trans_id);
if (!order) {
return res.status(404).send('Order not found');
}
if (status === 'completed') {
await orderService.markPaid(order, { transaction_hash, amount });
} else if (status === 'expired') {
await orderService.markExpired(order);
}
res.status(200).send('OK');
});
Signature Verification
To ensure webhooks genuinely come from PayerScan, you should verify the signature or cross-reference the data.
- HTTPS: Always use HTTPS for
callback_urlto prevent eavesdropping. - API Key Verification (completed only): The
completedwebhook includes yourapi_keyin the payload. Compare it with your stored API key to verify authenticity. Note: theexpiredwebhook does not includeapi_key.
Idempotency
A trans_id may only receive one completed event, but retries may resend it. Ensure your processing logic is idempotent (updating multiple times is safe, no double-crediting).
Retry on Webhook Failure
When the Merchant server returns an error (4xx/5xx) or times out, the system retries the webhook on this schedule:
| Attempt | Timing |
|---|---|
| Attempt 1 | Immediately (when invoice completes) |
| Attempt 2 | After 10 seconds |
| Attempt 3 | After 30 seconds |
| Attempt 4 | After 30 seconds |
| Attempt 5 | After 60 seconds |
Maximum 5 attempts. After 5 failures, callback_status changes to failed and no further attempts are made.
Merchants should ensure their endpoint is stable and returns 2xx quickly (< 10 seconds) to avoid unnecessary retries.