Order sandbox
The order sandbox gives your app a throwaway store to place test orders against, so you can exercise the order round trip before a merchant ever installs you.
It lives in the console, under My Apps → your app → Sandbox.
The store belongs to Quiqqy, not to a merchant. Nothing you do in it creates a merchant account, sends a merchant an email, charges anything, or appears in anyone’s reporting. An order placed here is a test order because of where it lives, not because of a flag — which is why there is no way to accidentally place one against a real store.
The stores you get
Section titled “The stores you get”The first time you open the tab, Quiqqy creates a sandbox store for your app and seeds it with a fixed menu. Reopening the tab reuses it rather than piling up stores.
You can add more. A POS integration routes orders by location, and the only
way to prove yours routes them to the right one is to have more than one to
route between. Each store arrives at your endpoint with its own location_id:
| Store | location_id your app receives |
|---|---|
| The first one | quiqqy-sandbox |
| “Second Kitchen” | quiqqy-sandbox-second-kitchen |
Reset store restores the selected store’s menu to its seeded state. Use it after you have 86’d things or want a clean run; it does not delete past orders.
The menu you get
Section titled “The menu you get”The menu is not meant to look like a real restaurant. It contains one example of every shape your parser has to survive:
| Item | Key | What it is there to break |
|---|---|---|
| Cup Cake | SBX-ITEM-PLAIN | A bare line — no variants, no modifiers |
| Arabian Pulpy | SBX-ITEM-VARIANTS | Three sizes, each its own sellable SKU |
| Chicken Shawarma | SBX-ITEM-MODIFIERS | A required pick-one group and an optional multi-select |
| Bottled Water | SBX-ITEM-EXEMPT | Tax-exempt — a zero-tax line inside a taxed order |
| Baklava | SBX-ITEM-OOS | Already unavailable, so you can exercise 86 handling on day one |
The two modifier groups are SBX-GRP-BREAD (required, choose exactly one) and
SBX-GRP-EXTRAS (optional, up to three).
Choosing where the order comes from
Section titled “Choosing where the order comes from”Quiqqy is an aggregator of aggregators: an order’s source names the
marketplace it actually came from, and source_account_id names the connector
that delivered it. The sandbox lets you pick that marketplace — and it is
explicit about what picking one really does.
Marketplaces Quiqqy really ingests
Section titled “Marketplaces Quiqqy really ingests”Talabat, Deliveroo, Careem, Noon, HungerStation, Jahez, Keeta, foodpanda and GrubTech direct all reach Quiqqy through the GrubTech connector.
Choosing one of these builds the webhook body GrubTech itself would have sent and runs it through the same ingest code a live order takes. The origin mapping, the minor-unit arithmetic and the catalogue resolution all really execute. A bug in any of them surfaces here rather than on a merchant’s first live order.
Marketplaces with no connector yet
Section titled “Marketplaces with no connector yet”Zomato, Swiggy, Uber Eats and DoorDash have no Quiqqy connector today. The sandbox can still stamp an order with them, and your webhook, your origin badge and your whole round trip are exercised — but the order is synthesised, not ingested.
The control case
Section titled “The control case”Quiqqy storefront (quiqqy_community) is an order with no marketplace in
the middle: full customer details, and the only origin that can carry a dine-in
order. Use it to isolate whether a bug is aggregator-specific.
What each marketplace does differently
Section titled “What each marketplace does differently”Picking a marketplace narrows the form to what it actually supports, and the console lists its quirks. These are the details that break integrations:
- No marketplace passes a usable customer email. Quiqqy synthesises a
no-replyaddress. Never key on it. - Phone numbers are relayed, not real, wherever the marketplace passes one.
- Money crosses in minor units, and KWD, BHD, OMR and JOD have three decimals rather than two. Pick a three-decimal currency in the sandbox — this is the single most expensive thing to get wrong, because it is silently off by a factor of ten.
- Dine-in never arrives from a marketplace. It belongs to Quiqqy’s own storefront and its table QR.
Placing an order and driving it
Section titled “Placing an order and driving it”-
Pick an origin, then build a cart. Required modifier groups are preselected, so the order is always one a kitchen could make.
-
Press Place test order. Quiqqy creates the order and fires
order.createdat your callback URL — through the production delivery path, signed with your real secret and retried on the normal ladder. -
Your app calls back the way it would for a real order:
POST /pos/orders/{order_id}/accept, then/status,/ready,/cancel. See Orders for the lifecycle. -
Watch both halves land in the inspector.
Courier callbacks
Section titled “Courier callbacks”ready is as far as Quiqqy moves a delivery order it did not deliver itself. A
sandbox order ingested through the GrubTech connector is
DELIVERY_BY_FOOD_AGGREGATOR — the marketplace’s own rider collects, and from
ready onward the order is the marketplace’s to move, not Quiqqy’s. In
production the marketplace calls back as its rider works; in the sandbox, the
Marketplace courier panel lets you fire those callbacks yourself, on your
own schedule, instead of waiting on one. Pickup and dine-in orders have no
courier leg, and the panel says so instead of appearing.
A freshly placed order sits at pending or confirmed, and dispatched
needs ready — drive it there first (step 3 above) before firing courier
events, or they will come back skipped.
Your POS integration has to handle out_for_delivery and delivered arriving
with no action of its own — they are pushed to you, not requested by you.
If your handler assumes a status only ever changes after a call it made, this
panel is where that assumption breaks safely, before a real order finds it.
Eight events, three that reach your webhook
Section titled “Eight events, three that reach your webhook”| Event | Effect on the order | You receive |
|---|---|---|
dispatched | ready → out_for_delivery | order.updated |
completed | → delivered (legal from ready or out_for_delivery) | order.delivered |
cancelled | → cancelled | order.cancelled |
deliveryJob.created | none | nothing |
deliveryJob.driverAssigned | none | nothing |
deliveryJob.started | none | nothing |
deliveryJob.completed | none | nothing |
deliveryJob.cancelled | none | nothing |
The five deliveryJob.* events are GrubTech’s rider-tracking callbacks — a job
created, a driver assigned, the job started, the job completed or cancelled.
Quiqqy acknowledges and logs each one and does nothing else: no order status
change, and no webhook to your app. Build your integration so it never waits on
one and never keys logic off one — out_for_delivery and delivered are the
signals that matter, and a marketplace that sends the rider events late, out of
order, or not at all is not something you need to handle differently.
A deliberate fault comes back 200
Section titled “A deliberate fault comes back 200”Firing an event the order’s current state cannot accept is not an error. It
returns 200 with outcome: "skipped" and a reason:
reason | Means |
|---|---|
illegal_transition | The order’s state machine refused the move — e.g. dispatched fired again after the order already reached delivered. |
unknown_order | No order matched the marketplace’s order number. |
no_change | The event was a replay — the order was already in that state. |
outcome is one of applied, skipped, acknowledged (the rider events,
above) or rejected. Only rejected means something actually went wrong — a
transport or unexpected failure. skipped is the deliberate fault landing
exactly as designed: firing dispatched on an order that already reached
delivered and getting back skipped: illegal_transition is a successful
test of your handler’s idempotency, not a broken sandbox. Treat it the way your
production webhook handler should treat a stray or out-of-order callback — as a
normal outcome, not a failure.
What a green run does not prove
Section titled “What a green run does not prove”The panel drives the same production code a real marketplace callback lands
on — GrubtechOrderService.statusFromWebhook for a connector-backed
marketplace, OrdersService.updateStatusInternal (with the aggregator echo
suppressed) for one Quiqqy has no connector to yet — so the ingest and
status-transition logic really executes.
What it skips is getting there the way GrubTech does: over HTTP, against
Quiqqy’s own inbound endpoint. The sandbox enters at the service layer, so
GrubtechApiKeyGuard — the guard that authenticates and tenant-resolves a real
inbound GrubTech callback — and the webhook-confirm interceptor behind it are
never exercised by a sandbox run. A green run here proves your webhook fires
and your order’s status machine behaves correctly. It proves nothing about the
credential a marketplace presents to Quiqqy, or about the wire itself.
Reading the round trip
Section titled “Reading the round trip”The inspector shows two lanes about a shared spine. Quiqqy’s legs — the ingest and the outbound webhooks — sit on the left. Your app’s calls sit on the right. Which half broke is meant to be obvious without reading every row.
Each leg carries its status, its latency and its payload. Outbound legs also carry the delivery’s attempt count, so a webhook that only landed on the third try says so.
Three numbers sit above the timeline:
- Legs — how many steps the run has produced so far.
- Slowest — where a timeout will show up first.
- Webhooks — whether every delivery actually landed. This is the most common reason to open this screen.
A rejected call of yours is not hidden. A 422 from /status with an invalid
value is one of the more useful things the timeline can show you, because it
names what Quiqqy would not accept.
What the sandbox does not do
Section titled “What the sandbox does not do”Being clear about the edges, so you do not certify on a false positive:
- It does not replace onboarding a store you own. A reviewer replays the real consent and onboarding flow, so you still have to exercise it — see Testing your integration.
- It does not compute tax. Lines carry zero tax. Totals are internally consistent, but if you reconcile tax, test that elsewhere.
- It does not prove a real marketplace will call back. Courier callbacks let you replay what GrubTech’s own dispatch, delivery and cancellation callbacks look like, on demand, proving your handler survives them and their faults. Whether the real Talabat, on a real delivery, actually sends them — and when — is between them and Quiqqy.
- It does not cover combos, as above.
Everything the console does is available directly, under your normal developer session:
| Method | Path |
|---|---|
GET | /developer/apps/{app_id}/sandbox/aggregators |
GET | /developer/apps/{app_id}/sandbox/catalogue |
GET | /developer/apps/{app_id}/sandbox/stores |
POST | /developer/apps/{app_id}/sandbox/stores |
DELETE | /developer/apps/{app_id}/sandbox/stores/{store_id} |
POST | /developer/apps/{app_id}/sandbox/sessions |
POST | /developer/apps/{app_id}/sandbox/sessions/{session_id}/order |
GET | /developer/apps/{app_id}/sandbox/sessions/{session_id}/timeline |
POST | /developer/apps/{app_id}/sandbox/sessions/{session_id}/courier |
POST | /developer/apps/{app_id}/sandbox/sessions/{session_id}/courier/scenario |
POST | /developer/apps/{app_id}/sandbox/store/reset |
Opening a session takes aggregator_key, and optionally store_id to pick one
of your sandbox stores — omit it for the first one. There is deliberately no way
to ask for an injection mode: the marketplace decides whether the order is
really ingested or synthesised, so no caller can claim a Zomato order took the
native path.
{ "order_type": "delivery", "currency": "KWD", "payment_mode": "prepaid", "items": [ { "external_id": "SBX-ITEM-MODIFIERS", "quantity": 2, "modifiers": [ { "group": "SBX-GRP-BREAD", "addon": "SBX-ADD-SAJ" }, { "group": "SBX-GRP-EXTRAS", "addon": "SBX-ADD-GARLIC" } ] } ]}The response tells you which path the order actually took:
{ "session_id": "019fd2f0-…", "order_id": "019fd3a0-…", "source_order_id": "TLB-0000042", "injection_mode": "native_grubtech"}A session lasts an hour. Placing again on the same session reuses its marketplace order number rather than minting a second one, so a retry does not look like a new order.
Courier callbacks fire the same way, scoped to the session’s order:
{ "event": "dispatched" }{ "event": "dispatched", "outcome": "applied", "reason": null, "previous_status": "ready", "order_status": "out_for_delivery"}courier/scenario takes { "scenario": "happy_path" | "late_cancel" | "out_of_order" | "rider_stalls" } and fires that preset sequence server-side,
returning { "data": CourierResult[] } — one result per event it fired, in
order.