Event: Dispatch
delivery_provider.dispatch — the order is placed and paid for. Create the delivery.
Event: Dispatch
event_type: delivery_provider.dispatch
The order exists and the customer has committed. This is the only event that creates anything, and the only one where a wrong answer strands a real order.
Request
{
"event_id": "b71e3c05-2a4f-4c11-9e83-15c9a2f0d477",
"event_type": "delivery_provider.dispatch",
"event_timestamp": 1785062700,
"user_id": 12,
"data": {
"brand_id": null,
"brand_guid": null,
"external_store_id": "PZ-0031",
"provider_store_id": "STORE-8871",
"order_guid": "6f1c2b18-3f4d-4f0e-9a6f-2f3b1a9c4d55",
"order_id": 884213,
"track_delivery": true,
"provider_quote_id": "prv_qt_abc123",
"pickup": {
"name": "Pizza Place Hillcrest",
"address": "10 Old Main Road, Hillcrest, 3610",
"latitude": "-29.778100",
"longitude": "30.762200",
"phone_number": "+27317650000",
"ready_at": null,
"deadline_at": null
},
"dropoff": {
"address": "25 Dovehouse Road, Hillcrest 3650",
"latitude": "-29.777900",
"longitude": "30.788900",
"phone_number": "+27825550100",
"ready_at": null,
"deadline_at": null
},
"manifest": {
"total_value_cents": 10590,
"sub_total_cents": 9590
},
"customer": {
"first_name": "Jane",
"last_name": "Roe",
"phone": "+27825550100",
"email": "[email protected]"
},
"order_number": "1234",
"order_items": [
{
"quantity": 1,
"name": "Large Margherita",
"unit_price_cents": 8990,
"total_price_cents": 8990
},
{
"quantity": 2,
"name": "Garlic Roll",
"unit_price_cents": 300,
"total_price_cents": 600
}
],
"payment": {
"paid": true,
"payment_type": "online",
"net_amount_paid": "105.90",
"amount_due": "0.0",
"driver_tip": "0.0"
},
"delivery_fee": "45.0",
"discount_total": "0.0",
"delivery_instructions": "Leave at the gate, dog in the yard",
"placed_at": "2026-08-07T14:05:00+02:00",
"requested_for_time": null
}
}Identity and routing
| Field | Type | Notes |
|---|---|---|
order_guid | UUID | YUMBI Gateway's order reference. Store it — it goes in the URL of every status update you push, and it is the idempotency key for this event. |
order_id | integer | The same order as a number, for platforms that cannot key on a UUID. Prefer order_guid unless yours cannot. |
provider_store_id | string | null | Your store identifier. |
external_store_id | string | YUMBI Gateway's store identifier. |
brand_id, brand_guid | null | Internal to YUMBI Gateway. Always null for you. |
provider_quote_id | string | null | The quote to honour. Absent when the channel dispatched without quoting. |
track_delivery | boolean | The channel wants live driver-position updates for this delivery. See below. |
Addresses
pickup and dropoff use the same shape as on Quote Request. At dispatch both sets of coordinates come from YUMBI Gateway's own records, so both serialise as decimal strings.
dropoff.name is not sent at dispatch — the customer's name is in the customer block instead. dropoff.deadline_at carries requested_for_time when the order is scheduled.
The order
| Field | Type | Notes |
|---|---|---|
order_number | string | null | The human-readable order number. Show this to the driver — it is what the store's staff will call out. |
order_items | array | Top-level lines only. |
manifest.total_value_cents | integer | null | Gross the customer owes, in cents. |
manifest.sub_total_cents | integer | null | Value of the goods being carried, in cents — before delivery fee and other charges. |
delivery_fee | decimal string | null | What the customer is charged for delivery. |
discount_total | decimal string | null | Total discounts applied to the order. |
delivery_instructions | string | null | The customer's free-text note. |
placed_at | ISO 8601 | null | When the order was placed. |
requested_for_time | ISO 8601 | null | Scheduled delivery time. null means ASAP. |
Providers differ on which value they want, which is why both are sent: declare the goods with sub_total_cents, and use total_value_cents where you record what the delivery is worth.
order_items
order_items{ "quantity": 1, "name": "Large Margherita", "unit_price_cents": 8990, "total_price_cents": 8990 }Top-level lines only. Modifiers and options are stored as children of their parent item and are deliberately not sent — a driver checking a bag wants the products, not the modifier tree. name is the item name as the customer sees it.
Prices are in cents. Use them if you size a manifest by value or show line prices in your courier app; ignore them if you only need names and quantities.
customer
customer| Field | Type |
|---|---|
first_name | string | null |
last_name | string | null |
phone | string | null, E.164 |
email | string | null |
customer.phone and dropoff.phone_number are the same number.
payment
payment{
"paid": true,
"payment_type": "online",
"net_amount_paid": "105.90",
"amount_due": "0.0",
"driver_tip": "0.0"
}| Field | Type | Notes |
|---|---|---|
paid | boolean | Whether any payment has been recorded against the order. |
payment_type | string | See below. |
net_amount_paid | decimal string | What the customer has already settled. |
amount_due | decimal string | null | What the driver must collect on arrival. |
driver_tip | decimal string | null | Tip already paid to the driver, included in net_amount_paid. |
payment_type is one of:
| Value | Meaning |
|---|---|
online | Paid by card through the channel. Nothing to collect. |
voucher | Settled by voucher. Nothing to collect. |
pay_in_store | The customer pays at the store. |
cash_on_delivery | The driver collects cash on arrival. |
card_on_delivery | The driver takes card payment on arrival. |
unpaid | No payment recorded and no collection type stated. |
You will only ever receive collection types you told us your drivers can handle. An order whose collection type you do not support is refused before this webhook is sent — see Payment Methods. Read amount_due rather than inferring from paid: an order can carry a partial payment and still owe money on arrival.
Monetary values in this block are decimal strings ("105.90"), not cents. The cents fields are on manifest and order_items.
Response
Return 200 with:
{
"provider_delivery_id": "prv_dl_xyz789",
"delivery_status": "assigned",
"pin_code": "0431",
"signature_required": true,
"pickup_ready_at": "2026-08-07T14:20:00+02:00",
"dropoff_deadline_at": "2026-08-07T14:50:00+02:00"
}| Field | Type | Notes |
|---|---|---|
provider_delivery_id | string | Required. Your delivery reference. Everything afterwards is keyed on it. |
delivery_status | string | A canonical status. Defaults to assigned when omitted. |
pin_code | string | null | Handover PIN for the customer to give the driver. Send it as a string — a leading zero is significant ("0431"). |
signature_required | boolean | Defaults to false. |
pickup_ready_at | ISO 8601 | null | Falls back to the quote's dropoff_eta. |
dropoff_deadline_at | ISO 8601 | null | Falls back to the quote's dropoff_deadline. |
The channel receives provider_delivery_id, pin_code and signature_required directly, and shows the PIN to the customer.
Idempotency
Be idempotent on order_guid. Dispatch is retried on timeouts and 5xx responses, and a repeat must return the existing provider_delivery_id rather than creating a second delivery. Two couriers for one order is the outcome this prevents, and it is expensive for everyone.
YUMBI Gateway also guards its own side: an order that already has a delivery in flight never reaches your endpoint again. The webhook is only sent when no active assignment exists — one that has not reached a terminal status. A delivery that was cancelled or failed leaves the order eligible for a fresh dispatch, which will be a new order_guid-scoped attempt against the same order, so expect to see the same order_guid again after a terminal outcome and treat it as a new delivery.
Live tracking
track_delivery: true asks you for driver-position pings. Honour it by pushing update_type: "location" updates as the driver moves — see Pushing Status Updates. If you cannot supply positions, ignore the flag; nothing breaks, the customer simply has no live map.
Failing
Return a non-2xx. No delivery is recorded and the order stays live, so the channel can re-dispatch or fall back to the store's own driver.
HTTP/1.1 422 Unprocessable Entity
{ "error": "no_drivers_available" }The channel receives a 422 carrying is_successful: false and your body under provider_response.
Updated about 17 hours ago
