Webhooks
IDM tells your system about changes by sending an HTTPS POST to an endpoint you register. Webhooks are a notification. When an event matters to a decision, read the job before you act (see Tracking and events).
Register an endpoint
POST /v1/webhooks
Idempotency-Key: register-hooks-2026-11
{
"url": "https://example.com/hooks/idm",
"subscriptions": ["delivery.assigned", "delivery.picked_up", "delivery.delivered", "delivery.cancelled", "delivery.failed"]
}
- The URL must be
https, carry no user name or password, and resolve only to public addresses. Private, loopback and link-local addresses are refused, and the address is checked again before every attempt. - A new endpoint answers
201with areference(WHK-…) and its signingsecret, shown only this once. Store it with your other secrets. - To change the URL or subscriptions, send the same request with
reference. The answer is200without the secret, and a disabled endpoint is enabled again.
What arrives
POST /hooks/idm HTTP/1.1
Content-Type: application/json
User-Agent: Intellisol-IDM-Webhooks/1
X-Intellisol-Event-Id: evt_01JC9W6Q4V5Y8Z2M3N4P5R6S7T
X-Intellisol-Signature: t=1789312385,v1=9f2c…
X-Request-Id: req_…
{
"event_id": "evt_01JC9W6Q4V5Y8Z2M3N4P5R6S7T",
"event_type": "delivery.assigned",
"tenant": "tnt_…",
"occurred_at": "2026-09-20T10:33:02Z",
"sequence": 4,
"data": {
"delivery_reference": "DX-9K4M7X2Q",
"customer_status": "Delivery partner assigned",
"external_order_reference": "order-1042"
}
}
X-Request-Id is present when the event was caused by one of your API calls, so you can match the two. It is not signed and proves nothing on its own.
Verify the signature
X-Intellisol-Signature is t=<unix seconds>,v1=<hex>, where the hex is HMAC-SHA256 of the string "<t>.<raw body>" keyed with the endpoint's secret.
- Read the raw body bytes before any JSON parsing.
- Split the header on
,and taketandv1. - Compute the HMAC of
t + "." + bodyand compare it withv1in constant time. - Reject the call if
tis far from your clock (five minutes is a sensible limit). - Only then parse the JSON.
Test vector: secret whsec_test, t=1767600000, body {"event_id":"evt_x"} gives v1=8b61f156403d7b84465146c781a51773e660c1c40547d4ac9bfd412c8bab0897.
Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/hooks/idm', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-Intellisol-Signature') ?? '';
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto.createHmac('sha256', process.env.IDM_WEBHOOK_SECRET)
.update(`${parts.t}.`).update(req.body).digest('hex');
const ok = parts.v1 && parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)) &&
Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
if (!ok) return res.sendStatus(400);
const event = JSON.parse(req.body.toString('utf8'));
queue.add(event); // process later; answer fast
res.sendStatus(200);
});
Python (Flask)
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/hooks/idm")
def idm_hook():
raw = request.get_data() # raw bytes, before parsing
parts = dict(p.split("=", 1) for p in request.headers.get("X-Intellisol-Signature", "").split(",") if "=" in p)
expected = hmac.new(os.environ["IDM_WEBHOOK_SECRET"].encode(), f'{parts.get("t")}.'.encode() + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(parts.get("v1", ""), expected) or abs(time.time() - int(parts.get("t", 0))) > 300:
abort(400)
enqueue(request.get_json())
return "", 200
Delivery guarantees
- At least once. The same event can arrive more than once. Deduplicate on
event_id. - Not in order. Use
sequence(per job) to discard an event older than one you already handled. - Retries. A non-2xx answer or a timeout is retried with exponential backoff. Answer
2xxquickly and do the work afterwards; a slow endpoint times out (five seconds at launch). - Automatic disable. An endpoint that keeps failing is disabled after a configured number of failed deliveries (ten at launch) and your administrators are told. Fix it, register it again with its
reference, then replay. - Replay.
POST /v1/webhooks/{id}/replaywith an optionalsinceandevent_typesre-sends failed deliveries and sends events the endpoint missed while it was disabled, within the retention window (seven days at launch). Events already delivered are not sent again.
Event types
| Event | When |
|---|---|
delivery.requested | The delivery was booked and released to dispatch |
delivery.assigned | A delivery company and rider now have the delivery |
rider.accepted | A rider accepted the offer |
delivery.picked_up | The sender confirmed handover; the rider has the item |
delivery.on_the_way | The rider is travelling to the drop-off |
delivery.arrived | The rider is at the drop-off |
delivery.delivered | The recipient confirmed receipt and the delivery was completed |
delivery.cancelled | The delivery was cancelled |
delivery.reassigned | The delivery went back to dispatch for another rider |
delivery.failed | The delivery ended without reaching the recipient |
delivery.exception_raised | Something stopped the delivery and it is being resolved |
delivery.batched | The delivery joined a shared route |
delivery.unbatched | The delivery left a shared route and is dispatched on its own |
relay.requested | The rider asked to hand the delivery to another rider of the same company |
relay.claimed | Another rider of that company agreed to take it |
relay.completed | The handoff happened; the new rider has the item |
relay.failed | The relay did not happen; the delivery is being resolved |
payment.state_changed | The delivery's payment state changed (for example paid in cash at the door) |
partner.blocked | A delivery company you use is not taking new work for now |
partner.reactivated | That delivery company is taking new work again |
bulk.created | A Bulk delivery was created and its recipients were sent links |
bulk.recipient_confirmed | A Bulk recipient confirmed their location |
bulk.quote_ready | Every child of a Bulk delivery has a quote |
bulk.confirmed | The sender confirmed the Bulk; each child is now a delivery |
bulk.progress_updated | A child of the Bulk changed status |
bulk.completed | Every child reached an end state |
bulk.cancelled | The Bulk was cancelled before dispatch |
credential.revoked | One of your API credentials was revoked. Sent to every endpoint you have. |
Relay events never change customer_status: to a customer the delivery is still On the way.