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 2xx status within 5 seconds
  • Return 200 even if you process the event asynchronously
Use npx nahupay listen during development to forward events to localhost.

Event types

EventDescription
payment.createdA new payment was created (not yet paid).
payment.processingThe customer has approved the USSD push — funds in transit.
payment.completedPayment confirmed. Safe to fulfill the order.
payment.failedPayment failed or was declined.
payment.expiredThe checkout URL expired without payment.
payment.refundedA refund was issued for this payment.
payment.refund_failedA 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 });
}
Always read the raw body string before parsing. JSON.parse changes whitespace, which invalidates the HMAC.

Retry policy

If your endpoint returns a non-2xx response or times out, NahuPay retries with exponential back-off:

AttemptDelay
2nd5 seconds
3rd30 seconds
4th2 minutes
5th10 minutes
6th1 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 200 immediately and enqueue the payload for background processing.
  • Handle duplicates — use event.id as an idempotency key; store seen IDs to safely ignore replays.
  • Don't rely on event order — a payment.completed can arrive before payment.processing under heavy load.