Build on QuickDispatch.
Two REST APIs, signed webhooks and a sandbox that never texts a real customer. Everything you can do in the dashboard, you can do from code.
Admin API
Orders, depot scans, booking requests and replies, slots, route optimisation, the AI route agent, runs and stops, live vehicle positions, fleet and shippers.
Admin API reference → For shippers · key issued by their courierCustomer API
Create consignments, follow every piece from depot to doorstep, get live tracking and proof of delivery, and book collections for returns.
Customer API reference →Your first consignment in three calls.
Get a sandbox key
Couriers create keys under Developers → API keys. Shippers get theirs from their courier. Sandbox keys start qd_test_ and never send real messages.
# every request Authorization: Bearer qd_test_4f9a0c1b2d3e5f60718293a4 # base URLs Customer API https://sandbox.api.quickdispatch.co.uk/v1 Admin API https://sandbox.api.quickdispatch.co.uk/admin/v1
Create a consignment
Send the recipient, the items and how many pieces each arrives in. Use an Idempotency-Key so a retry never creates a duplicate.
curl -X POST https://sandbox.api.quickdispatch.co.uk/v1/consignments \ -H "Authorization: Bearer $QD_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "reference": "OAK-48213", "recipient": { "name": "Sarah Khan", "phone": "+447700900123", "address": { "line1": "14 Headingley Avenue", "town": "Leeds", "postcode": "LS6 2AB" } }, "items": [ { "sku": "DIVAN-4FT6-GREY", "pieces": 2 } ], "service": { "crew": 2 } }' # 201 Created { "id": "con_8Kq2Lm", "status": "awaiting_scan", … }
Listen for updates
Register a webhook endpoint and verify the QD-Signature header on every event before trusting it.
// Node.js: verify a QuickDispatch webhook import crypto from 'node:crypto'; export function verify(rawBody, header, secret) { const parts = Object.fromEntries(header.split(',').map(p => p.split('='))); const age = Date.now() / 1000 - Number(parts.t); if (age > 300) return false; // older than 5 minutes const expected = crypto.createHmac('sha256', secret) .update(`${parts.t}.${rawBody}`).digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)); }
The same rules everywhere.
Keys & scopes
Bearer keys, live and sandbox. Admin keys carry scopes such as routes:write; a missing scope returns 403 insufficient_scope.
Rate limits
120 requests a minute per Customer key, 300 per Admin key. RateLimit-* headers on every response and Retry-After on a 429.
Idempotency
Every POST takes an Idempotency-Key. Retries with the same key return the original result for 24 hours.
Pagination & errors
Lists take limit and starting_after and return has_more. Errors always look like {"error": {"code", "message", "param", "request_id"}}.
Async jobs
Route optimisation returns 202 with a job. Poll GET /jobs/{id} or listen for job.completed. Nothing reaches drivers until you publish.
Events, not polling.
Signed with HMAC-SHA256, retried with back-off for 24 hours, and replayable from the dashboard.
| consignment.scanned | Pieces arrive at the depot |
| consignment.booked | Recipient booked a slot |
| consignment.out_for_delivery | On a van, with tracking URL |
| consignment.delivered | Delivered with proof |
| consignment.failed_attempt | Attempt failed, rebooking sent |
| order.scanned | Every piece is in |
| booking.reply_needs_review | A reply needs a person |
| job.completed | Optimisation finished |
| agent.suggestion_created | New route agent suggestion |
| run.started · stop.failed · run.completed | On the road |
Generate a client in any language.
Both APIs are described in OpenAPI 3.1. Point your favourite generator at the spec, or import it into Postman or Insomnia.