| Direction | Purpose |
| Deliverect -> OrderLemon | Menu, opening hours, busy mode, snooze, order status sync |
| OrderLemon -> Deliverect | paid orders and OrderLemon order status updates |
| Entry point | File | Purpose |
| /v1/webhooks/deliverect.php | v1/webhooks/deliverect.php | Main webhook router |
| send_order_to_deliverect | v1/POST/send_order_to_deliverect.php | Direct API function for sending an OL order to Deliverect |
Webhook handlers live in: `v1/webhooks/deliverect/`
| Handler | Purpose |
| menu_update.php | Menu, products, categories, modifiers, relations, images, availabilities |
| opening_hours.php | Deliverect opening hours -> OrderLemon |
| order_status_update.php | Deliverect order status -> OrderLemon |
| busy_mode.php | Busy delay, and PAUSED closing the shop for online orders |
| snooze_unsnooze.php | Product snooze / unsnooze |
| payment_completed.php | Paid OL order -> Deliverect |
| order_updated.php | OL order status update -> Deliverect |
| channel_registration.php | Deliverect channel registration |
| sync_opening_hours.php | Manual OL opening-hours sync -> Deliverect via Postman |
| Direction | Scheme | Header |
| Deliverect → OL | HMAC-SHA256 over the raw body | x-server-authorization-hmac-sha256 |
| OL → this microservice | Bearer token | Authorization: Bearer <token> |
| Environment | Accepted secret |
| PROD | `hmac_secret` only, the partner-level secret from Deliverect → Settings → Partner Integration. A `channelLinkId` is never a secret here |
| DEV | the channel's `channelLinkId`, because Deliverect staging has no partner page and signs with it |
Routing priority:
| Priority | Source | Notes |
| 1 | Query parameter for internal OL events | `?event=payment_completed`, `?event=order_updated`, `?event=sync_opening_hours` |
| 2 | Payload field | `type` or `event.type` |
| 3 | Payload shape detection | Used when no explicit type is present |
| 4 | Query parameter fallback | Accepted only if payload shape is compatible |
Supported query parameters:
?event=payment_completed
?event=order_updated
?event=sync_opening_hours
?event=menu_update
?event=order_status_update
?event=snooze_unsnooze
?event=busy_mode
?event=opening_hours
?event=channel_registration
Internal OrderLemon webhook URLs usually use `?event=`.
Example:
/v1/webhooks/deliverect?event=payment_completed
| Event | Direction | Purpose |
| menu_update | Deliverect -> OL | Sync products, categories, modifiers, relations, images, and availabilities |
| opening_hours | Deliverect -> OL | Sync weekly opening hours into OrderLemon |
| order_status_update | Deliverect -> OL | Update OrderLemon order status |
| busy_mode | Deliverect -> OL | BUSY sets a temporary delay through shops.min_timeframe, PAUSED closes the shop for online orders; ONLINE reverts both |
| snooze_unsnooze | Deliverect -> OL | Handle temporary product unavailability, for the addressed product only never for the products it is made of |
| channel_registration | Deliverect -> OL | Register channel link and return webhook URLs |
| payment_completed | OL -> Deliverect | Send a paid OrderLemon order to Deliverect |
| order_updated | OL -> Deliverect | Send OrderLemon order status changes to Deliverect |
| sync_opening_hours | OL -> Deliverect | Manual opening-hours sync, currently blocked by missing OL webhook event |
Reference: https://developers.deliverect.com/page/order-status
| Deliverect Status | Meaning |
| 0 | Unknown |
| 1 | Parsed |
| 2 | Received by POS |
| 3 | Sent to DMA |
| 4 | Before parsed |
| 5 | Receipt not found |
| 6 | Received by DMA |
| 7 | Printed by DMA |
| 10 | New (received at POS) |
| 20 | Accepted |
| 25 | Scheduled |
| 35 | Denied |
| 40 | Printed |
| 50 | Preparing |
| 60 | Prepared |
| 70 | Pickup_ready |
| 80 | In_delivery |
| 81 | Delivery created |
| 83 | En route to pickup |
| 84 | Almost at pickup |
| 85 | Arrived at pickup |
| 87 | En route to dropoff |
| 89 | Arrived at dropoff |
| 90 | Finalized / Delivered |
| 95 | Auto Finalized |
| 100 | Channel cancel |
| 110 | Canceled |
| 115 | Delivery canceled |
| 120 | Failed |
| 121 | POS failed |
| 122 | Retry failed |
| 124 | Parse failed |
Current implementation maps Deliverect final statuses to OrderLemon `done` (`status_id=3`), not `delivered` (`status_id=7`).
| Deliverect values | OrderLemon status_id | OrderLemon label |
| 0-7, 10, 20, 25, new, parsed, received, accepted, scheduled | 2 | ordered |
| 90, 95, delivered, finalized, auto_finalized | 3 | done |
| 40, 50, 60, printed, preparing, prepared | 4 | packed |
| 70, ready, pickup_ready | 5 | readyfordelivery |
| 80, 81, 83, 84, 85, 87, 89, in_delivery | 6 | shipped |
| 35, 100, 110, 115, denied, cancelled, canceled | 8 | cancelled |
| 120, 121, 122, 124, failed | 9 | lost |
Notes:
Final Statuses: `done` (`3`) and `delivered` (`7`) are final OrderLemon statuses
Current Deliverect final mapping: Deliverect `90/95` maps to OL `done` (`3`)
Cash orders: Cash orders are marked `payment_status_label = paid` when finalized/delivered by Deliverect
Archive fallback: Status update searches `orders_active_{shop_id}` first, then `orders_archive_{shop_id}`
| OrderLemon status_id | OrderLemon label | Deliverect status |
| 1 | ordering | 10 |
| 2 | ordered | 20 |
| 3 | done | 90 |
| 4 | packed | 50 |
| 5 | readyfordelivery | 70 |
| 6 | shipped | 80 |
| 7 | delivered | 90 |
| 8 | cancelled | 110 |
| 9 | lost | 120 |
The `__G_` suffix means that an OrderLemon product row is group-scoped.
Example: XTRA-CHEESE__G_fb271dcb
Base reference: XTRA-CHEESE
The suffix identifies a specific modifier-group or upsell-group context.
This exists because the same Deliverect item can appear in multiple groups with different sync context. A scoped row allows OrderLemon to store group-specific assignment, pricing, relations, and availability without overwriting the base reference.
When comparing product identity, compare the base reference before `__G_`.
Scoped `__G_` rows are expected in modifier and upsell sync. They are not random duplicates and should not be skipped or deleted only because the base reference already exists.
| Data | Rule |
| Prices | Cents, for example `10.50` -> `1050` |
| Data/time | UTC ISO 8601, format `Y-m-d\TH:i:s\Z` |
| VAT/tax | Permyriad-style code, for example `9000 = 9%` |
| Opening hours | Deliverect uses `1..7` for Monday .. Sunday |
| OrderLemon weekdays | OrderLemon uses `0..6` for Monday .. Sunday |
Day Mapping:
| Deliverect | OrderLemon |
| 1 | 0 |
| 2 | 1 |
| 3 | 2 |
| 4 | 3 |
| 5 | 4 |
| 6 | 5 |
| 7 | 6 |
| Event | Purpose |
| payment_completed | Send paid OrderLemon order to Deliverect |
| order_updated | Send OrderLemon status change to Deliverect |
`order_updated` only works for orders that have a Deliverect order ID mapping; otherwise returns `not_deliverect_order`.
Example URLs:
/v1/webhooks/deliverect?event=payment_completed
/v1/webhooks/deliverect?event=order_updated
Bearer auth hardening is currently not enforced until OrderLemon reliably sends the expected `Authorization: Bearer <secret>` header.
Paid orders are sent from OrderLemon to Deliverect
| Area | Behavior |
| Timezone | OL pickup and delivery times are stored in shop timezone and converted to UTC |
| Pickup fallback | Past or missing pickup time becomes ASAP fallback using shop `min_timeframe`, minimum 15 minutes |
| Items | Cart rows with `parent_id` become nested `subItems[]` |
| Discounts | Negative discount rows are not sent as regular items |
| Promotions | Promotions are aggregated into one `OrderLemon Promotion` discount |
| Delivery fee | Product reference `delivery_fee` becomes `deliveryCost` |
| Required fields | Payload includes `decimalDigits` and `orderIsAlreadyPaid` |
| Self-delivery | Delivery orders include courier = "restaurant" as a plain string |
| Success response | Deliverect can return plain `OK`, not always JSON |
Delivery courier payload:
{
"courier": "restaurant"
}The microservice:
| Step | Behavior |
| 1 | Resolves shop from `channelLinkId` / `locationId` |
| 2 | Parses channelOrderId, which is shop-scoped as OL-{shop_id}-{order_id} (for example OL-7-113) the embedded shop ID must match the shop resolved from channelLinkId/locationId, and a bare order ID is rejected |
| 3 | Validates Deliverect status |
| 4 | Maps Deliverect status to OrderLemon `status_id` |
| 5 | Updates `orders_active_{shop_id}` or `orders_archive_{shop_id}` |
| 6 | Creates an OrderLemon order event to trigger downstream flow |
| 7 | Stores last Deliverect-set status to prevent echo loops with `order_updated` |
OrderLemon cart prices are gross VAT-inclusive.
Do not send `items[].tax`. It is not a valid Deliverect field for this integration.
VAT is sent at order level:
| Field | Behavior |
| taxes[] | Aggregated per VAT rate from cart item gross prices |
| deliveryCostTax | Always `0`; delivery fee VAT is not calculated separately |
| serviceChargeTax | Always `0`; because OL has no service charge concept |
`menu_update` synchronizes:
| Entity | Behavior |
| Products | Created or updated in "products_{shop_id}" |
| Categories | Created or updated as OL categories |
| Modifiers | Stored as category-level modifiers |
| Relations | Product/modifier/category relations are synchronized |
| Images | Category and product images are downloaded and uploaded to OL API |
| Availabilities | Category/menu availability becomes OL `allowed_dow` |
Reference resolution:
| Context | Reference fallback |
| Menu sync products | referenceId -> plu -> productId -> payload key (context-dependent) |
| General helper | plu -> referenceId -> productId -> deliverect_id -> _id -> id -> fallback |
| Catalog entity identity | Stable IDs are preferred before PLU/reference aliases |
Reference resolution order is context-dependent. Menu sync prefers `referenceId` first, while the general helper prefers `plu` first.
Important menu sync rules:
| Rule | Details |
| snoozedProducts | Must not change `enabled` state during `menu_update` |
| Snooze handling | Availability is handled by `snooze_unsnooze` |
| Modifiers | Modifier model is category-level, not product-level |
| Combo roots | A root that is only a shell (productType 4) is disabled while its variants stay sellable A root that is itself orderable productType 1 with a price of its own, Deliverect's "Combo with Bundles Modifier Groups as Upsell" is not split and stays enabled, because its choices are modifier groups rather than variants |
| Group-scoped rows | `__G_` rows can have group-specific price, assignment, and availability |
| Duplicate guard | Must not skip rows that have assigned modifier-group context |
| allowed_dow priority | Category availabilities > menu-level availabilities > menu schedule > empty |
Deliverect data is stored in the `metadata` JSON column on per-shop tables under the `deliverect` namespace. Other namespaces in metadata are never touched (read-modify-write).
- `categories_{shop_id}` → `external_id` (Deliverect category `_id`)
- `products_{shop_id}` → `product_type` (Deliverect productType), combo_variants (variant references for combo shells), plu (the raw Deliverect PLU, written during the product upsert)
- `orders_active/archive_{shop_id}` → `order_id` (Deliverect order `_id`, set after send), `last_status` (last OL status set by `order_status_update`, used for loop prevention)
plu exists because the stored reference is often derived from the PLU, a canonical referenceId, or a group-scoped row where every unusual character became _ and the base was cut to 48 chars (REDPEPP-#O0#- is stored as REDPEPP-_O0_-__G_c0afa2ce). That is lossy, so snooze resolves its targets through this key instead of reversing the string. Rows published before this change need one menu publish to backfill it.
OAuth2 token is account-level and cached in `shop_settings` as `deliverect.oauth.access_token` and `deliverect.oauth.expires_at` under a shop_id.
Busy mode updates temporary shop delay through `shops.min_timeframe`
busy statuses:
| Payload status | Action |
| BUSY | Add busy delay to shops.min_timeframe |
| PAUSED, CLOSED, OFFLINE | Close the shop for online orders by switching off its calendar_{shop_id} rows |
| ONLINE, ACTIVE, OPEN, AVAILABLE, UNPAUSED, OFF | Reopen the rows closed by PAUSED and revert a previously added busy delay |
Rules:
| Rule | Details |
| Delay required | Only BUSY needs a positive delay; PAUSED carries none |
| Existing delay | Existing busy delay is deducted before applying a new one |
| Max value | `min_timeframe` is capped at `99` |
| Rows closed | Event types the opening-hours sync manages: 1 business, 2 pickup, 3 delivery |
| Scope of the pause | Every enabled row, not just today's, PAUSED has no end time and would otherwise lapse at midnight |
| Storage | deliverect.busy.mode.added_delay (delay), deliverect.busy.mode.paused (flag), deliverect.busy.mode.paused_calendar (JSON array of closed row IDs) in shop_settings. The flag is stored separately, so a shop with nothing currently open still remembers it is paused |
| Overlay over opening hours | A pause outranks the published hours until ONLINE. It is reapplied after an opening-hours sync and after a menu publish, including the menu schedule. On those paths the recorded IDs are replaced with exactly what the new schedule left open (empty list included), so ONLINE uncovers the latest hours; a plain repeated PAUSED unions them |
| Locking | SELECT ... FOR UPDATE during the update, plus the per-shop availability lock shared with menu_update, snooze_unsnooze and opening_hours, since all four write the same calendar rows |
| Failure handling | A pause that could not be applied or reapplied answers HTTP 500 so Deliverect retries; a lock timeout does the same. Settings that exist but cannot be parsed are reported as failures instead of being read as "not paused" |
| Response | busy_mode answers raw {"status": "<state>"} rather than the usual wrapper. The Channel API OpenAPI only requires a 200, so this mirrors the documented example rather than a hard requirement |
Opening hours sync goes from Deliverect to OrderLemon.
Rules:
| Rule | Details |
| Direction | Deliverect -> OrderLemon |
| Missing weekday | Treated as closed |
| Overwrite | Weekly Deliverect availability overwrites existing OL weekly schedule |
| Max slots | Max 2 slots per day |
| Slot update order | Existing slots update by `id ASC` |
| Source of truth | `availabilities` is the source of truth |
| Holiday hours | Not supported because Channel API payload has no holiday/special-hours context |
| Busy-mode pause | An active PAUSED is reapplied after every sync, and the recorded row IDs are replaced with what the new schedule left open. Without it a sync would reopen the shop mid-pause |
| Locking | Takes the same per-shop availability lock as busy_mode, menu_update and snooze_unsnooze |
Manual OL -> Deliverect opening hours sync exists as `sync_opening_hours`, but automatic sync is blocked because OrderLemon has no webhook event for opening-hour changes.
Channel registration stores Deliverect channel link/location mapping and returns event-specific webhook URLs
Returned URLs include:
| URL key | Event |
| statusUpdateURL | order_status_update |
| menuUpdateURL | menu_update |
| snoozeUnsnoozeURL | snooze_unsnooze |
| busyModeURL | busy_mode |
| File | Responsibility |
| v1/general/deliverect_functions.php | Deliverect Channel API communication, OAuth2, order sending |
| v1/general/deliverect_catalog_functions.php | Catalog and menu sync helpers |
| v1/general/deliverect_sync_functions.php | Sync utilities, mapping, relation logic |
| v1/general/deliverect_order_functions.php | Order transformation and status mapping |
| v1/general/ms_functions.php | Main microservice logic |
| v1/webhooks/deliverect.php | Main webhook router |
| v1/webhooks/deliverect/*.php | Event-specific webhook handlers |
| v1/POST/send_order_to_deliverect.php | Direct order send endpoint |
| Environment | URL |
| DEV | https://deliverect.ms.orderlemon.dev/v1/ |
| PROD | https://deliverect.ms.orderlemon.com/v1/ |
| Webhook | https://deliverect.ms.orderlemon.{dev|com}/v1/webhooks/deliverect |
Automatic OrderLemon to Deliverect opening-hours sync is blocked by missing OrderLemon webhook events.
| Constraint | Status |
| Holiday hours | Not supported by Deliverect Channel API weekly availability payload |
| OL -> Deliverect automatic opening-hours sync | Blocked by missing OrderLemon webhook event |