Webhooks
Webhooks let your server receive real-time notifications when payment events occur — such as when a payment confirms, expires, or is flagged. This guide covers registering endpoints, verifying signatures, and handling retries.
Registering a webhook
Register a webhook endpoint to receive events for specific event types.
curl -X POST https://api.mpchat.com/v1/webhooks \
-H "Authorization: Bearer {key_id}:{timestamp}:{signature}" \
-H "Content-Type: application/json" \
-d '{
"url": "https://acme.com/webhooks/mpchat",
"events": ["payment.confirmed", "payment.expired", "withdrawal.completed"]
}'{
"id": "wh_01HQ...",
"url": "https://acme.com/webhooks/mpchat",
"events": ["payment.confirmed", "payment.expired", "withdrawal.completed"],
"is_active": true,
"created_at": "2024-01-15T10:00:00Z"
}Event types
payment.confirmedevent | A payment order was fully paid and confirmed on-chain. Fulfill the order. |
payment.expiredevent | An order expired without receiving payment. The address is released. |
payment.underpaidevent | Payment received was below the required amount (outside tolerance). Manual review may be needed. |
payment.frozenevent | A transaction was flagged by KYT compliance screening. Do not fulfill until cleared. |
withdrawal.completedevent | A withdrawal was broadcast to the blockchain and confirmed. |
withdrawal.failedevent | A withdrawal attempt failed. The funds are returned to your balance. |
Payload structure
Every webhook request is an HTTP POST with a JSON body. Two custom headers are always present:
// Headers
X-Merchant-Signature: t=1700000000,v1=a1b2c3d4e5f6...
X-Webhook-Event: payment.confirmed
// Body
{
"event": "payment.confirmed",
"timestamp": "2024-01-15T10:05:00Z",
"data": {
"order_id": "ord_01HQ...",
"order_number": "ord_a1b2c3",
"status": "PAID",
"order_amount": "99.99",
"received_amount": "99.99",
"currency": "USDT",
"network": "TRC20",
"paid_at": "2024-01-15T10:04:58Z",
"metadata": {
"customer_id": "cust_123"
}
}
}Verifying signatures
Always verify the X-Merchant-Signature header before processing any webhook. The signature prevents replay attacks and ensures the payload came from MP Merchant.
Signature format
The header value has the format:
X-Merchant-Signature: t={unix_timestamp},v1={hmac_hex}The HMAC-SHA256 is computed over {timestamp}.{raw_body} using your webhook secret.
⚠️ Reject stale timestamps
Verification code
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, secret) {
const [tPart, v1Part] = signature.split(',');
const timestamp = tPart.replace('t=', '');
const receivedHash = v1Part.replace('v1=', '');
// Reject stale timestamps (5 minute tolerance)
const age = Math.abs(Date.now() / 1000 - parseInt(timestamp));
if (age > 300) throw new Error('Stale timestamp');
const payload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(receivedHash, 'hex');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
throw new Error('Invalid signature');
}
}Retry policy
If your endpoint returns a non-2xx status code or times out (10s timeout), the delivery is retried with exponential backoff:
| Attempt | Delay | Cumulative time |
|---|---|---|
| 1 (immediate) | — | 0s |
| 2 | 1 minute | 1m |
| 3 | 5 minutes | 6m |
| 4 | 30 minutes | 36m |
| 5 | 2 hours | ~2h 36m |
| 6 (final) | 24 hours | ~26h 36m |
After 6 failed attempts, the delivery is marked FAILED and no further retries occur. You can view delivery history in the dashboard or via GET /v1/webhooks/{id}/deliveries.
💡 Idempotent handlers
data.order_id to de-duplicate — check if you've already processed this order before taking action.Best practices
- Respond quickly. Return HTTP 200 within 10 seconds. Defer heavy processing to a background job.
- Always verify signatures. Never process a webhook without checking the HMAC.
- Make handlers idempotent. The same event may be delivered more than once.
- Reject stale timestamps. Add the 5-minute timestamp check shown above.
- Use HTTPS endpoints only. HTTP endpoints are rejected.
- Log raw payloads. Store the raw body before parsing to aid debugging.
Next steps
- API Reference — Webhooks — list, update, delete endpoints
- Error Reference — webhook error messages