# Order Lifecycle

Understand the complete order flow from validation through fulfillment.

## Order Flow

Every order goes through two API steps: **validation** and **placement**.

<Mermaid chart={`graph LR
    A[Build Cart] --> B[Validate Order]
    B --> C{Errors?}
    C -->|Yes| A
    C -->|No| D[Place Order]
    D --> E[Track Status]`} />

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

:::warning[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.
:::

```jsonc
{
  "cart": { ... },
  "fulfillment": { ... },
  "client": {
    "ip": "203.0.113.7",                                    // the customer's address, not your server's
    "userAgent": "Mozilla/5.0 (X11; Linux x86_64) Chrome/128.0 Safari/537.36"
  }
}
```

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`](/errors/CLIENT_CONTEXT_REQUIRED) | `client` absent, or `ip`/`userAgent` missing or blank |
| [`CLIENT_IP_INVALID`](/errors/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`](/errors/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](/distribution-partners/marketfront-api/conventions).

## 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 `Order` object, including a computed `amounts` breakdown and an `errors[]` array.
- The read-only boolean `isValid` on the order tells you whether placement is safe to proceed. When `isValid: false`, `errors[]` is non-empty and describes what is wrong. When `isValid: true` and `errors` is empty, the order passed all checks at that moment.
- Because validate is read-only it is naturally idempotent — no `Idempotency-Key` header 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+json` `ProblemDetail`. The `400` response covers payment declines, store rejections, and missing idempotency keys; the top-level `errorCode` field (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 | 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 |

:::tip{title="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](#error-codes) below |
| `message` | string | Curated, user-facing description |
| `pointer` | string | [RFC 6901](https://www.rfc-editor.org/rfc/rfc6901) JSON Pointer to the offending element, e.g. `/cart/lineItems/0` |

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.

### 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` | Transient payment processing failure | Retry place with the same idempotency key |
| `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` | Order total is below the store's minimum | Add more items |
| `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 |

## Order State & Status

Orders use two fields: **state** (lifecycle position) and **status** (granular progress).

### State

<Mermaid chart={`stateDiagram-v2
    [*] --> open
    open --> completed
    open --> cancelled`} />

### Status

Within the `open` state, status tracks progress:

<Mermaid chart={`stateDiagram-v2
    [*] --> pending
    pending --> confirmed
    confirmed --> preparing
    preparing --> ready
    ready --> inTransit: Delivery orders
    ready --> [*]: Pickup (completed)
    inTransit --> [*]: completed`} />

For complete details on state values, status values, and error codes, see the **[API Reference](/api/marketfront)**.

## 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**

```json
{
  "fulfillment": {
    "mode": "PICKUP",
    "scheduleType": "ASAP"
  }
}
```

**DELIVERY_BY_MERCHANT — scheduled**

```json
{
  "fulfillment": {
    "mode": "DELIVERY_BY_MERCHANT",
    "scheduleType": "SCHEDULED",
    "scheduledTime": "2025-09-15T19:30:00Z",
    "address": {
      "address1": "123 Main St",
      "address2": "Apt 4B",
      "city": "New York",
      "state": "NY",
      "postalCode": "10001"
    },
    "deliveryType": "DOOR_TO_DOOR",
    "instructions": "Leave at the front desk"
  }
}
```

:::note[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`).

- **Generate a fresh UUID v4 per distinct order** and include it on every retry of that order.
- A replay with the same key returns the original response (24h TTL) — safe to retry on network errors and 5xx without risking a duplicate charge.
- **Do not reuse a key with a different payload.** Today the cached response is returned regardless of body; a future release will reject same-key/different-body with `422`. Keep keys 1:1 with payloads now to avoid surprises.

`validate` is naturally idempotent (read-only pricing) and does not use the header.

## Related

- **[Validate Order](/api/marketfront#validateOrder)** — Check availability and pricing
- **[Place Order](/api/marketfront#placeOrder)** — Submit order for fulfillment
- [Payments](/distribution-partners/shared-guides/payments) — Payment options and Card-on-File setup
- [Webhooks](/distribution-partners/shared-guides/webhooks) — Signature verification and retry policy for order status webhooks
