Getting started · Errors
Errors & rate limits
Every error has a consistent JSON shape: a machine-readable error code, a human message and a `request_id` you can quote when contacting support.
Error response format
All errors follow the same JSON structure regardless of which endpoint returned them:
| Field | Type | Description |
|---|---|---|
| error | string | A human-readable sentence. On a 502 this is the airline's own message, bracketed code included. It is NOT a stable machine-readable code — do not branch on its text. |
| message | string | Present only on the structured 402 quota and 503 feature-gate responses, alongside `reason`/`docs_url`. Absent from every other error. |
// Every NDC endpoint — one field, nothing else, by default.
{ "error": "missing X-API-Key" }
// A 502 carries the airline's own text.
{ "error": "airline error: [230000002] Invalid or Expired Offer" }
// An order id this account doesn't hold adds a stable code, and a
// suggestion when one of your own orders is a one-character typo away.
{
"error": "no order with id \"AA001Y1XD7ZB6\" belongs to this account — copy the order_id field from POST /v1/orders or GET /v1/orders rather than retyping it. Did you mean AA001Y1XD7ZA6?",
"code": "order_not_found",
"did_you_mean": "AA001Y1XD7ZA6"
}
// The two structured exceptions: 503 feature gate and 402 quota.
{
"error": "coming_soon",
"message": "The booking flow is not yet available in this environment.",
"docs_url": "https://norba.io/docs/changelog"
}Error codes
Every error returned by the API uses one of the following codes:
| HTTP | Class | Meaning |
|---|---|---|
| 400 | Validation | The handler rejected your body. The message names the problem: `adults must be >= 1`, `invalid departure_date`, `origin and destination are required`, `offer_id is required`, `at least one passenger is required`, `unknown airline` (an offer or order id with no recognisable airline prefix and no `?airline=`). On an order-id endpoint specifically, a supplied `?airline=` that disagrees with the order's own stored carrier answers `airline does not match the order` instead — the carrier there is inferred from the order, not from the query string. Body that is not valid JSON answers `invalid request body`. |
| 401 | Auth | `missing X-API-Key` when the header is absent, `invalid api key` when the key is not a valid Norba API key. `GET /v1/orders` also answers 401 when the key has no agency behind it. |
| 402 | Quota | Standing check failed on a developer-portal key. `reason` is `payment_required` (free allowance exhausted), `payment_method_required` (plan requires a card, none on file), `delinquent`, or `past_due` — alongside `message`, `plan_code`, `period_start` and `period_end`. See /docs/billing. |
| 403 | Unknown plan | `unknown_plan` — the account's plan code matches nothing in the pricing catalogue. Same structured body as 402; a configuration problem no card on file can fix. |
| 404 | Not found | On an order id: `code: "order_not_found"` — no order with that id in your agency's store; there is no airline fallback any more, whatever `?airline=` you send. Carries `did_you_mean` when one of your agency's own orders is a one-character typo away (O/0, I/1 and the like). Or `offer no longer available at airline`, `connection offer not found or expired`. |
| 413 | Body too large | `request body too large` — every request body is capped at 1 MiB. A valid document over the cap gets its own status rather than being reported as malformed JSON. |
| 429 | Rate limit | More than 60 searches in the last minute (`Retry-After: 60`), or `reason: allowance_burst` — more calls in two minutes than the free requests you have left, held back rather than overspent (`Retry-After: 120`). Neither means anything is owed. |
| 502 | Airline error | The carrier refused the request and the message is its own text, e.g. `[230000002] Invalid or Expired Offer`. Also `all airline connections failed — please retry later`. |
| 503 | Feature gate | `coming_soon` — order writes are disabled in this environment. Carries `Retry-After: 3600`. Reads keep working. |
Rate limits
Rate limits are sliding-window per agency. On `429` the response carries a `Retry-After` header indicating how many seconds to wait before retrying.
| Limit | Scope | Details |
|---|---|---|
| 60 req/min (default) | Per agency | POST /v1/shopping/offers and /stream. Adjustable per account; not a platform cap. |
| Daily/monthly ceilings, look-to-book policy | Per agency | Shopping and order creation, when one has been agreed for the account. Contractual limits some airlines require, not a capacity limit — most agencies have none set. |
| No limit at all | Per agency | /v1/aviation/* and /v1/reference/* — exempted outright, since they never reach an airline. |
| No limit | Per agency | Every remaining endpoint. Metered against your plan quota rather than rate-limited — exhausting it returns 402, not 429. |