Receive events

Webhook API

Instead of polling, let Zonmpay POST every payment event to your server. Each delivery is signed and retried, and every attempt is logged.

POSThttps://your-server.example/zonmpay/webhook

Step by step

  1. 1

    Create an endpoint

    Build a public HTTPS route on your server that accepts POST with a JSON body and answers 200 quickly.

  2. 2

    Register it

    Add the URL in your developer settings and pick the events. A signing secret is shown once — store it server-side.

  3. 3

    Verify every delivery

    Compute HMAC-SHA256 of the raw body with your signing secret and compare it to the signature header before trusting the payload.

  4. 4

    Answer 200, then work

    Acknowledge first and process afterwards. Anything other than 200 is retried with exponential backoff, and each attempt is recorded.

  5. 5

    Stay idempotent

    A reference can arrive more than once. Apply the event only if you have not already applied that reference and status.

Payload fields

referencestringRequired

Transaction reference the event belongs to

eventstringRequired

PAYIN or PAYOUT

statusstringRequired

SUCCESS, FAILED or PENDING

completedAtstringOptional

ISO timestamp of the final state

Verify the signature

Node / TypeScript
// Example handler (Node / TypeScript)
import { createHmac, timingSafeEqual } from "crypto";

export async function handler(request: Request) {
  const raw = await request.text();
  const signature = request.headers.get("x-zonmpay-signature") ?? "";
  const expected = createHmac("sha256", process.env.ZONMPAY_WEBHOOK_SECRET!)
    .update(raw)
    .digest("hex");

  if (
    signature.length !== expected.length ||
    !timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  ) {
    return new Response("Invalid signature", { status: 401 });
  }

  const event = JSON.parse(raw);
  // mark the order using event.reference / event.status
  return new Response("ok");
}

Responses

PAYIN
{
  "reference": "A1B2C3D4E5",
  "event": "PAYIN",
  "accountName": "Joseph",
  "channelProvider": "Vodacom Tanzania",
  "phoneNumber": "255712345678",
  "status": "SUCCESS",
  "completedAt": "2026-09-19T12:00:00+03:00"
}
PAYOUT
{
  "reference": "XYZ98ABC12",
  "event": "PAYOUT",
  "accountName": "Amina",
  "channelProvider": "Airtel Tanzania",
  "phoneNumber": "255687819056",
  "status": "SUCCESS",
  "completedAt": "2026-09-19T14:30:00+03:00"
}

Things that can go wrong

  • 401 from your serverSignature check failed — confirm you hashed the raw body.
  • retry exhaustedYour endpoint never answered 200; check the delivery log.