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.
https://your-server.example/zonmpay/webhookStep by step
- 1
Create an endpoint
Build a public HTTPS route on your server that accepts POST with a JSON body and answers 200 quickly.
- 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
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
Answer 200, then work
Acknowledge first and process afterwards. Anything other than 200 is retried with exponential backoff, and each attempt is recorded.
- 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
referencestringRequiredTransaction reference the event belongs to
eventstringRequiredPAYIN or PAYOUT
statusstringRequiredSUCCESS, FAILED or PENDING
completedAtstringOptionalISO timestamp of the final state
| Field | Type | Required | Description |
|---|---|---|---|
| reference | string | Yes | Transaction reference the event belongs to |
| event | string | Yes | PAYIN or PAYOUT |
| status | string | Yes | SUCCESS, FAILED or PENDING |
| completedAt | string | No | ISO timestamp of the final state |
Verify the signature
// 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
{
"reference": "A1B2C3D4E5",
"event": "PAYIN",
"accountName": "Joseph",
"channelProvider": "Vodacom Tanzania",
"phoneNumber": "255712345678",
"status": "SUCCESS",
"completedAt": "2026-09-19T12:00:00+03:00"
}{
"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.