Webhooks
Webhooks push delivery lifecycle events to your server so you don't have to poll. Configure URLs once per API key; Armada signs the HTTP request, retries on 5xx, and logs every attempt for debugging.
Configuring
Open the API Keys page in the business app, expand a key, and you'll see three webhook blocks:
- Order update webhook —
delivery.*events. Tick which status transitions you want notified on. We recommend all five. - Driver location webhook — fires on a configurable interval while the
order is
en_route. Pick an interval (10s / 30s / 60s / 2m / 5m) — shorter = more server load on your side. Most partners use 30s. - Wallet low-balance webhook — fires once when balance drops below your configured threshold. Configure that threshold on the same page.
Headers on every webhook
x-armada-webhook-topic requiredstringdelivery.completed, driver.location_updated. x-armada-webhook-id requiredstringwh_. Use for idempotency: ignore duplicates by id. x-armada-key-id requiredstringx-armada-key-type requiredstringv2. x-armada-api-version requiredstringv2 today. x-armada-timestamp requiredstring (ISO-8601)User-Agent requiredstringArmada Automated Ordering. Delivery semantics
- At-least-once. A network hiccup on your side can produce a duplicate — use
x-armada-webhook-idfor idempotency. - Retries. On a non-2xx response (or a connect error), Armada retries with exponential backoff — 3 attempts over ~15 minutes, then gives up.
- Order. Events for the same order are sent in order; events across different orders may arrive in any order.
- Timeout. Armada waits 10 s for a response. Keep your handler fast — do the minimum synchronously (acknowledge, enqueue) and process async.
Minimal receiver
import express from 'express';
const app = express();
app.use(express.json({ limit: '1mb' }));
app.post('/armada-webhook', (req, res) => {
const topic = req.headers['x-armada-webhook-topic'];
const id = req.headers['x-armada-webhook-id'];
// Idempotency: skip if we've processed this wh_ id already.
if (seen.has(id)) return res.sendStatus(200);
seen.add(id);
switch (topic) {
case 'delivery.accepted':
case 'delivery.en_route':
case 'delivery.completed':
case 'delivery.failed':
case 'delivery.canceled':
onOrderUpdate(req.body);
break;
case 'delivery.location_updated':
onDriverLocation(req.body);
break;
case 'wallet.balance_low':
notifyFinance(req.body);
break;
}
// Return 2xx fast. Any slow work should be queued.
res.sendStatus(200);
});Event catalog
delivery.*
Five events, one per lifecycle transition. Payload is the same shape as GET /v2/deliveries/:id at the moment the transition happened.
delivery.accepted— driver assigned;driverIdbecomes non-null.delivery.en_route— driver left the pickup and is moving to the destination.delivery.completed— delivered. Wallet debited.delivery.canceled— pre-terminal cancellation.delivery.failed— dispatcher couldn't complete it.
{
"id": "670f2a4e1f6b3c0012abcd12",
"reference": "order-100245",
"status": "en_route",
"code": "A1B2",
"amount": 4.5,
"currency": "KWD",
"deliveryFee": 2,
"driverId": "580de0389f2e4a3600e4bc7a",
"driverName": "Ahmad",
"driverPhone": "+96512345678",
"driverLocation": { "latitude": 29.3420, "longitude": 47.9560 },
"customerName": "John Doe",
"customerPhone": "+96590000000",
"distance": 4820,
"duration": 743,
"trackingLink": "https://tracking.armadadelivery.com/A1B2",
"testMode": false,
"createdAt": "2026-04-16T18:12:03.117Z"
}delivery.location_updated
Fires while the order is en_route, at your configured interval. Minimal payload —
use it to update the driver's pin on your tracking UI.
{
"id": "670f2a4e1f6b3c0012abcd12",
"status": "en_route",
"driverId": "580de0389f2e4a3600e4bc7a",
"driverLocation": { "latitude": 29.3421, "longitude": 47.9558 },
"updatedAt": "2026-04-16T18:14:11.503Z"
}wallet.balance_low
Fires once when the wallet crosses warningBalanceLevel going down. See Wallet for how the threshold is configured.
{
"balance": 4.25,
"currency": "KWD",
"warningBalanceLevel": 5,
"triggeredAt": "2026-04-16T18:15:01.000Z"
}Testing your receiver
- Expose your local endpoint.
ngrok http 3000,cloudflared tunnel --url http://localhost:3000, or a similar tool. - Paste the public URL into the key's webhook field in the business app.
- Hit the Send delivery test button (right next to the URL input) — Armada posts a canned payload to your receiver so you can verify wiring without creating an order.
- For full lifecycle testing, create an order with a test-mode key. The bot driver plays out all five delivery events in ~30 s.
Webhook logs
Every webhook attempt (success or failure) is logged on the API key. The business-app shows the 100 most recent attempts per key — topic, URL, response status, response time, and the request + response body. Useful for debugging a receiver that's intermittently failing.