Webhooks
NahuPay sends HTTP POST requests to your webhook endpoint when payment events occur. Webhooks are the right way to fulfill orders — polling the API is unreliable and may miss events.
Overview
Register a webhook endpoint in Dashboard → Settings → Webhooks. The endpoint must:
- Be publicly accessible over HTTPS
- Return a
2xxstatus within 5 seconds - Return
200even if you process the event asynchronously
npx nahupay listen during development to forward events to localhost.Event types
| Event | Description |
|---|---|
payment.created | A new payment was created (not yet paid). |
payment.processing | The customer has approved the USSD push — funds in transit. |
payment.completed | Payment confirmed. Safe to fulfill the order. |
payment.failed | Payment failed or was declined. |
payment.expired | The checkout URL expired without payment. |
payment.refunded | A refund was issued for this payment. |
payment.refund_failed | A refund attempt failed. |
Event payload
Every event has the same envelope:
{
"id": "evt_01hxbcdef",
"object": "event",
"type": "payment.completed",
"created": "2026-07-10T08:17:32Z",
"data": {
"object": {
"id": "pay_01hx9m2k4bw3zfjq",
"status": "completed",
"amount": 50000,
"currency": "ETB",
"method": "telebirr",
"metadata": { "order_id": "ORD-1234" }
// ... full payment object
}
}
}Signature verification
NahuPay signs every webhook request with a NahuPay-Signature header. Always verify this before acting on an event.
The signature is an HMAC-SHA256 of the raw request body, keyed with your webhook secret. Use the SDK helper:
// Node.js — Next.js App Router example
import NahuPay from 'nahupay';
const client = new NahuPay({ apiKey: process.env.NAHUPAY_SECRET_KEY });
export async function POST(req: Request) {
const body = await req.text(); // raw body — NOT JSON.parse'd
const sig = req.headers.get('NahuPay-Signature')!;
let event;
try {
event = client.webhooks.constructEvent(body, sig,
process.env.NAHUPAY_WEBHOOK_SECRET);
} catch (err) {
return Response.json({ error: 'invalid signature' }, { status: 400 });
}
// handle event...
return Response.json({ received: true });
}Retry policy
If your endpoint returns a non-2xx response or times out, NahuPay retries with exponential back-off:
| Attempt | Delay |
|---|---|
| 2nd | 5 seconds |
| 3rd | 30 seconds |
| 4th | 2 minutes |
| 5th | 10 minutes |
| 6th | 1 hour |
| 7th (final) | 6 hours |
After 7 failed attempts, the event is marked as permanently failed and no further retries occur. You can manually replay events from the dashboard.
Best practices
- Always verify signatures — never trust a webhook without checking the header.
- Respond fast, process async — return
200immediately and enqueue the payload for background processing. - Handle duplicates — use
event.idas an idempotency key; store seen IDs to safely ignore replays. - Don't rely on event order — a
payment.completedcan arrive beforepayment.processingunder heavy load.