Skip to content

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 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:

Storelocation_id your app receives
The first onequiqqy-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 is not meant to look like a real restaurant. It contains one example of every shape your parser has to survive:

ItemKeyWhat it is there to break
Cup CakeSBX-ITEM-PLAINA bare line — no variants, no modifiers
Arabian PulpySBX-ITEM-VARIANTSThree sizes, each its own sellable SKU
Chicken ShawarmaSBX-ITEM-MODIFIERSA required pick-one group and an optional multi-select
Bottled WaterSBX-ITEM-EXEMPTTax-exempt — a zero-tax line inside a taxed order
BaklavaSBX-ITEM-OOSAlready 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).

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.

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.

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.

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.

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-reply address. 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.
  1. Pick an origin, then build a cart. Required modifier groups are preselected, so the order is always one a kitchen could make.

  2. Press Place test order. Quiqqy creates the order and fires order.created at your callback URL — through the production delivery path, signed with your real secret and retried on the normal ladder.

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

  4. Watch both halves land in the inspector.

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”
EventEffect on the orderYou receive
dispatchedready → out_for_deliveryorder.updated
completed→ delivered (legal from ready or out_for_delivery)order.delivered
cancelled→ cancelledorder.cancelled
deliveryJob.creatednonenothing
deliveryJob.driverAssignednonenothing
deliveryJob.startednonenothing
deliveryJob.completednonenothing
deliveryJob.cancellednonenothing

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.

Firing an event the order’s current state cannot accept is not an error. It returns 200 with outcome: "skipped" and a reason:

reasonMeans
illegal_transitionThe order’s state machine refused the move — e.g. dispatched fired again after the order already reached delivered.
unknown_orderNo order matched the marketplace’s order number.
no_changeThe 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.

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.

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.

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:

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

POST /developer/apps/{app_id}/sandbox/sessions/{session_id}/order
{
"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:

POST /developer/apps/{app_id}/sandbox/sessions/{session_id}/courier
{ "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.