Guide

Webhooks

We POST a signed JSON event to your HTTPS endpoint when something happens.

Events

typeWhen
order.acceptedA Partner took the order.
order.arrivedThe Partner reached the pickup.
order.startedPicked up / trip started.
order.completedDelivered / trip completed.
order.cancelledCancelled by you, the Partner or the system.
wallet.low_balanceYour 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=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

v1 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.

Events can arrive more than once or out of order. Use id to ignore duplicates and GET /orders/{id} when you need the latest state.