Order Lifecycle
Understand the complete order flow from validation through fulfillment.
Order Flow
Every order goes through two API steps: validation and placement.
Reading the diagram
"Errors?" means isValid: false on the HTTP 200 validateOrder response — business/order problems never produce a 4xx from validate.
End-user client context
Required on every order request
Both order calls require a client object carrying the end user's originating IP and user agent. Requests without it are rejected with a 400 ProblemDetail (errorCode: CLIENT_CONTEXT_REQUIRED).
This is a breaking change for existing integrations. The enforcement date is being communicated to each live partner directly; if you have not received one, contact your Gett integration lead before shipping.
Code
Your order requests reach Gett server-to-server, so the connection we see belongs to your backend rather than to the person ordering. The commerce partners we forward orders to run anti-fraud checks that need the real customer's signals, so you have to forward them from the request your own front end received — the same request that produced the cart.
We reject rather than substitute a placeholder: a synthetic address forwarded to a partner counts against the integration in the traffic reviews they run, and it would do so silently.
Three named errors cover the field:
errorCode | Cause |
|---|---|
CLIENT_CONTEXT_REQUIRED | client absent, or ip/userAgent missing or blank |
CLIENT_IP_INVALID | ip unparseable, or in a range no end user can originate from — private, loopback, link-local, CGNAT, reserved |
CLIENT_USER_AGENT_INVALID | userAgent longer than 512 characters |
If your front end sits behind a proxy or CDN, client.ip is the first address in the forwarded-for chain, not your own server's. client is declared on OrderCore, so it applies to validate and place alike — see Schema inheritance.
Validation vs Placement
The API uses a two-step model: a soft pre-check (validateOrder) followed by the committing transaction (placeOrder). Understanding when each returns errors — and in what shape — is central to a correct integration.
How each call behaves
POST /v1/marketfront/orders/validate is a dry run. It computes real server-side pricing and checks availability without writing anything. Think of it like Square's CalculateOrder or Stripe's requirements.errors — a preview you can show to the user before they commit.
- Always returns HTTP 200 (or 401 for auth failures). There is no 4xx for business/order problems.
- On 200 the body is the full
Orderobject with anerrors[]array. When the order was priced, it also carries a computedamountsbreakdown; when validate refuses,amountsisnullrather than an echo of what you sent. A refusal means we did not price the order, and a zeroed or echoed block would be indistinguishable on the wire from a successful validate that priced the cart at nothing. - The read-only boolean
isValidon the order tells you whether placement is safe to proceed. WhenisValid: false,errors[]is non-empty and describes what is wrong. WhenisValid: trueanderrorsis empty, the order passed all checks at that moment. - Because validate is read-only it is naturally idempotent — no
Idempotency-Keyheader is required.
POST /v1/marketfront/orders/place is the committing call. It re-validates, charges the customer, and submits the order to the restaurant.
- On success it returns 2xx with the authoritative post-placement
Order. - On failure it returns 4xx as an RFC 9457
application/problem+jsonProblemDetail. The400response covers payment declines, store rejections, and missing idempotency keys; the top-levelerrorCodefield (e.g.PAYMENT_DECLINED,MISSING_IDEMPOTENCY_KEY) names the overall failure reason.
Decision table
| Situation | Endpoint | HTTP | Key fields | Partner action |
|---|---|---|---|---|
| Validate clean | validateOrder | 200 | isValid: true, errors: [], amounts populated | Show pricing preview; proceed to place |
| Validate with problems | validateOrder | 200 | isValid: false, errors[] non-empty, amounts: null | Surface errors to user; do not call place |
Unknown errors[].code value | validateOrder | 200 | unrecognized code in errors[] | Treat as OTHER; show a generic message |
| Place success | placeOrder | 2xx | Order with final amounts | Confirm order to user; start tracking |
| Place hard failure | placeOrder | 4xx | ProblemDetail + top-level errorCode + errors[] | Handle per errorCode; retry with same idempotency key only for PAYMENT_FAILED |
| Malformed request | Either | 400 / 422 | ProblemDetail | Fix request body before retrying |
A clean validate does not guarantee place succeeds
Stock levels, prices, and payment authorization can change in the window between the two calls. Always inspect errors even on a 200 validate response, and always be prepared for placeOrder to return a 4xx even after a successful validate.
Unified OrderError shape
OrderError is the same object on both paths — order.errors[] on a validate 200 and ProblemDetail.errors[] on a place 4xx. You only need one error-rendering path in your client:
| Field | Type | Description |
|---|---|---|
code | string (open enum) | Machine-readable reason — see Error codes below |
message | string | Curated, user-facing description |
pointer | string | null | RFC 6901 JSON Pointer to the offending element, e.g. /cart/lineItems/0. null when the failure comes back from a commerce partner that did not tell us which element caused it |
Note the distinction between OrderError.code (per-element, in errors[]) and the top-level ProblemDetail.errorCode (single string naming the overall place failure). Both can appear on a 4xx place response but they serve different purposes.
pointer is now RFC 6901 everywhere — check your parsing
A small number of pointer values previously shipped as dotted, unrooted paths — amounts.total,
fulfillment.address, paymentId, environment — which are not JSON Pointers and which a conformant
RFC 6901 library rejects. Every emitted pointer is now the rooted, slash-separated form:
/amounts/total, /fulfillment/address, /paymentId, /environment.
This brings the wire into line with what this page and all five OpenAPI documents have always
described, so a client built against the published contract is unaffected. If you special-cased the
dotted values, or split on ., remove that handling. Resolving pointer with any standard JSON
Pointer library is now correct for every value we emit.
Error codes
errors[].code is an open enum (x-extensible-enum). The list below is the current set, but it is not exhaustive — Gett may add new codes at any time as new failure modes are identified. Following the Zalando API extension convention: clients must provide default/fallback behaviour for unknown values (route to OTHER) and must not hard-fail or throw on an unrecognized code. Do not deserialize into a closed/strict enum type.
| Code | Meaning | Suggested action |
|---|---|---|
ITEM_UNAVAILABLE | An item in the cart is no longer available | Remove the item and re-validate |
PAYMENT_FAILED | The payment was declined and the processor did not tell us why | Ask the user for a different payment method |
STORE_CLOSED | Store is not accepting orders right now | Re-discover stores or try a scheduled order |
DELIVERY_UNAVAILABLE | Delivery is not available for this order | Offer pickup or a different store |
OUTSIDE_AVAILABILITY_WINDOW | Requested scheduled time is outside the store's hours | Adjust scheduledTime or switch to ASAP |
MINIMUM_NOT_MET | The whole order is below the store's order minimum | Add more items. Contrast MODIFIER_REQUIRED, which is about one item |
MODIFIER_REQUIRED | One item is missing a choice its menu requires (a modifier group below its minimumAllowed) | Resolve pointer to the line item and have the user pick the required option. Adding more items does not help |
ORDER_TOTAL_DIFFERENT | Total changed since the order was last validated | Re-validate to get the updated amounts |
OTHER | Catch-all for any reason not covered above, and the fallback for unrecognized codes | Show a generic error message |
INSUFFICIENT_FUNDS | The card was declined for insufficient funds | Ask the user for a different payment method |
INCORRECT_CVC | The card was declined because the security code did not match | Let the user re-enter the security code and retry the same card |
CARD_EXPIRED | The card has expired, or expired while stored | Collect a new card |
CARD_LOST_OR_STOLEN | The card cannot be used | Ask the user for a different payment method. Show a generic decline message — never tell a cardholder their card is flagged |
PROCESSING_ERROR | A temporary fault at the processor, not a decision about the card | Retrying later with the same card may succeed |
The last five refine PAYMENT_FAILED rather than replacing it: you will still receive PAYMENT_FAILED
whenever a payment is declined without an attributable reason. PROCESSING_ERROR is the only member of
this group for which retrying the same card is sensible advice — the others are decisions about the card,
not transient faults.
Order State & Status
Orders use two fields: state (lifecycle position) and status (granular progress).
State
Status
Within the open state, status tracks progress:
For complete details on state values, status values, and error codes, see the API Reference.
Fulfillment
Every order carries a fulfillment object that is a discriminated union on the mode property. The two variants are PICKUP and DELIVERY_BY_MERCHANT.
Common fields (all variants)
| Field | Required | Description |
|---|---|---|
mode | Yes | Discriminator: PICKUP or DELIVERY_BY_MERCHANT |
scheduleType | Yes | ASAP or SCHEDULED |
scheduledTime | Conditional | UTC ISO-8601 date-time. Required when scheduleType is SCHEDULED; must be null (or omitted) when ASAP. |
Delivery-only fields (DELIVERY_BY_MERCHANT)
| Field | Required | Description |
|---|---|---|
address | Yes | Delivery destination. On a request, provide either an addressId reference or inline street fields. On a response, the fully resolved and geocoded address is returned. |
deliveryType | Yes | DOOR_TO_DOOR or LEAVE_AT_DOOR |
instructions | No | Free-text delivery instructions, max 500 characters |
Examples
PICKUP — ASAP
Code
DELIVERY_BY_MERCHANT — scheduled
Code
Fulfillment vs store discovery
The fulfillment object on an order (discriminated union above) is distinct from the fulfillmentType flat enum (PICKUP | DELIVERY_BY_MERCHANT) used on DiscoverStoresRequest to filter search results. They share the same values but serve different purposes — one describes how to fulfill a specific order, the other filters which stores to surface.
Idempotency
POST /v1/marketfront/orders/place requires an Idempotency-Key header. Calls without it are rejected with a 400 ProblemDetail (errorCode: MISSING_IDEMPOTENCY_KEY).
One rule: a key identifies an order attempt, not a retry
If you received a response — success or failure — that attempt is finished. A new attempt needs a new key.
If you received no response, the outcome is unknown. Replay the same key to find out what happened before you decide anything.
Nearly every case below follows from that one test — did I get a response? — so you rarely have to
branch on which error you got. There is exactly one exception, and it is explicit about itself:
409 IDEMPOTENCY_KEY_IN_PROGRESS means the attempt is not finished, so the first half of the
rule does not apply. It carries retryable: true; every other terminal response does not.
- Generate a fresh UUID v4 per order attempt.
- No response — timeout, dropped connection,
5xx. Replay the same key. If the order was in fact placed, you get the original response back instead of placing a second one. This is the case idempotency exists for, and using a fresh key here is what causes duplicate orders. The original response comes back even if the store's menu or status, or the payment method, has changed since that attempt: the key is checked before any of them. - A response arrived, and it failed (HTTP
200witherrors[]). That attempt is complete and the outcome is cached for 24h, so replaying the same key returns the identical failure without re-contacting the processor — it can never become a success. A genuine re-attempt needs a fresh key. SeePROCESSING_ERRORfor the one failure where re-attempting is worthwhile, and for the charge risk that comes with it. - A response arrived saying the attempt is still running (
409,errorCode: IDEMPOTENCY_KEY_IN_PROGRESS,errorCategory: idempotency,retryable: true). An earlier request on this key is still in flight — most often your own client retried before the first call returned. Retry the same key, unchanged, until you get a terminal answer. This is the one response where reusing the key is correct: the in-flight attempt owns your order's outcome, and a fresh key here presents the same order to us as a new one. The response carries aRetry-Afterheader giving the number of seconds to wait before that retry — honour it rather than retrying immediately, or you will simply queue behind the same in-flight attempt and collect another409. - Do not reuse a key with a different payload. Same-key/different-body is rejected with
422(errorCode: IDEMPOTENCY_KEY_CONFLICT,errorCategory: idempotency). The fingerprint is a hash of the canonicalized body, so key order and whitespace do not matter — any changed value does. A key is also scoped to the partner and end user that placed the order. The same key sent by a different partner or end user does not get the original response: it is refused with the same422, or earlier with403if its payment type is not allowed for that caller.
Telling a replay from a fresh attempt
A replayed response carries Idempotent-Replayed: true. Absence means the request executed; we
never send false, so test for the header's presence.
This matters most on the case you cannot otherwise distinguish. A payment failure arrives as an HTTP
200 with isValid: false and a populated errors[] — and a replayed failure is byte-identical
to a fresh one. Without this header, a client that retried after a timeout has no way to tell whether
we re-contacted the processor or handed back the answer we already had.
Code
Concurrent duplicates are held off rather than replayed: a request arriving while an earlier one on
the same key is still in flight receives 409 IDEMPOTENCY_KEY_IN_PROGRESS, which is retryable on
that same key and carries a Retry-After header saying when. The conflict is not recorded
against the key, so the in-flight attempt's real outcome is still yours to collect on the retry.
What idempotency cannot protect
These guarantees are about our record of the order. If a payment processor times out, the
authorization may have succeeded even though the order came back failed — a fresh-key re-attempt can
then charge the customer twice, and no Idempotency-Key we hold can prevent that. Reconcile before
re-attempting when you can, and treat a customer-reported charge against an order you never saw
succeed as this case.
validate is naturally idempotent (read-only pricing) and does not use the header.
Related
- Validate Order — Check availability and pricing
- Place Order — Submit order for fulfillment
- Payments — Payment options and Card-on-File setup
- Webhooks — Signature verification and retry policy for order status webhooks