Webhooks Guide
Webhooks notify your backend when payment and payout states change. Your handler should verify the signature, process the event idempotently, and return a 2xx response quickly.
Prerequisites: You need a public callback URL and the same API secret used
for MultiHub API request signing.
Events
| Event | Description |
|---|
payment.created | Payment was created |
payment.processing | Payment is processing |
payment.completed | Payment completed successfully |
payment.failed | Payment failed |
payment.cancelled | Payment was cancelled |
payment.refunded | Payment was refunded |
payout.created | Withdrawal was created |
payout.completed | Withdrawal completed successfully |
payout.failed | Withdrawal failed |
Payload
Webhook deliveries use an outer envelope. The payment object is under data.result.payment.
{
"id": "pay_123:payment.completed",
"created_at": "2026-04-02T08:23:04.379Z",
"data": {
"next": null,
"result": {
"payment": {
"amount": { "value": 500000, "currency": "ARS" },
"identifiers": {
"c_id": "merchant-order-1",
"h_id": "pay_123",
"p_id": "provider-ref-123",
"utr": "412345678901"
},
"status": {
"status": "success",
"final": true,
"success": true,
"error": null
},
"timestamps": {
"created": "2026-04-02T08:22:21.453Z",
"updated": "2026-04-02T08:22:22.795Z",
"finished": "2026-04-02T08:22:21.790Z"
},
"destination": "in",
"receiver": {},
"operations": []
}
},
"success": true,
"request_id": "729aebbf-5a6b-4299-87fa-1cf05c1121a6",
"processing_time": 0
},
"merchant_id": "19"
}
Internal fields prefixed with _ are not included in the delivered webhook
body. Do not build integrations around _payment_id or other internal-only
fields.
identifiers.utr is optional and appears only when a non-empty UTR/reference value is available.
| Header | Description |
|---|
Content-Type | application/json |
X-Data-Hash | SHA-512 signature: SHA512(rawBody + API secret) |
X-Webhook-Id | Delivery/event identifier |
X-Webhook-Timestamp | ISO 8601 delivery timestamp |
X-Webhook-Nonce | Per-delivery nonce |
X-Webhook-Signature-V2 | Optional replay-resistant signature: SHA512(timestamp + rawBody + API secret) |
Verify Signatures
Use the exact raw request body bytes. Re-serializing parsed JSON changes whitespace/key ordering and will break verification.
Webhook deliveries created or rotated after the API secret rollout use the merchant API secret. Existing legacy webhook configurations may continue to verify with the previously issued signing secret until the merchant rotates the API secret.
import hashlib
import hmac
import json
API_SECRET = "your_api_secret"
def verify_webhook(raw_body: bytes, api_secret: str, received_hash: str) -> bool:
expected = hashlib.sha512(raw_body + api_secret.encode()).hexdigest()
return hmac.compare_digest(expected, received_hash or "")
# Flask-style handler
raw_body = request.get_data()
received_hash = request.headers.get("X-Data-Hash")
if not verify_webhook(raw_body, API_SECRET, received_hash):
return {"error": "Invalid signature"}, 400
payload = json.loads(raw_body)
payment = payload["data"]["result"]["payment"]
status = payment["status"]["status"]
Retry Behavior
A webhook delivery succeeds on any HTTP 2xx response. Non-2xx responses, timeouts, network errors, URL validation failures, or circuit-breaker blocks are failures.
Defaults:
| Setting | Default |
|---|
| Timeout | 30 seconds |
| Max attempts | 3 |
| Base retry delay | 1 second |
Retry delay uses exponential backoff with jitter and a 24-hour cap. Delivery order is not guaranteed, so your handler must be idempotent.
Handling Statuses
Use payment.status.final to decide whether the payment reached a terminal state.
const payment = payload.data.result.payment;
if (payment.status.final && payment.status.success) {
await markOrderPaid(payment.identifiers.c_id);
}
if (payment.status.final && payment.status.success === false) {
await markOrderFailed(payment.identifiers.c_id, payment.status.error);
}
Per-Payment Webhook URLs
You can pass webhook_url in payment.in and payment.out requests:
{
"method": "payment.in",
"service_id": 14701,
"params": {
"payment": {
"identifiers": { "c_id": "order-123" },
"amount": { "value": 10000, "currency": "INR" },
"payer": { "email": "customer@example.com" },
"webhook_url": "https://merchant.example/webhooks/123hub"
}
}
}
The URL must be public HTTP(S). The gateway validates the URL when the payment is created and again before delivery.
Best Practices
- Verify every signature before processing.
- Return 2xx quickly and process heavy work asynchronously.
- Use
id plus payment.status.status as a deduplication key.
- Store
request_id, identifiers.c_id, and identifiers.h_id for support and reconciliation.
- Treat webhook delivery as at-least-once.