← Delivery integrations

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.

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:

  1. POST /uploads with {"kind":"item_photo","delivery_id":null,"mime":"image/jpeg","size":BYTE_LENGTH,"sha256":"LOWERCASE_HEX","caption":"..."}.
  2. Read upload_id; split bytes into chunks of at most 18,000 bytes. For each, POST /uploads/{id}/parts with zero-based {"part":0,"data":"BASE64_CHUNK"} and its own stable idempotency key.
  3. POST /uploads/{id}/finalize with {}. The server verifies complete size/checksum and actual image content, normalizes the image and strips metadata.
  4. Place returned image_id in parcels[].photo_ids before 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.

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.