Skip to main content

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 201 with a reference (WHK-…) and its signing secret, shown only this once. Store it with your other secrets.
  • To change the URL or subscriptions, send the same request with reference. The answer is 200 without 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.

  1. Read the raw body bytes before any JSON parsing.
  2. Split the header on , and take t and v1.
  3. Compute the HMAC of t + "." + body and compare it with v1 in constant time.
  4. Reject the call if t is far from your clock (five minutes is a sensible limit).
  5. 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 2xx quickly 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}/replay with an optional since and event_types re-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​

EventWhen
delivery.requestedThe delivery was booked and released to dispatch
delivery.assignedA delivery company and rider now have the delivery
rider.acceptedA rider accepted the offer
delivery.picked_upThe sender confirmed handover; the rider has the item
delivery.on_the_wayThe rider is travelling to the drop-off
delivery.arrivedThe rider is at the drop-off
delivery.deliveredThe recipient confirmed receipt and the delivery was completed
delivery.cancelledThe delivery was cancelled
delivery.reassignedThe delivery went back to dispatch for another rider
delivery.failedThe delivery ended without reaching the recipient
delivery.exception_raisedSomething stopped the delivery and it is being resolved
delivery.batchedThe delivery joined a shared route
delivery.unbatchedThe delivery left a shared route and is dispatched on its own
relay.requestedThe rider asked to hand the delivery to another rider of the same company
relay.claimedAnother rider of that company agreed to take it
relay.completedThe handoff happened; the new rider has the item
relay.failedThe relay did not happen; the delivery is being resolved
payment.state_changedThe delivery's payment state changed (for example paid in cash at the door)
partner.blockedA delivery company you use is not taking new work for now
partner.reactivatedThat delivery company is taking new work again
bulk.createdA Bulk delivery was created and its recipients were sent links
bulk.recipient_confirmedA Bulk recipient confirmed their location
bulk.quote_readyEvery child of a Bulk delivery has a quote
bulk.confirmedThe sender confirmed the Bulk; each child is now a delivery
bulk.progress_updatedA child of the Bulk changed status
bulk.completedEvery child reached an end state
bulk.cancelledThe Bulk was cancelled before dispatch
credential.revokedOne 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.