Skip to main content

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:

  1. completed — Payment successful: A matching transaction has been confirmed with the invoice (blockchain or Binance Pay).
  2. 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"
}
FieldDescription
merchant_idStore's merchant code.
api_keyAPI Key (for your cross-reference if needed).
request_idYour order ID (For you to compare with your invoice).
trans_idInvoice code. Use to update the corresponding order.
statusAlways "completed".
amountUSD amount (string).
token_symbolCoin/Token (e.g., USDT).
token_priceExchange rate at the time (string). May fallback to amount if unavailable.
token_amountActual crypto amount transferred (string). May be null in rare cases.
network_symbolNetwork (e.g., BSC, TRC20, ETH, SOL, BINANCE_ID, OKX_UID, BYBIT_UID, ...).
from_addressSender's wallet address.
to_addressReceiving wallet address (Store's).
transaction_hashTransaction hash on blockchain.

Merchant Requirements

  1. Receive POST at the endpoint corresponding to callback_url.
  2. Parse JSON body.
  3. Verify (optional but recommended): check merchant_id or trans_id against your internal data.
  4. Update order: mark the order matching trans_id or request_id as paid; save transaction_hash if needed.
  5. 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"
}
FieldDescription
merchant_idStore's merchant code.
request_idYour order ID (if provided when creating the invoice).
trans_idInvoice code. Use to update the corresponding order.
statusAlways "expired".
amountUSD 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_url to prevent eavesdropping.
  • API Key Verification (completed only): The completed webhook includes your api_key in the payload. Compare it with your stored API key to verify authenticity. Note: the expired webhook does not include api_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:

AttemptTiming
Attempt 1Immediately (when invoice completes)
Attempt 2After 10 seconds
Attempt 3After 30 seconds
Attempt 4After 30 seconds
Attempt 5After 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.