Delivery API and webhooks
Authentication and duplicate protection
Create an API key from Website integrations while signed in as the shipping business. Send it from your backend with Authorization: Bearer YOUR_SECRET. All POST operations require a unique Idempotency-Key. Retry a timed-out operation with exactly the same key and body. A changed body with the same key returns 409. Credentials cannot perform driver actions or administrator operations.
Base path: /api/v1 on the website’s HTTPS origin. Requests and responses are JSON. Errors contain {"ok":false,"error":"..."}. GET operations are read-only. Timestamps are UTC epoch seconds; all monetary amounts use the currency’s integer minor units. Do not send a claim that the delivery is paid: the server checks the business’s verified prepaid delivery balance.
1. Options and quote
GET /api/v1/delivery-options returns enabled country policies, geographic bounds, fees, vehicle types and limits. Get a quote with the complete booking payload:
POST /api/v1/delivery-quotes
Authorization: Bearer YOUR_SECRET
Idempotency-Key: order-1042-quote-1
Content-Type: application/json
{
"external_reference": "ORDER-1042",
"pickup": {
"address": "Warehouse street address", "country": "US",
"unit": "Loading bay 2", "instructions": "Ask for dispatch",
"location": {"latitude": 40.71, "longitude": -74.01},
"contact": {"name": "Dispatch", "phone": "+12025550101"}
},
"dropoff": {
"address": "Recipient street address", "country": "US",
"unit": "Apartment 4B", "instructions": "Ring buzzer",
"location": {"latitude": 40.72, "longitude": -74.00},
"contact": {"name": "Alex", "phone": "+12025550102"}
},
"parcels": [{
"description": "Sealed glass jars", "quantity": 1,
"category": "groceries",
"weight": {"value": 2.5, "unit": "kg"},
"dimensions": {"length": 30, "width": 20, "height": 15, "unit": "cm"},
"handling": ["fragile", "liquid", "food", "upright"],
"handling_notes": "Keep sealed", "photo_ids": []
}],
"payment": {"mode": "prepaid", "goods_minor": 0},
"proof": {"photo_required": true, "signature_required": true}
}The quote response includes quote_id, expires_at, normalized parcel measurements and quote.payment.fee_minor. Quotes expire after ten minutes. The fixed quote uses the configured base fee plus per-kilometre rate and an explicitly estimated distance (straight-line distance × 1.3); it is not a road-route distance promise. Confirm the price with your customer before booking. Request a new quote/key after expiry or any payload change.
Categories: documents, clothing, groceries, prepared_food, electronics, household, flowers, parcel, other. Handling flags: fragile, liquid, food, perishable, upright, no_stack, other. Custom handling requires handling_notes. Use kg/lb and cm/in units. The service enforces configured total weight, dimension and distance limits; drivers confirm actual vehicle suitability before accepting.
2. Create and follow the delivery
POST /api/v1/deliveries
Idempotency-Key: order-1042-book-1
{"quote_id":"quo_..."}The server creates one request per quote. A business’s nonempty external_reference is unique. Default prepaid booking deducts the delivery fee and commits the booking, ledger entry, events and webhook queue in one transaction. Insufficient funds return 409 without creating a shipment. Credits currently come from administrator-verified deposits; this release does not charge a card automatically.
GET /api/v1/deliveries: your latest 100 shipments.GET /api/v1/deliveries/{id}: current state, version, assignment, tracking, event history and authorized proof references.GET /api/v1/deliveries/{id}/events: full ordered event history for reconciliation.GET /api/v1/deliveries/{id}/proof: finalized proof references.POST /api/v1/deliveries/{id}/cancel:{"version":CURRENT_VERSION,"reason":"..."}. Only before pickup; prepaid fee is refunded once.
Tracking ID and pickup_at are absent before physical pickup. The driver’s Shipment received action creates them exactly once. Deliveries follow requested → accepted → at_pickup → in_transit → at_dropoff → delivered. Exceptions and returns keep a full history. Required signature/photo or collection cannot be bypassed by a direct API call. Driver GPS updates are available while the driver keeps the delivery screen open; the displayed observed time shows when a position becomes stale.
3. Item-photo uploads
Photos belong to a business and are private. Do not submit arbitrary remote image URLs. For each JPEG/PNG photo:
POST /uploadswith{"kind":"item_photo","delivery_id":null,"mime":"image/jpeg","size":BYTE_LENGTH,"sha256":"LOWERCASE_HEX","caption":"..."}.- Read upload_id; split bytes into chunks of at most 18,000 bytes. For each,
POST /uploads/{id}/partswith zero-based{"part":0,"data":"BASE64_CHUNK"}and its own stable idempotency key. POST /uploads/{id}/finalizewith{}. The server verifies complete size/checksum and actual image content, normalizes the image and strips metadata.- Place returned image_id in
parcels[].photo_idsbefore requesting the quote.
At most five item images per parcel, ten parcel lines, thirty distinct item images per shipment. Each upload is limited to 2 MB, with decoded photo dimensions bounded to 20 million pixels. GET /images/{id} returns authorized image MIME, purpose, caption and base64 bytes. Images cannot be retrieved by another business or an ineligible driver. Item photos never satisfy delivery-proof requirements.
4. Signed webhooks
POST /api/v1/webhook-subscriptions (alias /webhooks) with {"url":"https://your-shop.example/delivery-events"}. Save the returned secret immediately; it is shown once. Reply to the signed webhook.verify event with HTTP 200 and JSON containing exactly the received challenge value. No delivery events are sent to an unverified subscription.
{
"event_id":"evt_...", "event_type":"delivery.picked_up",
"schema_version":"1", "occurred_at":1793731330,
"business_id":"123", "delivery_id":"del_...",
"delivery_version":6, "external_reference":"ORDER-1042",
"data":{
"status":"in_transit", "tracking_id":"trk_...",
"picked_up_at":1793731330,
"driver":{"id":44,"display_name":"Sam"},
"vehicle":{"id":55,"make":"Example","model":"Van","plate":"TEST","type":"van"},
"parcel_count":1,"condition":"Received sealed"
}
}Verify X-Delivery-Signature: v1=HEX as HMAC-SHA256 of X-Delivery-Timestamp + "." + exact_raw_body. Compare in constant time, reject timestamps outside your allowed window (suggested five minutes), and persist event_id before applying the event. Retries retain the event ID and receive a fresh signing timestamp.
Events include created, payment_confirmed, requested, accepted, arrived_pickup, picked_up, arrived_dropoff, proof_added, delivered, exception, retry_started, return_started, returned, released and cancelled, prefixed with delivery.. Delivered includes the recipient, time, collection amount and evidence references. Images/signatures are not embedded in webhook events.
Delivery is at least once and can arrive out of order. Compare delivery_version; reconcile gaps using the delivery/history endpoint. Return 2xx only after durable receipt. Timeouts, 429 and 5xx retry with delays of 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours and daily retries; repeated failures stop for manual replay. Most other 4xx are permanent failures. HTTPS port 443 only; redirects and private/loopback destinations are refused.
GET /webhooks: subscriptions and recent attempts.POST /webhooks/{subscription_id}/disable: stop sending.POST /webhooks/{subscription_id}/rotate: new secret and verification challenge; copy the secret once.POST /webhooks/{outbox_id}/replay: replay an attempt, preserving its original event ID.POST /keys/{key_id}/revoke: revoke a key from the signed-in integrations screen.
Keep the signing secret separate from the booking API credential. Do not expose either in client-side code or logs.
Multi-carrier shipments
Read the multi-carrier API guide for routing through handover hubs, private carrier offers and counteroffers, combined prepaid quotes and confirmed custody transfers. TTISG uses the same form and shipper API as other businesses.