Webhooks
Events sent to your server as they happen: a rider near the door, a delivery done, a vehicle that hasn’t moved.
Webhooks come with fleet tracking. Webhooks reference
Events
| Event | When | data |
|---|---|---|
task.created | A delivery was created | task |
task.assigned | A vehicle was given the delivery (or another one took it over) | task |
task.arriving | The rider is within the task’s arrivingS (3 minutes by default) or 250 m of the drop-off | task |
task.completed | Marked delivered | task |
task.failed | Marked failed | task |
vehicle.offline | A vehicle sent nothing for offlineAfterS (10 minutes by default), once per silence | vehicle |
task.updated | You edited the delivery (PATCH /tasks/{id}); changed lists the fields | task, changed |
task.started | An N-Ways driver started the delivery (driver connect) | task |
delivery.rated | The customer rated the delivery (1–5 stars, seen only by your company) | task, rating |
zone.entered, zone.exited | A vehicle entered or left one of your zones | vehicle, zone |
connection.approved, connection.declined | An N-Ways driver approved or declined your invitation | connection or invite |
connection.revoked, connection.ended | The driver stopped sharing with you, or your company ended the connection | connection |
driver.availability_changed | A connected driver switched Available for you on or off | connection |
dispatch.offered, .accepted, .declined, .expired, .withdrawn | A delivery request to an N-Ways driver, and what happened to it | offer, task |
ERP teams (SAP, Odoo, Microsoft Dynamics): see Connect your ERP for field mapping, signature checks in C#, Groovy and Python, and reconciling with the export API.
Alerts (an area left, a zone entered, a vehicle silent or stopped) come as asset.* events too, and can go to email, push, Google Sheets, Telegram or Excel: see Alerts and forwarding.
Add an endpoint
Give an HTTPS address on your server, and the events it wants (none listed means all). The answer has the endpoint’s signing secret: it’s shown only now, so store it.
curl -X POST "https://nways.ng/maps/v1/fleet/webhooks" \
-H "X-NWays-Api-Key: YOUR_SERVER_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://example.com/nways/events", "events": ["task.arriving", "task.completed", "task.failed"]}'POST /webhooks/{id}/test sends a ping event to check it works; GET /webhooks/{id}/deliveries shows what was sent, your server’s answers and the next retry.
What arrives, and checking it’s from us
Each event is a POST with a JSON body (id, type, createdAt, data) and these headers:
| Header | Value |
|---|---|
X-NWays-Event | The event type |
X-NWays-Delivery | This delivery’s ID (dlv_…), the same on every retry |
X-NWays-Signature | t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" with your endpoint’s secret |
Check the signature on the raw body, before parsing it, and refuse a timestamp more than 5 minutes off:
import { createHmac, timingSafeEqual } from 'node:crypto'
import { createServer } from 'node:http'
const SECRET = process.env.NWAYS_WEBHOOK_SECRET
function verified(raw, header) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false
const want = createHmac('sha256', SECRET).update(`${parts.t}.${raw}`).digest('hex')
return want.length === parts.v1?.length && timingSafeEqual(Buffer.from(want), Buffer.from(parts.v1))
}
createServer((req, res) => {
let raw = ''
req.on('data', (c) => (raw += c))
req.on('end', () => {
if (!verified(raw, req.headers['x-nways-signature'] ?? '')) return res.writeHead(401).end()
const event = JSON.parse(raw)
if (event.type === 'task.arriving') console.log(`Order ${event.data.task.id}: rider arriving`)
res.writeHead(204).end() // answer fast; do slow work afterwards
})
}).listen(8080)import hashlib
import hmac
import os
import time
SECRET = os.environ["NWAYS_WEBHOOK_SECRET"].encode()
def verified(raw: bytes, header: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
if abs(time.time() - int(parts.get("t", 0))) > 300:
return False
want = hmac.new(SECRET, parts["t"].encode() + b"." + raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, parts.get("v1", ""))Retries and order
- Answer with any 2xx within 10 seconds. Anything else is tried again after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1, 3, 6 and 12 hours, then marked failed.
- Events can arrive more than once or out of order: keep the event
ids you’ve handled, and trust the task’sstatusover the order events came in. - Redirects aren’t followed, and the address must be public.
- Leaked a secret?
POST /webhooks/{id}/rotate-secretgives a new one at once.