Appearance
Deliveroo Integration Setup
Overview
Deliveroo is a food delivery platform. This integration lets you receive Deliveroo orders directly in Upvendo and manage them alongside your other orders. The integration is location-scoped -- each Deliveroo site (restaurant) is linked to a specific Upvendo location. Menu data is pushed from Upvendo to Deliveroo, and orders flow from Deliveroo into Upvendo via webhooks.
Integration Type: Third-party delivery channel (location-scoped) Sync Direction: Menu is one-way (Upvendo to Deliveroo); Orders are one-way (Deliveroo to Upvendo) Payment Handling: Deliveroo handles all customer payments. You receive payouts from Deliveroo directly. Authentication: OAuth2 client credentials grant using platform-level Deliveroo app credentials (not per-merchant OAuth). Access tokens are cached per location.
Purpose
The Deliveroo integration serves restaurants that want to:
- Receive Deliveroo orders inside the Upvendo back-office and kitchen workflow
- Push their Upvendo menu (items, categories, modifiers, prices, schedules) to Deliveroo
- Sync item availability (out of stock / hidden) without re-uploading the full menu
- Avoid manual double-entry of menu data across platforms
Key Concepts
| Concept | Description |
|---|---|
| Site ID | Deliveroo's identifier for a specific restaurant location. Entered by the merchant from the Deliveroo Restaurant Hub. In order webhooks it arrives as location_id. |
| Brand ID | Deliveroo groups sites under a brand. The brand ID is fetched automatically from the Site ID during setup (via the Site API). |
| Market | The country/region code associated with the brand (e.g. uk, fr, be). Resolved automatically from the brand list during setup. |
| Sandbox Mode | Deliveroo provides a sandbox environment for testing. It is deployment-owned, not merchant-selectable -- the backend forces sandbox on every non-production deployment and production on the production deployment. |
| Third-Party Integration Record | An internal Upvendo ThirdPartyIntegration record storing credentials, sync status, and settings per location. Provider is deliveroo. |
| Sync Status | Tracked per integration record. The ThirdPartySyncStatus enum has exactly three values: standby, syncing, error. (The back-office UI also displays legacy labels such as in_progress/success/ready if present, but the enum written by the backend is the three above.) |
| PLU | The identifier sent to Deliveroo for menu items/modifiers. Items use the item's own plu; modifiers use kitchenName_modifierId. |
| Mealtime | Deliveroo's concept for a menu section with its own schedule. Maps to an Upvendo Menu. |
| Tabletless site | Deliveroo marks each order payload with an is_tabletless flag. On a tabletless site there is no Deliveroo tablet, so Upvendo owns the accept decision. On a tablet site the restaurant accepts on the Deliveroo tablet and Upvendo only ingests the order once it reaches accepted. |
Prerequisites
1. Deliveroo Partner Account
- Active Deliveroo for Business account
- Restaurant approved on Deliveroo platform
- Access to Deliveroo Restaurant Hub
- Your Site ID from the Deliveroo Restaurant Hub
2. Upvendo Environment Variables
The backend requires platform-level secrets (configured by the Upvendo team, not by the merchant), under config('services.deliveroo'):
DELIVEROO_ENABLED-- platform kill switch, defaults tofalse. While it is off, connecting on production fails with "Deliveroo connections are currently disabled". Non-production deployments bypass the gate.DELIVEROO_CLIENT_ID-- OAuth client ID for the Deliveroo APIDELIVEROO_CLIENT_SECRET-- OAuth client secret for the Deliveroo APIDELIVEROO_WEBHOOK_SECRET-- HMAC secret used to verify incoming order/menu webhooksDELIVEROO_WEBHOOK_VERSION-- expectedx-deliveroo-webhook-versionheader value (default1)DELIVEROO_CONNECT_TIMEOUT(default5) /DELIVEROO_TIMEOUT(default20) /DELIVEROO_MAX_RETRIES(default2) -- HTTP client tuning for Deliveroo API callsDELIVEROO_MENU_V3_THRESHOLD_BYTES-- payload size at which menu sync switches to the v3 async upload (default 5 MiB)DELIVEROO_PRODUCTION_API_URL/DELIVEROO_SANDBOX_API_URL/DELIVEROO_PRODUCTION_AUTH_URL/DELIVEROO_SANDBOX_AUTH_URL-- host overrides (defaults are the Deliveroo URLs listed under Business Rules)DELIVEROO_ORDER_WEBHOOK_URL/DELIVEROO_MENU_WEBHOOK_URL-- webhook URLs registered with Deliveroo
3. Menu Setup
- Menu items should be created in Upvendo first
- Prices can differ from in-store prices (sync pushes Upvendo prices as-is; use a separate menu for delivery markup)
- Ensure items have correct tax rates assigned (the delivery tax rate is used)
- Set allergen and dietary preference data on items (synced to Deliveroo)
- Assign kitchen names to items and modifiers (used as
operational_nameand in modifier PLUs)
Not fully verified here: exact Deliveroo Hub onboarding steps and any location-address/business-hours requirements on the Deliveroo side. Opening hours are pushed from Upvendo on connect (see Location Sync below).
Setup Steps
Availability: Deliveroo self-serve connect is gated by the platform flag
DELIVEROO_ENABLED(defaults tofalse). The back-office Add Channel dialog shows the Deliveroo tile only when the backend returnsdeliverooin the online-channelsapplicablelist -- i.e.DELIVEROO_ENABLED=trueand the merchant is not US-based (OrderingChannelService::onlineChannels, which excludes Deliveroo and Trivec for US merchants). There is no longer a non-production build gate on the tile. A listed tile can still render disabled when the merchant has no POS integration (pos_integration_required) or no location is selected (location_required). Separately, the connect call itself is rejected with "Deliveroo connections are currently disabled" only on a production backend with the flag off (DeliverooService::enableCredentials:! config('services.deliveroo.enabled') && app()->environment('production')) -- non-production backends bypass it. Merchants on production need the Upvendo team to enable the flag. The/deliveroopage itself is reachable by direct URL, and appears in the navigation once the channel is active.
1. Connect Deliveroo Integration
- Go to the Deliveroo integration page
- Enter your Site ID
- Click Connect
The page shows a read-only sandbox notice; there is no merchant-facing environment switch -- see Business Rules.
What happens behind the scenes (DeliverooService::enableCredentials):
- A
ThirdPartyIntegrationrecord is created with providerdeliverooand an initial sync status ofstandby("Integration enabled, awaiting first sync") - Credentials are validated by requesting an OAuth access token (client credentials grant)
- The Brand ID is fetched from the Site ID via the Deliveroo Site API
- The brand list is fetched and the Market code is resolved for the brand
site_id,brand_id, andmarketare stored in the record, sync status set tostandby("Integration enabled")SyncLocationToDeliverooJobis dispatched to push opening hours- If validation or any fetch fails, the integration record is rolled back (deleted) and an "Unable to setup Deliveroo integration" error is returned
2. Sync Your Menu
- On the Deliveroo page, select one or more menus
- Click Sync Menu
- The system transforms the selected menus into Deliveroo's format, validates the result locally, and uploads it (large payloads take an asynchronous upload path -- see Menu Sync below)
3. Sync Item Availability (optional)
Click Sync Availability to push unavailable/hidden item statuses to Deliveroo without re-uploading the menu. This is a full reconcile -- individual item and stock changes are pushed automatically without it.
Data Sync Details
Menu Sync (Upvendo -> Deliveroo)
Menu sync is one-way from Upvendo to Deliveroo, triggered manually via the Sync Menu action. The sync builds the full payload and, for payloads below the v3 threshold, sends it with PUT {menuUrl}/{site_id} where menuUrl is {base}/menu/v1/brands/{brand_id}/menus. Larger payloads take the v3 async path described below.
What is synced:
| Upvendo Entity | Deliveroo Entity | Details |
|---|---|---|
| Menu | Mealtime | Name, description, image (first display-group image), schedule, category references |
| Display Group (Category) | Category | Name, description, item references |
| Item | Item (type: ITEM) | Name, description, image, price, allergens, dietary preferences, PLU, delivery tax rate, alcohol flag, modifier-group references |
| Modifier Group | Modifier | Name, description, modifier item references |
| Modifier | Item (type: CHOICE) | Name, image, price, PLU, delivery tax rate, selection/repeatability constraints |
| Combo / bundle item | Modifier (type: bundle-item) | One pseudo-modifier group per combo step, plus a party_size on the parent item -- see "Combos / bundles" below |
Price conversion: Prices are sent in the smallest currency unit (cents) via the money object's cents() method.
Language mapping: The merchant's default language is mapped to a Deliveroo-compatible code. Supported: en, fr, it, ar, zh, nl. Anything else defaults to en.
Schedule handling:
- If the menu availability type is "Specific Day/Time", the schedule is transformed into Deliveroo's per-day
time_periodsformat (day_of_week 0=Monday ... 6=Sunday) - "Available all day" for a day yields a single
00:00-23:59period - Days with no time periods are omitted; if availability is not "Specific Day/Time",
scheduleisnull(no constraint)
Modifier constraints (from the modifier group's settings):
is_mandatorytrue ->min_selection= 1allow_same_modifier_more_than_onetrue ->repeatable= true; false or absent ->repeatable= falseallow_select_more_than_onewithmax_selected->max_selection= that value- Otherwise ->
max_selection= 1 - Modifier price: if the group's pricing is "Individual", each modifier's own price is used; otherwise the group's base price is applied to all modifiers in the group
Tax rates: The location's delivery tax rate is used (via TaxRateTrait::getTaxRate(...)->getDeliveryRate()), for both items and modifiers.
Menu Payload Structure
json
{
"name": "Menu",
"menu": {
"categories": [],
"items": [],
"mealtimes": [],
"modifiers": []
},
"site_ids": ["<site_id>"]
}Each item entry includes (varies slightly between ITEM and CHOICE):
id-- Upvendo item/modifier IDname/description-- keyed by the mapped language codeprice_info.price-- integer (cents);price_info.fees-- empty array;price_info.overrides-- empty unless the item is a choice inside a combo, in which case it carries one per-parent price adjustment entry (see "Combos / bundles")tax_rate-- the location's delivery tax rateplu-- item'splufor items;kitchenName_modifierIdfor modifiersoperational_name-- the kitchen nametype--ITEMorCHOICEallergies-- allergen array (empty for modifiers)diets,contains_alcohol,modifier_ids-- items onlyimage.url-- item/modifier imagemin_selection/max_selection/repeatable-- modifiers only
Combos / bundles
Items that have a combo definition are expanded before upload:
- Each combo step becomes a pseudo-modifier group with id
bundle-{itemId}-{external_step_id or step index},type: bundle-item,repeatable: false,min_selection/max_selectionfrom the step'smin_selections/max_selections, anditem_idslisting the step's choices - Those step ids are appended to the parent item's
modifier_ids, and the parent item gets aparty_size - Each choice receives a
price_info.overridesentry{ "id": "<parent item id>", "type": "ITEM", "price": <price adjustment> } - The sync fails with "Combo definition not found for Deliveroo item {id}" if the combo definition is missing, and "Deliveroo bundle step {id} has no referenced menu choices" if a step resolves to no choices that are present in the uploaded menu
Pre-flight validation
The mapped payload is validated locally before any Deliveroo call is made. The sync aborts (and the sync status goes to error with the message) if:
- Any item, category or modifier group is missing an
id, or two share the same id ("Deliveroo {label} is missing an id" / "Duplicate Deliveroo {label} id: ...") - A category references an unknown item, a mealtime references an unknown category, a modifier group references an unknown item, or an item references an unknown modifier group ("Deliveroo {owner} references unknown {target}: ...")
- An item or mealtime image URL is not a well-formed
https://URL ("Deliveroo {owner} has an invalid HTTPS image URL") - A modifier group is missing
min_selection,max_selection,repeatableortype - The payload has no menu content or no site id
Large menus: v3 async publish
The sync JSON-encodes the mapped payload and picks the upload path by size:
- Below
DELIVEROO_MENU_V3_THRESHOLD_BYTES(default 5 MiB): a singlePUT {menuUrl}/{site_id}(v1), as above - At or above the threshold: the sync presigns an upload with
PUT /menu/v3/brands/{brand_id}/menus/{site_id}, uploads the JSON to the returnedupload_url, publishes withPOST /menu/v3/brands/{brand_id}/jobs(action: publish_menu_to_live,params.menu_id,params.version), and then reads back the returnedjob_id's status. If the presign response is missingupload_urlorversionthe sync fails with "Deliveroo v3 did not return upload_url and version"
The integration log entry for a successful sync records which path was used (upload_version: v1 or v3).
Item Availability Sync (Upvendo -> Deliveroo)
DeliverooService::syncItemAvailability collects items from the integration's configured menus, classifies them, and pushes the result to Deliveroo's item-unavailabilities API:
Unavailablestatus, or tracked stock <= 0 ->unavailable_idsHiddenorInactivestatus ->hidden_ids- Sent via
PUT {base}/menu/v1/brands/{brand_id}/menus/{site_id}/item_unavailabilities/{site_id}-- menu sync consolidates all Upvendo menus into a single Deliveroo menu addressed by the site ID, so the same value is used for both path segments
This full sync only ever adds ids. Before the PUT, the current remote unavailable/hidden lists are fetched and merged (union) with the Upvendo-derived lists, so an item that became available again in Upvendo is not cleared from Deliveroo by this action. Per-item changes (the automatic path below) use the individual POST entry point instead and can set an item back to available.
Triggers -- all three run through SyncDeliverooItemAvailabilityJob (queued, WithoutOverlapping per location):
- The manual Sync Availability action -- full reconcile as described above
- Automatically, whenever an item is edited or its stock changes and the location has an active Deliveroo integration. The job pushes a single
item_unavailabilitiesentry for that item, with statushidden(item Hidden/Inactive),unavailable(item Unavailable, or tracked stock <= 0) oravailable - A successful menu upload webhook, which dispatches a full availability reconcile for the location
Location Sync (Upvendo -> Deliveroo)
SyncLocationToDeliverooJob pushes the location's opening hours to the Deliveroo site. It is dispatched automatically on connect, and on update when the site ID changes. It sends opening hours as POST {siteUrl}/opening_hours where siteUrl is {base}/site/v1/brands/{brand_id}/sites/{site_id}:
- One entry per available day (
local_start_time/local_end_time) - "Available all day" days are sent as
00:00-23:59 - Unavailable days are omitted
Webhook Events
Deliveroo sends webhooks to Upvendo at:
POST /api/webhook/deliveroo/orders(order events)POST /api/webhook/deliveroo/menu(menu upload result)
Both are protected by the verify.deliveroo-webhook middleware (VerifyDeliverooWebhook), which requires the headers x-deliveroo-sequence-guid, x-deliveroo-hmac-sha256, x-deliveroo-payload-type and x-deliveroo-webhook-version, and verifies an HMAC-SHA256 signature computed over "{sequence_guid} {raw_body}" using DELIVEROO_WEBHOOK_SECRET. Missing headers or an invalid signature return 401.
Two further gates return 422:
x-deliveroo-webhook-versionnot matchingDELIVEROO_WEBHOOK_VERSION(default1) -- "Unsupported webhook version"- An event / payload-type pair outside
order.newororder.status_updatewithx-deliveroo-payload-typeequal toevent/{event}on the orders route, ormenu.upload_resultwith payload typewebhook_menuon the menu route
A server with no DELIVEROO_WEBHOOK_SECRET configured returns 500.
Order Webhook Flow
Deliveroo POST /api/webhook/deliveroo/orders
|
WebhookController::deliverooWebhookOrders (acks 200 immediately)
|
ProcessDeliverooWebhookJob (queue: payments-medium, tries 3, backoff 30s,
timeout 120s, unique-locked per Deliveroo order id)
|
DeliverooWebhookService::handleEvent()
|
Reads order.location_id (site id) + order.brand_id
|
Matches credentials.site_id + credentials.brand_id + provider=deliveroo
-> no match: the queued job throws and retries (3 attempts) before failing
|
Routes by event / latest status_log statusThe endpoint acks 200 immediately and hands the payload to ProcessDeliverooWebhookJob; all resolution and processing happens on the queue with retry/backoff. The unique lock is keyed on the Deliveroo order id (held up to 300s) so a re-delivered webhook cannot race into a duplicate transaction.
Event / Status Handling
| Event / status | Action |
|---|---|
order.new | Branches on the payload's is_tabletless flag. Tablet sites (is_tabletless false): Upvendo takes no action unless the latest status_log entry is already accepted, in which case it injects the order -- the tablet owns the accept/reject decision. Tabletless sites: Upvendo injects the order first (transaction, transaction items, KDS entries and the payment-capture job) and only then auto-accepts via PATCH /order/v1/orders/{id} with status accepted; if injection throws an argument error the order is rejected instead. For a tabletless order with confirm_at set, Upvendo confirms immediately when confirm_at is already past, otherwise ConfirmDeliverooScheduledOrderJob is dispatched with a delay until confirm_at. |
status_log accepted | Injects the order (transaction + transaction items + KDS + payment-capture job) and sends sync_status: succeeded to Deliveroo. If injection throws, sync_status: failed is sent with the error message as reason/notes and the job fails. Skipped for is_tabletless orders -- those were already handled at order.new. |
status_log confirmed | Logged only (scheduled order confirmed). |
status_log in_kitchen / preparing | Sets the existing transaction's order_status to In Progress. |
status_log ready_for_collection / ready | Sets the existing transaction's order_status to Ready. |
status_log collected / delivered | Sets the existing transaction's order_status to Complete and marks all of its transaction items is_done = true. |
status_log rejected | Logged only -- no sync_status call and no transaction change. |
status_log canceled / cancelled | Marks the existing transaction Cancelled and sets its transaction items is_done = true. |
| other statuses | Logged as unhandled. |
The status log is sorted by occurred_at (falling back to created_at) before the latest entry is taken, and a status whose timestamp is not newer than the stored external_data.deliveroo.last_status_at is discarded as duplicate/out-of-order. After a status is handled, last_status and last_status_at are written back to the transaction's external_data.deliveroo.
Order Webhook Payload (key fields used)
json
{
"event": "order.new | order.status_update",
"body": {
"order": {
"id": "<deliveroo_order_id>",
"order_number": "<display_order_number>",
"location_id": "<site_id>",
"brand_id": "<brand_id>",
"fulfillment_type": "deliveroo | restaurant | customer",
"is_tabletless": false,
"prepare_for": "<ISO_datetime>",
"confirm_at": "<ISO_datetime, scheduled orders>",
"status_log": [ { "status": "accepted", "occurred_at": "<ISO_datetime>" } ],
"partner_order_total": { "fractional": 1200 },
"partner_order_subtotal": { "fractional": 1000 },
"offer_discount": { "fractional": 0 },
"total_price": { "currency_code": "EUR" },
"customer": { "first_name": "...", "phone_number": "..." },
"delivery_location": { "address1": "...", "city": "...", "post_code": "..." },
"items": [
{
"pos_item_id": "<upvendo_item_id>",
"name": "...",
"quantity": 1,
"unit_price": { "fractional": 1200 },
"discount_amount": { "fractional": 0 },
"total_price": { "fractional": 1200 },
"modifiers": [
{ "pos_item_id": "<upvendo_modifier_id>", "quantity": 1, "unit_price": { "fractional": 200 } }
]
}
]
}
}
}Order -> Transaction Mapping
When an order is injected:
- Items are matched to Upvendo items by
pos_item_id(logged as a warning if not found -- usually a PLU mismatch). The unmatched item is still created, falling back to the Deliveroo name/price, and its transaction item is flaggedexternal_data.deliveroo.is_fallback = true - Modifiers are matched by
pos_item_id; prices come from Deliveroo'sunit_price.fractional / 100 - A transaction is built with
order_channel = deliveroo, dining option derived fromfulfillment_type(deliverooandrestaurant-> Delivery;customerand any unrecognised value -> Pickup), totals frompartner_order_total/partner_order_subtotal/offer_discount(fractional / 100), customer name/phone/address, idempotency keydeliveroo_order_<id>, andexternal_ids.deliveroo = <order id> - The order date is parsed from
prepare_forin the location's timezone
Order metadata stored on the transaction
The transaction's external_data.deliveroo block keeps the Deliveroo-side context:
order_id,order_number,brand_id,site_id,is_tabletless,fulfillment_typelast_status/last_status_at-- the latest processed status and its timestamp (the duplicate/out-of-order guard)injected_at-- set once the order has been injected; a replayed or re-delivered webhook sees it and does nothingremake_details,payments,promotions(oroffers),meal_cardscheduling--prepare_for,start_preparing_at,confirm_atfulfillment-- fulfillment type,prepare_for,pickup_at,expected_delivery_time,confirm_at, delivery address and the rider's name/phone when Deliveroo sends them
The full Deliveroo payload is also kept on the transaction under snapshots.raw_order. Each transaction item carries its own external_data.deliveroo with external_item_id, pos_item_id, raw_item and the is_fallback flag.
Every Deliveroo webhook is additionally stored (provider deliveroo, keyed on the order id, marked success or skipped) so it can be inspected and replayed.
Menu Webhook
POST /api/webhook/deliveroo/menu is the menu upload result callback. It is not a no-op: when a site_id is present it updates the matching integration's sync_status.menu_upload_result (menu id, status, errors, sequence guid, received-at) and caches the result under deliveroo_menu_uploaded:{site_id} for 5 minutes. If the reported status is a success, it also dispatches a full item-availability reconcile for the location.
Actions (Back-Office API)
All back-office endpoints are under /api/back-office, behind the admin-vendor-override and location-owner middleware (a location belonging to another merchant returns 403). Every write action -- connect, update, disconnect, sync menu, sync availability, set site status -- additionally requires the manage-integration-settings permission; the read actions (status, logs, site status) do not.
| Action | Method | Endpoint | Description |
|---|---|---|---|
| Get Status | GET | /back-office/deliveroo/{locationId} | Returns the integration record/status, or { "is_active": false } |
| Connect | POST | /back-office/deliveroo/{locationId} | Connect with site_id |
| Update | PUT | /back-office/deliveroo/{locationId} | Update site_id and/or force_reject |
| Disconnect | DELETE | /back-office/deliveroo/{locationId} | Removes the Deliveroo integration record for the location. The stored site ID, menu_ids, sync status and logs stop being returned, and reconnecting starts a fresh record. Surfaced in the UI on the /deliveroo Settings tab as a Disconnect button behind a typed confirmation dialog (type Deliveroo); requires manage integration-settings, and returns you to Transactions on success |
| Sync Menu | POST | /back-office/deliveroo/{locationId}/sync-menu | Push selected menus (menu_ids) |
| Sync Availability | POST | /back-office/deliveroo/{locationId}/sync-availability | Push item availability |
| Get Logs | GET | /back-office/deliveroo/{locationId}/logs | Integration log + recent Deliveroo orders. Optional limit query parameter, clamped to 1-200, default 50 |
| Get Site Status | GET | /back-office/deliveroo/{locationId}/site-status | Reads the Deliveroo site's open/closed status |
| Set Site Status | PUT | /back-office/deliveroo/{locationId}/site-status | Sets it. Body { "status": ... }, one of OPEN, CLOSED, READY_TO_OPEN. Responds with { "status": ..., "updated_at": ... } |
All of these are surfaced on the /deliveroo page. Get Logs feeds the Status tab's activity panel (refreshable, limit 50 from the UI, which also renders the returned recent_orders), and Get / Set Site Status back the Status tab's Open / Ready to open / Closed toggle -- the page loads the site status on mount and on location change, and selecting Closed asks for a browser confirmation first.
Integration Logs
GET /back-office/deliveroo/{locationId}/logs returns two blocks:
sync_status-- the integration's sync-status document, including a rollinglogsarray capped at the 100 most recent entries, newest first. Each entry is{ timestamp, type, message, context };token,client_secretandauthorizationkeys are stripped fromcontextbefore it is stored. Entries are written on connect, settings update, menu submission (success and failure), availability reconcile, per-item availability change and remote site-status change.recent_orders-- the most recent Deliveroo-channel transactions for the location (limitof them), each withid,order_no,order_status,dining_option,total,currency,customer_name,created_at, plus anexternalblock with the Deliverooorder_id,order_number,fulfillment_typeandfulfillment.
Fields
Connect Request (POST)
| Field | Type | Required | Description |
|---|---|---|---|
site_id | string | Yes | Deliveroo Restaurant Hub Site ID |
is_sandbox | boolean | No | Accepted by validation but ignored -- the backend forces sandbox on every non-production deployment and production on the production deployment |
Update Request (PUT)
| Field | Type | Required | Description |
|---|---|---|---|
site_id | string | No (sometimes) | New Site ID (triggers brand ID / market re-fetch) |
is_sandbox | boolean | No (sometimes) | Accepted by validation but not applied; an integration keeps the environment it was created with |
force_reject | boolean | No (sometimes) | Sandbox certification only -- rejects incoming tabletless orders instead of accepting them. Setting it on a non-sandbox integration fails with "force_reject is available only for sandbox certification" |
Sync Menu Request (POST)
| Field | Type | Required | Description |
|---|---|---|---|
menu_ids | array of strings | Yes | Upvendo menu IDs to sync (validated to exist) |
Business Rules
One integration per location: Connecting a second Deliveroo integration on a location throws "Deliveroo integration already enabled for this location".
Credential validation on connect: Credentials are validated by requesting an OAuth token immediately. On failure during connect, the record is deleted (rollback) and "Unable to setup Deliveroo integration" is returned.
Credential rollback on update failure: If updating fails validation, the original credentials are restored and sync status is set to
errorwith message "Invalid Deliveroo credentials".Sandbox vs Production URLs:
- API base (sandbox):
https://api-sandbox.developers.deliveroo.com - API base (production):
https://api.developers.deliveroo.com - Auth (sandbox):
https://auth-sandbox.developers.deliveroo.com/oauth2/token - Auth (production):
https://auth.developers.deliveroo.com/oauth2/token
The environment is deployment-owned: forced to sandbox on every non-production deployment and to production on the production deployment, and an existing integration keeps the environment it was created with. There is no environment switch in the back office -- only an informational sandbox notice.
- API base (sandbox):
Connect is feature-flagged: On production, connecting fails with "Deliveroo connections are currently disabled" unless
DELIVEROO_ENABLED=true. Non-production deployments bypass the gate.Webhook matching: Order webhooks are matched by
credentials.site_idandcredentials.brand_idon adeliveroointegration (the match does not checkis_active). A payload missing either id is logged and dropped; a non-matching pair fails the queued job, which retries 3 times before landing in failed jobs. Deliveroo always receives a 200 from the endpoint itself.Webhook signature: Webhooks must pass HMAC-SHA256 verification against
DELIVEROO_WEBHOOK_SECRET, else 401.Auto-accept applies only to tabletless sites: Upvendo auto-accepts new orders only when the payload's
is_tabletlessflag is set. Tablet sites decide on the Deliveroo tablet and Upvendo only ingests once the order reachesaccepted. There is no merchant-facing auto-accept toggle. Theforce_rejectsetting rejects instead of accepting, and only fires when the order is tabletless and the integration is sandbox -- it exists for sandbox certification.Order injection is idempotent:
external_data.deliveroo.injected_atprevents a replayed or re-delivered webhook from creating a second transaction, and every Deliveroo webhook is stored for inspection and replay.Payment is external: Deliveroo handles all payments; no Upvendo payment profile is needed for Deliveroo orders.
Menu sync is full payload: Each sync sends the complete menu for the selected menus; there is no delta sync. (Availability has a separate endpoint, which only ever adds unavailable/hidden ids -- see Item Availability Sync.)
Order Flow
Customer orders on Deliveroo
|
order.new webhook
|
+----+--------------------------------------------+
| |
tabletless site tablet site
| |
transaction created in Upvendo restaurant accepts on the
(Deliveroo channel), then auto-accept Deliveroo tablet
| |
| "accepted" status webhook ->
| transaction created in Upvendo
+----+--------------------------------------------+
|
Sent to KDS + payment-capture job
|
Kitchen prepares order (in_kitchen / ready status webhooks
move the order through In Progress and Ready)
|
Rider picks up -> collected/delivered webhook -> order CompleteNote: Payment is handled by Deliveroo. You receive payouts from Deliveroo directly.
FAQs
Can I use different prices on Deliveroo than in my store?
Not through this integration directly -- the sync pushes Upvendo item prices as-is (converted to cents). To charge a delivery markup, create a separate Upvendo menu for Deliveroo and sync that.
What allergen and dietary data is sent to Deliveroo?
Each item's allergens (allergies) and dietary preferences (diets), plus the contains_alcohol flag. Set these correctly before syncing.
Can I sync multiple menus at once?
Yes. Sync Menu accepts an array of menu IDs; all selected menus are combined into one payload.
Does the menu sync automatically when I change items in Upvendo?
The full menu sync is manual -- select menus and click Sync Menu. Item availability is not: editing an item or changing its stock automatically queues a per-item availability push to Deliveroo (hidden / unavailable / available), and a successful menu upload triggers a full availability reconcile. Use the Sync Availability button only when you want to force that full reconcile.
What happens if I change my Site ID?
Updating the Site ID re-fetches the Brand ID and Market. If the new credentials fail validation, the original credentials are restored.
Can I have Deliveroo on multiple locations?
Yes. Each location has its own Deliveroo integration with its own Site ID, configured and synced independently.
How are modifier prices handled?
If the modifier group's pricing is "Individual", each modifier's own price is sent; otherwise the group's base price is applied to all modifiers in that group.
How do I disconnect Deliveroo?
On /deliveroo, open the Settings tab and click Disconnect, then type Deliveroo in the confirmation dialog. This calls DELETE /back-office/deliveroo/{locationId}, which removes the integration record for that location; reconnecting starts a fresh record. The button requires the manage integration-settings permission.
Troubleshooting
Orders not coming through
- Confirm the integration is connected and the site ID is correct
- Verify
DELIVEROO_WEBHOOK_SECRETis configured and incoming signatures are valid (else 401) - Ensure the order's
location_id(site id) andbrand_idmatch the integration's stored site/brand IDs - On a tablet site, an order only reaches Upvendo once it has been accepted on the Deliveroo tablet -- a placed-but-not-accepted order is expected to be absent
- Check the integration logs (
GET /back-office/deliveroo/{locationId}/logs) for webhook errors
"Deliveroo connections are currently disabled" on connect
- The platform kill switch
DELIVEROO_ENABLEDis off on this production deployment - This is not merchant-fixable -- escalate to the Upvendo team to enable the flag
"Unable to setup Deliveroo integration" error
- The Site ID may be incorrect -- double-check it in the Deliveroo Restaurant Hub
- The platform credentials (
DELIVEROO_CLIENT_ID/DELIVEROO_CLIENT_SECRET) may be invalid -- contact the Upvendo team - The record is rolled back automatically on failure; you can retry
"Invalid Deliveroo credentials" on update
- The new Site ID may be wrong -- the system restores the original credentials on failure
- The integration sync status will show
errorwith details
Menu sync fails
- Ensure at least one menu is selected and items have valid data (names, prices, delivery tax rates, images)
- Verify the Brand ID is set on the integration (fetched automatically at connect)
- Many failures come from Upvendo's own pre-flight validation before Deliveroo is called -- duplicate or missing ids, a category/mealtime/modifier pointing at something that is not in the uploaded menu, or an image URL that is not a valid
https://URL (see Pre-flight validation) - Review the integration logs for the exact message; a failed sync also writes it to the sync status
Order shows wrong prices
- Prices are taken from Upvendo items at the time of menu sync, in cents; order totals come from Deliveroo's fractional amounts
- Re-sync the menu after updating prices, and verify the delivery tax rate
Webhook accepted but no order appears
- Deliveroo always receives a 200 from the webhook endpoint, so a successful delivery on Deliveroo's side does not mean the order was ingested
- The payload's
location_id(site id) andbrand_idmust both match the integration's stored credentials. If they do not, the queued job fails after 3 attempts and the failure shows up in failed jobs rather than as an HTTP error - If you recently changed the Site ID, confirm the new credentials saved successfully
Assistant Guidance
When helping users with the Deliveroo integration:
- Verify the Site ID is correct before troubleshooting other issues
- Remind users that the full menu sync is manual -- but item availability does auto-push when an item or its stock changes, so do not tell users availability requires a manual click
- There is no environment switch in the UI -- the environment is deployment-owned, and moving an integration between environments is a reconnection
- For order issues, check webhook signature/headers (401) and site/brand matching first; a missing order on a tablet site usually just means it has not been accepted on the Deliveroo tablet yet
- Deliveroo payments are fully external -- do not suggest configuring payment profiles for Deliveroo