# API Conventions

This section covers the core mechanics of the Marketfront API, including error handling, rate limiting, and caching.

## Authentication

All requests require a Bearer token in the `Authorization` header. See the [Authentication guide](/distribution-partners/shared-guides/authentication) for key management, environment setup, and security best practices.

## Money & amounts

Every monetary value is a **self-describing money object**: the `amounts` breakdown carries a `currency` (ISO 4217, e.g. `USD`) and each amount is a **decimal string** — `"10.50"`, never a bare JSON number.

```json
{
  "amounts": {
    "currency": "USD",
    "subTotal": "18.00",
    "fees": "2.99",
    "taxes": "1.71",
    "tip": "3.00",
    "total": "25.70"
  }
}
```

Send amounts the same way you receive them — as strings (e.g. `amounts.tip` on `validateOrder`).

**Why strings?** Parsing money as a JSON number invites IEEE-754 drift (`0.1 + 0.2 !== 0.3`). A decimal string parsed with a decimal/big-decimal type on your side is exact. Currency is explicit so you never have to assume USD.

### Schema inheritance

Some schemas in the API reference render as `allOf: [{$ref: Base}, {additional properties}]`. Read this as inheritance — the object has all the fields from the referenced base schema, plus the additional ones listed. For example, `DeliveryFulfillment` extends `Fulfillment` (adding `address`, `deliveryType`, `instructions`), and `OrderPlaceRequest` extends `OrderValidateRequest` (adding `payment`).

This is why a field can appear once and apply everywhere. The required `client` object is declared on `OrderCore`, so both `validateOrder` and `placeOrder` carry it — see [End-user client context](/distribution-partners/marketfront-api/guides/order-lifecycle).

### Decimal places per currency

An amount must not carry more decimal places than its currency's minor unit, or the request is rejected with `422` [`PRECISION_EXCEEDED`](/errors/PRECISION_EXCEEDED). Round to the currency's scale before sending.

| Minor-unit scale | Example currencies | Example |
|------------------|--------------------|---------|
| 2 (default) | `USD`, `EUR`, `GBP`, `CAD`, `AUD` | `"10.50"` |
| 0 | `JPY`, `KRW`, `VND`, `CLP` | `"1000"` |
| 3 | `BHD`, `KWD`, `OMR`, `TND` | `"10.500"` |

## Error Handling

All error responses use [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) `application/problem+json` format. Every error includes machine-readable fields (`errorCategory`, `retryable`, `retryAfter`) for programmatic handling. Domain-specific errors include an `errorCode` field identifying the specific issue.

The API supports content negotiation via the `Accept` header — request `text/markdown` for a compact text representation suitable for AI agents and CLI tooling. See the [Error Reference](/errors) for full details.

### HTTP Status Codes

| Status | Meaning |
|--------|---------|
| `400` | Invalid request body, missing fields, or domain-specific errors (see `errorCode`) |
| `401` | Missing or invalid API key |
| `404` | Resource does not exist |
| `409` | Cart or store conflict — `CART_STALE`, `STORE_PAUSED`, `ENVIRONMENT_MISMATCH` |
| `422` | Request-shape or semantic validation of the request body failed (e.g. malformed body, [`PRECISION_EXCEEDED`](/errors/PRECISION_EXCEEDED)) |
| `429` | Rate limit exceeded |
| `500` | Unexpected server error |

:::note[Order/business validation is different]
`validateOrder` never returns a 4xx for business or order problems (item unavailable, store closed, minimum not met, etc.). It returns **HTTP 200 with `isValid: false` and a populated `errors[]`** on the `Order` body. Request-shape, authentication, **and cart/store conflict** errors still produce 4xx from that endpoint — a stale cart, a paused store, an environment mismatch (`409`) or an over-precise amount (`422`) are raised before the order is ever built, so handle both shapes. See [Validation vs Placement](/distribution-partners/marketfront-api/guides/order-lifecycle#validation-vs-placement) in the Order Lifecycle guide.
:::

### Error Codes

Domain-specific errors include an `errorCode` field.

:::note[Two kinds of code appear below]
The tables in this section list both:

- **Top-level `ProblemDetail.errorCode`** — names an overall 4xx failure and carries an HTTP status, an `errorCategory` and a `retryable` flag.
- **`errors[].code`** — a per-element reason on an order. These arrive on an HTTP `200` response with `isValid: false` and have no status, category or retryable of their own.

The **Kind** column on each table below says which one a code is. The authoritative list of `errors[].code` reasons is the [Order Lifecycle guide](/distribution-partners/marketfront-api/guides/order-lifecycle#error-codes).
:::

#### Cart & Item Errors

| Code | Kind | Description | Action |
|------|------|-------------|--------|
| [`CATALOGSET_REQUIRED`](/errors/CATALOGSET_REQUIRED) | `errorCode` `400` | Cart creation requires a catalogSetId | Include `catalogSetId` when cart is null |
| [`ITEM_UNAVAILABLE`](/errors/ITEM_UNAVAILABLE) | `errors[].code` | Item no longer available | Remove from cart and re-validate |
| `MINIMUM_NOT_MET` | `errors[].code` | The **whole order** is below the store's order minimum | Add more items |
| [`MODIFIER_REQUIRED`](/errors/MODIFIER_REQUIRED) | `errors[].code` | **One item** is missing a required choice | Resolve `pointer` to that line item and have the user pick the option |

#### Store Errors

| Code | Kind | Description | Action |
|------|------|-------------|--------|
| [`STORE_CLOSED`](/errors/STORE_CLOSED) | `errors[].code` | Store is not accepting orders right now | Re-discover stores |
| `OUTSIDE_AVAILABILITY_WINDOW` | `errors[].code` | Requested scheduled time is outside the store's hours | Adjust `scheduledTime` or switch to ASAP |
| `DELIVERY_UNAVAILABLE` | `errors[].code` | Delivery is not available for this order | Offer pickup or a different store |
| `STORE_PAUSED` | `errorCode` `409` | Gett has paused this store | Offer another store, or retry after the pause lapses |
| [`STORE_NOT_FOUND`](/errors/STORE_NOT_FOUND) | `errorCode` `404` | Store does not exist | Re-discover stores |
| [`CATALOGSET_NOT_FOUND`](/errors/CATALOGSET_NOT_FOUND) | `errorCode` `404` | CatalogSet does not exist | Re-discover for updated `catalogSetId` |

#### Payment Errors

| Code | Kind | Description | Action |
|------|------|-------------|--------|
| [`PAYMENT_FAILED`](/errors/PAYMENT_FAILED) | `errors[].code` | Payment declined without an attributable reason | Ask the user for a different payment method |
| [`PAYMENT_TOKEN_INVALID`](/errors/PAYMENT_TOKEN_INVALID) | `errorCode` `400` | `paymentToken` is not usable for this order | Re-vault the card and quote the new token |
| [`CARD_EXPIRED`](/errors/CARD_EXPIRED) | **both** | Card has expired, or expired while stored | Collect and vault a new card |
| [`INSUFFICIENT_FUNDS`](/errors/INSUFFICIENT_FUNDS) | `errors[].code` | Declined — the account does not cover the amount | Ask the user for a different payment method |
| [`INCORRECT_CVC`](/errors/INCORRECT_CVC) | `errors[].code` | Declined — the security code did not match | Let the user re-enter the security code and retry the same card |
| [`CARD_LOST_OR_STOLEN`](/errors/CARD_LOST_OR_STOLEN) | `errors[].code` | The card cannot be used | Ask for a different method. Show a **generic** message — never tell a cardholder their card is flagged |
| [`PROCESSING_ERROR`](/errors/PROCESSING_ERROR) | `errors[].code` | A temporary fault at the processor, not a decision about the card | Retry the same card with a **fresh** idempotency key — see the page for the double-charge caveat |
| [`PAYMENT_DECLINED`](/errors/PAYMENT_DECLINED) | `errorCode` (reserved) | Declined by issuer — not currently emitted | Use a different payment method |

#### Order Errors

| Code | Kind | Description | Action |
|------|------|-------------|--------|
| [`ORDER_TOTAL_DIFFERENT`](/errors/ORDER_TOTAL_DIFFERENT) | `errors[].code` | Total changed since validation | Re-validate the order |
| `OTHER` | `errors[].code` | Catch-all, and the fallback for any unrecognized code | Show a generic error message |
| `CART_STALE` | `errorCode` `409` | The store's menu changed since the cart was built | Start a new cart from the current menu |
| `ENVIRONMENT_MISMATCH` | `errorCode` `409` | Store is not available for your account's environment | Check sandbox vs live credentials |
| [`IDEMPOTENCY_KEY_CONFLICT`](/errors/VALIDATION_FAILED) | `errorCode` `422` | `Idempotency-Key` reused with a different request body, or by a different partner or end user | Use a fresh key for a new order, or replay the same body as the same partner and end user |
| [`IDEMPOTENCY_KEY_IN_PROGRESS`](/errors/CONFLICT) | `errorCode` `409` | An earlier request on this `Idempotency-Key` is still in flight | Retry **unchanged on the same key** — a fresh key here places a duplicate order |

#### Open enum: order errors[].code

The `errors[].code` values on an order are an **open enum** (`x-extensible-enum`). The reason codes listed above are current but not exhaustive — new codes may be added as new failure modes are identified. Clients must handle unknown values as `OTHER` and must not hard-fail on an unrecognized code. Do not deserialize into a closed/strict enum type. See the full code table with suggested actions in the [Order Lifecycle guide](/distribution-partners/marketfront-api/guides/order-lifecycle#error-codes).

This is distinct from the top-level `ProblemDetail.errorCode` field (a single string naming the overall failure reason on a 4xx place response).

#### Authentication Errors

| Code | HTTP | Description | Action |
|------|------|-------------|--------|
| [`SESSION_USER_REQUIRED`](/errors/SESSION_USER_REQUIRED) | `401` | Endpoint requires an authenticated user identity | Ensure the end user is signed in before making this request |

### Retry Strategy

| Category | Retryable? | Strategy |
|----------|------------|----------|
| `401` Unauthorized | No | Fix your API key |
| `400` Bad Request | No | Fix the request |
| `404` Not Found | No | Resource doesn't exist |
| `429` Too Many Requests | Yes | Wait the number of seconds in `Retry-After`, then retry |
| `500` Internal Error | Yes | Exponential backoff (1s, 2s, 4s, max 30s) |

> Always include the `requestId` when contacting support about a specific error.

## Rate Limits

**One limit: 1,000 requests per minute, per partner organization, shared across every endpoint.**

There are no per-endpoint tiers. Reads, writes and store discovery all draw on the same counter, so
1,000 calls to `/stores/discover` and 1,000 calls to `/orders/place` in the same minute are 2,000
requests against one budget, not two budgets of 1,000.

Rate limits are scoped to your **partner organization** — all keys belonging to the same organization
share the same quota. Sandbox and production draw on the same limit; there is no sandbox multiplier.

### When you are limited

The API returns `429 Too Many Requests` with a **`Retry-After`** header giving the number of seconds
to wait. That is the only rate-limit header we send — there are no `X-RateLimit-*` headers, so do not
build against a running remaining-count.

Wait for `Retry-After` seconds, then retry. If the header is absent, use exponential backoff starting
at 1 second.

:::note[A 429 is shaped differently from our other errors]
Rate limiting is enforced at the gateway, ahead of the API. A `429` is therefore generated by the
gateway itself and does **not** carry the [`ProblemDetail`](#error-responses) envelope the rest of
this page describes — no `errorCode`, no `errorCategory`, no `retryAfter` body field. Branch on the
`429` status and the `Retry-After` header, not on the body.
:::

## Caching Strategy

| Data Type | Strategy | Why |
|-----------|----------|-----|
| **Store results** | Never cache | Store status changes constantly |
| **CatalogSets** | Cache aggressively | Immutable — use `catalogSetId` as cache key |
| **Validation tokens** | Expire in 15 min | Security and price accuracy |
