> ## Documentation Index
> Fetch the complete documentation index at: https://firespark.cloud/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Validate order

> Validate cart lines and totals against a fresh menu for the order scope before checkout.

Recomputes expected prices from a **fresh menu** for the same selling scope and compares them to the submitted `lines` and `totals`. Use this before checkout so the channel can show per-line problems (price changes, unavailable products, closed menu, and similar) without trusting client-side money.

> [!NOTE]
>
> Requires a Storefront API access token from [Token
> exchange](https://firespark.cloud/docs/storefront-api/oauth/exchange/post.md). Send the **current draft**
> (scope + lines + totals), not only a persisted order id — unsynced client
> edits are validated too.

In `@fire-spark/storefront`, call `order.validateOrder()`. On failure it throws
`OrderValidationError` with the same `issues` and `lines` payload described
below.

## Request body

| Field | Required | Type | Description |
| - | - | - | - |
| `store_id` | Yes | string | External store id (scope) |
| `channel_id` | Yes | string | External channel id (scope) |
| `fulfillment_id` | Yes | string | External fulfillment id (scope) |
| `brand_id` | No | string \| null | External brand id when the order is brand-scoped |
| `lines` | Yes | array | Current cart `OrderLine` rows (including modifier children) |
| `totals` | Yes | object | Current order totals (`currency`, `subtotal`, `discount_*`, `tax_*`, `total`) |

## Request

```bash theme={null}
curl -X POST "https://firespark.cloud/api/storefront/v1/orders/validate" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "downtown",
    "channel_id": "app",
    "fulfillment_id": "delivery",
    "brand_id": null,
    "lines": [
      {
        "type": "PRODUCT",
        "uid": "line-root-1",
        "id": "meal-combo",
        "parent_uid": null,
        "parent_id": null,
        "name": "Meal combo",
        "quantity": 1,
        "tax_rate": 0.15,
        "discount_rate": 0,
        "unit_subtotal": 10.99,
        "unit_discount": 0,
        "unit_tax_total": 1.6485,
        "unit_total": 12.6385,
        "subtotal": 10.99,
        "discount_total": 0,
        "tax_total": 1.6485,
        "total": 12.6385,
        "unit_group_subtotal": 12.49,
        "unit_group_discount": 0,
        "unit_group_tax_total": 1.8735,
        "unit_group_total": 14.3635,
        "group_subtotal": 12.49,
        "group_discount": 0,
        "group_tax_total": 1.8735,
        "group_total": 14.3635,
        "metadata": {},
        "bundled_items": []
      }
    ],
    "totals": {
      "currency": "USD",
      "subtotal": 12.49,
      "discount_rate": 0,
      "discount_total": 0,
      "tax_rate": 0.15,
      "tax_total": 1.8735,
      "total": 14.3635
    }
  }'
```

## Success response

Returns `200` when every check passes.

```json theme={null}
{
  "data": {
    "ok": true
  }
}
```

## Validation failure (`422`)

When at least one check fails, the API returns `422` with:

```json theme={null}
{
  "error": "Order validation failed",
  "details": {
    "issues": [
      { "code": "MENU_CLOSED" },
      { "code": "SCOPE_INACTIVE", "field": "store_id" }
    ],
    "lines": {
      "line-root-1": [
        {
          "code": "PRICE_MISMATCH",
          "field": "unit_subtotal",
          "expected": "10.99",
          "actual": "9.99"
        }
      ]
    }
  }
}
```

Money comparisons use exact Big.js equality (no epsilon). `expected` / `actual` are decimal strings when present.

### Order-level issue codes (`details.issues`)

| Code | Meaning |
| - | - |
| `NO_MENU` | No ACTIVE menu resolved for the scope (or none after schedule selection). |
| `MENU_CLOSED` | A menu was resolved but its schedule is not active now. |
| `SCOPE_INACTIVE` | Store, channel, fulfillment, and/or brand is missing or not `ACTIVE`. `field` is `store_id`, `channel_id`, `fulfillment_id`, or `brand_id` (one issue per inactive part). |
| `EMPTY_ORDER` | There are no root lines (`parent_uid == null`). |
| `TOTALS_MISMATCH` | Submitted `totals` do not match the sum of root line group totals. `field` names the totals key. |

### Redeem issue codes

These come in `details.issues` for order redeems (`field` is `redeem.N.…`) and in `details.lines[uid]` for line redeems.

| Code | Meaning |
| - | - |
| `REDEEM_PAYMENT_REQUIRED` | The redeem has no `payment_id` (`field` ends in `payment_id`) or no `amount` above 0 (`field` ends in `amount`). |
| `REDEEM_PAYMENT_METHOD_NOT_CONFIGURED` | `payment_id` is not an active `REDEMPTION` payment of the merchant. `actual` is `NOT_FOUND` (missing or not active) or `NOT_REDEMPTION`. |
| `REDEEM_AMOUNT_MISMATCH` | `amount` is above its cap: the pre-tax value of the line or order with `before_taxes` (default), else the full total, and never above total minus tip and delivery. `expected` is the cap. |
| `REDEEM_EXPIRED` | `expires_at` has passed. |

### Line-level issue codes (`details.lines[uid]`)

| Code | Meaning |
| - | - |
| `PRODUCT_NOT_FOUND` | Root product id is not on the fresh menu. |
| `HIDDEN` | Product `availability.status` is `HIDDEN`. |
| `OUT_OF_STOCK` | Product `availability.status` is `OUT_OF_STOCK`. |
| `MODIFIER_NOT_FOUND` | A child line id is not a valid modifier under its parent. |
| `MODIFIER_RULES_INVALID` | Modifier selection fails catalog min/max / required rules. |
| `VARIANT_BASE_NOT_ORDERABLE` | The line is a product with `variant_group_ids`. It only lets the customer choose options; send the resulting variant product instead. |
| `VARIANT_NOT_OFFERED` | The product is a variant, but no active, visible base resolves to it anymore. On a modifier line, the base listed in the parent's modifier group doesn't resolve to this variant. |
| `PRICE_MISMATCH` | A price money field differs from the menu rebuild (`unit_subtotal`, `total`, group totals, …). |
| `TAX_MISMATCH` | Tax rate or tax totals differ from the menu rebuild. |
| `DISCOUNT_MISMATCH` | Discount rate or discount totals differ from the menu rebuild. |
| `QUANTITY_MISMATCH` | Line quantity (or unit×qty vs line totals for fees) does not match. |
| `STRUCTURE_MISMATCH` | Tree shape differs (missing/extra children, wrong product id under a node). |

> [!TIP]
>
> Render `details.lines[uid]` next to the matching cart row so the customer can
> remove or re-customize the item. Treat `details.issues` as cart-wide banners
> (closed store, bad totals, empty cart).

## Other error responses

| Status | Description |
| - | - |
| `400` | Malformed body (Zod validation) |
| `401` | Missing or invalid access token |
| `422` | Order validation failed (see above), or invalid/inactive selling scope when resolving the menu |

## Related

> [card] [Get menus](https://firespark.cloud/docs/storefront-api/menus/get.md)
>
> Fresh menu for the same scope

> [card] [Update order](https://firespark.cloud/docs/storefront-api/orders/\[id]/patch)
>
> Sync lines after the customer fixes the cart

> [card] [Placing orders](https://firespark.cloud/docs/storefront-api/guides/placing-orders.md)
>
> Full checkout journey


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.