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

# Authentication and Errors

> How Retail authenticates to your integration, the error format it expects, and how long each call may take

# Authentication and Errors

## The API key

Retail sends one header on every request:

```
X-API-Key: <the key Gomry issued you>
```

<Warning>
  **`X-API-Key`, not `Authorization: Bearer`.** It is a plain shared secret, not an OAuth token. An integration that reads `Authorization` answers 401 to every call, and both sides' unit tests still pass. Header names are case-insensitive, so `x-api-key` is the same header.
</Warning>

**Gomry generates the key.** It is 32 random bytes, delivered to you once over a secure channel. You never choose it, which lets us guarantee its strength and rotate it without asking you to redeploy code.

Your integration must:

1. **Fail closed.** If no key is configured on your side, reject every request. An unset key must never mean "allow everyone": `POST /orders` spends real money.
2. **Compare in constant time**, so a key cannot be guessed one character at a time.
3. **Accept more than one key.** During a rotation both the old and the new key are live for a while. A second key also lets you give a tester access without sharing Retail's own.
4. **Answer `401`** for a missing or wrong key, with the error body below.

```javascript Node.js theme={null}
import { timingSafeEqual } from "node:crypto";

const accepted = [process.env.GOMRY_RETAIL_API_KEY, process.env.GOMRY_RETAIL_API_KEY_NEXT]
  .map((k) => k?.trim())
  .filter(Boolean);

export function requireApiKey(request) {
  if (accepted.length === 0) throw unauthorized("API key is not configured");
  const presented = Buffer.from(request.headers.get("x-api-key") ?? "");
  const ok = accepted.some((key) => {
    const expected = Buffer.from(key);
    return expected.length === presented.length && timingSafeEqual(expected, presented);
  });
  if (!ok) throw unauthorized("Invalid or missing X-API-Key");
}
```

## Error format

Answer every failure with a JSON body in this shape:

```json theme={null}
{
  "error": {
    "code": "validation_error",
    "message": "Invalid request body: quantity Expected number, received string"
  }
}
```

`message` is for humans and goes into Retail's logs. Never put a buyer's email, address or a barcode in it.

| Status | `code` | Use it when |
| - | - | - |
| `400` | `validation_error` | The request is malformed, or your system refused it in words (the price moved, the hold expired, a required field for this delivery format is missing). |
| `401` | `unauthorized` | The API key is missing or wrong. |
| `404` | `not_found` | You do not know the thing asked about. See each endpoint for what that means there. |
| `429` | `throttled` | Your system (or your supplier) is rate limiting. Include `Retry-After`. |
| `502` | `integration_error` | Your upstream call failed or timed out. |
| `500` | `internal_error` | Anything else. |

### 404 has a meaning

Retail treats `404` as an answer, not a failure:

* **Listings:** a listing that was bought disappears. It does not report quantity 0.
* **Fulfilment, credentials:** you do not know this order yet.
* **Venue map:** treated as "no map".

Every other non-2xx status is a failure, and Retail never treats a failure as "nothing there".

### 429 pauses Retail's calls to you

When you answer `429`, Retail pauses its page-render and background calls to you, from every instance, until the time you give. Checkout calls still go through, so a buyer who is paying is not stranded by a catalog sync.

Give the time in either of these forms. Retail reads them in this order:

1. A `Retry-After` header, in seconds or as an HTTP date.
2. `error.retryAfter` in the body, as an ISO 8601 timestamp.

Without either, Retail assumes you will take calls again after the current minute.

```json 429 theme={null}
{
  "error": {
    "code": "throttled",
    "message": "Rate limit reached",
    "retryAfter": "2026-09-30T18:53:00Z"
  }
}
```

If your supplier limits calls per application, across all endpoints, say so when you [go live](/vendor-api/going-live). Retail keeps a shared per-minute budget per supplier and gives checkout and buyer-facing calls priority over background sync.

## Do not retry inside a call

Your integration **must not retry internally** on any endpoint. Retries belong to Retail, because only Retail knows whether an operation is safe to repeat:

* On **listings**, a slow success is worse than a fast failure. The buyer's page has already given up and rendered the event as unavailable.
* On **place order**, a retry can buy the ticket twice. See [Place order](/vendor-api/place-order).

## Time budgets

Retail aborts a call after these limits. A reply after the limit is discarded.

| Call | Limit | Why |
| - | - | - |
| Listings, while a page renders | 4.5 seconds | A buyer is waiting on the page. |
| Listings, background refresh | 12 seconds | Warms Retail's cache ahead of buyers. |
| Catalog page | 30 seconds | Scheduled sync. Size your pages to fit. |
| Quote, place order, credentials | 20 seconds | Checkout and delivery to the buyer. |
| Search orders, fulfilment | 25 seconds | Background reconciliation and delivery polling. |

A timed-out `POST /orders` is not a failure to Retail. It is an unknown outcome, resolved by [searching for the reference](/vendor-api/search-orders).
