Naafi Pay

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

FieldTypeRequiredDescription
payment.completedeventoptionalA payment was fully verified and captured.
payment.pendingeventoptionalA payment session was created and is awaiting customer action.
payment.failedeventoptionalA payment attempt failed (declined card, expired session, etc.).
payment.cancelledeventoptionalA payment session was cancelled by you or the customer.
subscription.createdeventoptionalA new subscription started after the first successful charge.
subscription.renewedeventoptionalA subscription was successfully renewed and charged for a new period.
subscription.cancelledeventoptionalA subscription was cancelled (immediately or at period end).
subscription.expiredeventoptionalA subscription period ended without renewal (dunning exhausted).

Payload shape

Every webhook POSTs the same envelope:

Webhook payload
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:

FieldTypeRequiredDescription
x-paygate-timestampstringrequiredUnix timestamp (seconds) when the webhook was signed. Reject if older than 300 seconds.
Example: 1748618782
x-paygate-signature-v2stringrequiredHex HMAC-SHA256 of `${timestamp}.${raw_body}` using your endpoint's signing secret.
Example: 9f8c1e2a...
DangerReject any request older than 5 minutes
The 5-minute replay window is mandatory. Without it, an attacker who captures one valid webhook could replay it indefinitely. Compare the x-paygate-timestamp header to your server clock and reject anything older than 300 seconds.

Verification examples

PHP
1<?php
2$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.

TipMake your handler idempotent
The same event id can be delivered more than once. Store the event id and ignore duplicates — for example, with a database constraint on (invoice_id, event).
WarningRespond fast
Return 200 as quickly as possible, ideally under 2 seconds. If you need to do heavy work (sending email, calling other APIs), enqueue a background job and respond immediately.
Updated at: May 30, 2026