Webhooks
Webhooks are how NaafiPay tells your application that a payment or subscription changed state. Whenever something interesting happens — a payment completes, a subscription renews, a refund is processed — NaafiPay POSTs a JSON payload to the webhook URL you register in your dashboard. Webhooks are the right way to keep your application in sync because they fire asynchronously and reliably, even if the customer closes the browser before the success page loads. Every payload is signed with HMAC-SHA256 and includes a timestamp so you can reject replays.
Registering an endpoint
Go to Settings → Webhooks and add the HTTPS URL where you want events delivered. You can register multiple endpoints — one for production, one for staging, one for your data warehouse — and choose which events each one receives.
Event list
| Field | Type | Required | Description |
|---|---|---|---|
| payment.completed | event | optional | A payment was fully verified and captured. |
| payment.pending | event | optional | A payment session was created and is awaiting customer action. |
| payment.failed | event | optional | A payment attempt failed (declined card, expired session, etc.). |
| payment.cancelled | event | optional | A payment session was cancelled by you or the customer. |
| subscription.created | event | optional | A new subscription started after the first successful charge. |
| subscription.renewed | event | optional | A subscription was successfully renewed and charged for a new period. |
| subscription.cancelled | event | optional | A subscription was cancelled (immediately or at period end). |
| subscription.expired | event | optional | A subscription period ended without renewal (dunning exhausted). |
Payload shape
Every webhook POSTs the same envelope:
1{2 "event": "payment.completed",3 "invoice_id": "inv_01HZX5JM3W7QZK8YF7N9P0RTS2",4 "payment_status": "completed",5 "amount": 1500,6 "currency": "BDT",7 "metadata": { "order_id": "ORD-1001" },8 "timestamp": "2026-05-30T14:06:22Z"9}Signing and replay protection
Each request carries two headers used together for verification:
| Field | Type | Required | Description |
|---|---|---|---|
| x-paygate-timestamp | string | required | Unix timestamp (seconds) when the webhook was signed. Reject if older than 300 seconds. Example: 1748618782 |
| x-paygate-signature-v2 | string | required | Hex HMAC-SHA256 of `${timestamp}.${raw_body}` using your endpoint's signing secret. Example: 9f8c1e2a... |
x-paygate-timestamp header to your server clock and reject anything older than 300 seconds.Verification examples
1<?php2$payload = file_get_contents('php://input');3$timestamp = $_SERVER['HTTP_X_PAYGATE_TIMESTAMP'] ?? '';4$received = $_SERVER['HTTP_X_PAYGATE_SIGNATURE_V2'] ?? '';5$secret = getenv('NAAFIPAY_WEBHOOK_SECRET');6 7if (abs(time() - (int) $timestamp) > 300) {8 http_response_code(401);9 exit('replay window exceeded');10}11 12$expected = hash_hmac('sha256', $timestamp . '.' . $payload, $secret);13if (!hash_equals($expected, $received)) {14 http_response_code(401);15 exit('invalid signature');16}17 18$event = json_decode($payload, true);19// handle $event['event'], $event['invoice_id'], ...20http_response_code(200);Retry policy
If your endpoint returns any non-2xx response or times out after 10 seconds, NaafiPay retries with exponential backoff: 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, then every 6 hours for up to 24 hours. After that, the delivery is marked permanently failed and we email the account owner.
(invoice_id, event).