Skip to content

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

ConceptDescription
Site IDDeliveroo'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 IDDeliveroo groups sites under a brand. The brand ID is fetched automatically from the Site ID during setup (via the Site API).
MarketThe country/region code associated with the brand (e.g. uk, fr, be). Resolved automatically from the brand list during setup.
Sandbox ModeDeliveroo 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 RecordAn internal Upvendo ThirdPartyIntegration record storing credentials, sync status, and settings per location. Provider is deliveroo.
Sync StatusTracked 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.)
PLUThe identifier sent to Deliveroo for menu items/modifiers. Items use the item's own plu; modifiers use kitchenName_modifierId.
MealtimeDeliveroo's concept for a menu section with its own schedule. Maps to an Upvendo Menu.
Tabletless siteDeliveroo 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 to false. 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 API
  • DELIVEROO_CLIENT_SECRET -- OAuth client secret for the Deliveroo API
  • DELIVEROO_WEBHOOK_SECRET -- HMAC secret used to verify incoming order/menu webhooks
  • DELIVEROO_WEBHOOK_VERSION -- expected x-deliveroo-webhook-version header value (default 1)
  • DELIVEROO_CONNECT_TIMEOUT (default 5) / DELIVEROO_TIMEOUT (default 20) / DELIVEROO_MAX_RETRIES (default 2) -- HTTP client tuning for Deliveroo API calls
  • DELIVEROO_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

  1. Menu items should be created in Upvendo first
  2. Prices can differ from in-store prices (sync pushes Upvendo prices as-is; use a separate menu for delivery markup)
  3. Ensure items have correct tax rates assigned (the delivery tax rate is used)
  4. Set allergen and dietary preference data on items (synced to Deliveroo)
  5. Assign kitchen names to items and modifiers (used as operational_name and 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 to false). The back-office Add Channel dialog shows the Deliveroo tile only when the backend returns deliveroo in the online-channels applicable list -- i.e. DELIVEROO_ENABLED=true and 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 /deliveroo page itself is reachable by direct URL, and appears in the navigation once the channel is active.

1. Connect Deliveroo Integration

  1. Go to the Deliveroo integration page
  2. Enter your Site ID
  3. 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 ThirdPartyIntegration record is created with provider deliveroo and an initial sync status of standby ("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, and market are stored in the record, sync status set to standby ("Integration enabled")
  • SyncLocationToDeliverooJob is 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

  1. On the Deliveroo page, select one or more menus
  2. Click Sync Menu
  3. 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 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 EntityDeliveroo EntityDetails
MenuMealtimeName, description, image (first display-group image), schedule, category references
Display Group (Category)CategoryName, description, item references
ItemItem (type: ITEM)Name, description, image, price, allergens, dietary preferences, PLU, delivery tax rate, alcohol flag, modifier-group references
Modifier GroupModifierName, description, modifier item references
ModifierItem (type: CHOICE)Name, image, price, PLU, delivery tax rate, selection/repeatability constraints
Combo / bundle itemModifier (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_periods format (day_of_week 0=Monday ... 6=Sunday)
  • "Available all day" for a day yields a single 00:00-23:59 period
  • Days with no time periods are omitted; if availability is not "Specific Day/Time", schedule is null (no constraint)

Modifier constraints (from the modifier group's settings):

  • is_mandatory true -> min_selection = 1
  • allow_same_modifier_more_than_one true -> repeatable = true; false or absent -> repeatable = false
  • allow_select_more_than_one with max_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.

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 ID
  • name / description -- keyed by the mapped language code
  • price_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 rate
  • plu -- item's plu for items; kitchenName_modifierId for modifiers
  • operational_name -- the kitchen name
  • type -- ITEM or CHOICE
  • allergies -- allergen array (empty for modifiers)
  • diets, contains_alcohol, modifier_ids -- items only
  • image.url -- item/modifier image
  • min_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_selection from the step's min_selections / max_selections, and item_ids listing the step's choices
  • Those step ids are appended to the parent item's modifier_ids, and the parent item gets a party_size
  • Each choice receives a price_info.overrides entry { "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, repeatable or type
  • 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 single PUT {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 returned upload_url, publishes with POST /menu/v3/brands/{brand_id}/jobs (action: publish_menu_to_live, params.menu_id, params.version), and then reads back the returned job_id's status. If the presign response is missing upload_url or version the 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:

  • Unavailable status, or tracked stock <= 0 -> unavailable_ids
  • Hidden or Inactive status -> 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):

  1. The manual Sync Availability action -- full reconcile as described above
  2. Automatically, whenever an item is edited or its stock changes and the location has an active Deliveroo integration. The job pushes a single item_unavailabilities entry for that item, with status hidden (item Hidden/Inactive), unavailable (item Unavailable, or tracked stock <= 0) or available
  3. 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-version not matching DELIVEROO_WEBHOOK_VERSION (default 1) -- "Unsupported webhook version"
  • An event / payload-type pair outside order.new or order.status_update with x-deliveroo-payload-type equal to event/{event} on the orders route, or menu.upload_result with payload type webhook_menu on 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 status

The 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 / statusAction
order.newBranches 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 acceptedInjects 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 confirmedLogged only (scheduled order confirmed).
status_log in_kitchen / preparingSets the existing transaction's order_status to In Progress.
status_log ready_for_collection / readySets the existing transaction's order_status to Ready.
status_log collected / deliveredSets the existing transaction's order_status to Complete and marks all of its transaction items is_done = true.
status_log rejectedLogged only -- no sync_status call and no transaction change.
status_log canceled / cancelledMarks the existing transaction Cancelled and sets its transaction items is_done = true.
other statusesLogged 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:

  1. 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 flagged external_data.deliveroo.is_fallback = true
  2. Modifiers are matched by pos_item_id; prices come from Deliveroo's unit_price.fractional / 100
  3. A transaction is built with order_channel = deliveroo, dining option derived from fulfillment_type (deliveroo and restaurant -> Delivery; customer and any unrecognised value -> Pickup), totals from partner_order_total / partner_order_subtotal / offer_discount (fractional / 100), customer name/phone/address, idempotency key deliveroo_order_<id>, and external_ids.deliveroo = <order id>
  4. The order date is parsed from prepare_for in 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_type
  • last_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 nothing
  • remake_details, payments, promotions (or offers), meal_card
  • scheduling -- prepare_for, start_preparing_at, confirm_at
  • fulfillment -- 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.

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.

ActionMethodEndpointDescription
Get StatusGET/back-office/deliveroo/{locationId}Returns the integration record/status, or { "is_active": false }
ConnectPOST/back-office/deliveroo/{locationId}Connect with site_id
UpdatePUT/back-office/deliveroo/{locationId}Update site_id and/or force_reject
DisconnectDELETE/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 MenuPOST/back-office/deliveroo/{locationId}/sync-menuPush selected menus (menu_ids)
Sync AvailabilityPOST/back-office/deliveroo/{locationId}/sync-availabilityPush item availability
Get LogsGET/back-office/deliveroo/{locationId}/logsIntegration log + recent Deliveroo orders. Optional limit query parameter, clamped to 1-200, default 50
Get Site StatusGET/back-office/deliveroo/{locationId}/site-statusReads the Deliveroo site's open/closed status
Set Site StatusPUT/back-office/deliveroo/{locationId}/site-statusSets 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 rolling logs array capped at the 100 most recent entries, newest first. Each entry is { timestamp, type, message, context }; token, client_secret and authorization keys are stripped from context before 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 (limit of them), each with id, order_no, order_status, dining_option, total, currency, customer_name, created_at, plus an external block with the Deliveroo order_id, order_number, fulfillment_type and fulfillment.

Fields

Connect Request (POST)

FieldTypeRequiredDescription
site_idstringYesDeliveroo Restaurant Hub Site ID
is_sandboxbooleanNoAccepted by validation but ignored -- the backend forces sandbox on every non-production deployment and production on the production deployment

Update Request (PUT)

FieldTypeRequiredDescription
site_idstringNo (sometimes)New Site ID (triggers brand ID / market re-fetch)
is_sandboxbooleanNo (sometimes)Accepted by validation but not applied; an integration keeps the environment it was created with
force_rejectbooleanNo (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)

FieldTypeRequiredDescription
menu_idsarray of stringsYesUpvendo menu IDs to sync (validated to exist)

Business Rules

  1. One integration per location: Connecting a second Deliveroo integration on a location throws "Deliveroo integration already enabled for this location".

  2. 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.

  3. Credential rollback on update failure: If updating fails validation, the original credentials are restored and sync status is set to error with message "Invalid Deliveroo credentials".

  4. 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.

  5. Connect is feature-flagged: On production, connecting fails with "Deliveroo connections are currently disabled" unless DELIVEROO_ENABLED=true. Non-production deployments bypass the gate.

  6. Webhook matching: Order webhooks are matched by credentials.site_id and credentials.brand_id on a deliveroo integration (the match does not check is_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.

  7. Webhook signature: Webhooks must pass HMAC-SHA256 verification against DELIVEROO_WEBHOOK_SECRET, else 401.

  8. Auto-accept applies only to tabletless sites: Upvendo auto-accepts new orders only when the payload's is_tabletless flag is set. Tablet sites decide on the Deliveroo tablet and Upvendo only ingests once the order reaches accepted. There is no merchant-facing auto-accept toggle. The force_reject setting rejects instead of accepting, and only fires when the order is tabletless and the integration is sandbox -- it exists for sandbox certification.

  9. Order injection is idempotent: external_data.deliveroo.injected_at prevents a replayed or re-delivered webhook from creating a second transaction, and every Deliveroo webhook is stored for inspection and replay.

  10. Payment is external: Deliveroo handles all payments; no Upvendo payment profile is needed for Deliveroo orders.

  11. 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 Complete

Note: 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_SECRET is configured and incoming signatures are valid (else 401)
  • Ensure the order's location_id (site id) and brand_id match 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_ENABLED is 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 error with details
  • 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) and brand_id must 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