Webhooks
We POST a signed JSON event to your HTTPS endpoint when something happens.
Events
| type | When |
|---|---|
order.accepted | A Partner took the order. |
order.arrived | The Partner reached the pickup. |
order.started | Picked up / trip started. |
order.completed | Delivered / trip completed. |
order.cancelled | Cancelled by you, the Partner or the system. |
wallet.low_balance | Your live wallet went below the alert amount set in the dashboard (once until the next top-up). |
{
"id": "evt_7Qm2...",
"type": "order.completed",
"mode": "live",
"created_at": "2026-10-07T10:42:11.000Z",
"data": {
"order": {
"id": "ord_live_8fK2mQ9xLp3RtV7aB1cD",
"mode": "live",
"reference": "INV-1042",
"ride_number": "1001234",
"status": "completed",
"batch_id": null
}
}
}Setting up
Business dashboard → Developers → Webhooks: add an https:// URL, pick the events and the mode (test or live). The signing secret (whsec_...) is shown once; you can roll it at any time. “Send test event” posts a webhook.test event to check your endpoint.
Verifying the signature
Each request has these headers: FieldNova-Event, FieldNova-Event-Id, FieldNova-Delivery and
FieldNova-Signature: t=1791234567,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdv1 is the hex HMAC-SHA256 of <t>.<raw request body> with your endpoint secret. Compute it over the raw body (before JSON parsing), compare in constant time, and reject timestamps older than 5 minutes.
import crypto from 'node:crypto';
// Express: app.post('/hooks', express.raw({ type: 'application/json' }), handler)
export function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const want = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const got = String(parts.v1 ?? '');
return want.length === got.length && crypto.timingSafeEqual(Buffer.from(want), Buffer.from(got));
}Answer quickly, retries
Answer any 2xx within 10 seconds (do slow work afterwards). Anything else, a timeout or a redirect counts as a failure and we retry after 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h, 12 h — 9 attempts in all — then mark the delivery failed. Every attempt is in the dashboard’s delivery log, where you can also resend.
id to ignore duplicates and GET /orders/{id} when you need the latest state.