# Errors

Errors use standard HTTP status codes and an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) `application/problem+json` body:

```json
{
  "type": "https://quickota.com/developers/errors/flights.price_changed",
  "title": "Price changed",
  "status": 409,
  "code": "flights.price_changed",
  "detail": "The airline repriced the fare above the accepted tolerance between search and booking. Nothing was booked.",
  "suggestion": "Price the offer again (or search again), show the new price to your customer and book only after they accept it.",
  "retryable": false,
  "requestId": "req_7f3a2c9e41d0b8a6c5e21f90",
  "context": { "offerTotal": { "amount": "180.00", "currency": "USD" } }
}
```

| Field | Use it for |
|---|---|
| `code` | **Branch your code on this.** Stable, never renamed. |
| `status` | The HTTP status, repeated. |
| `title`, `detail` | Human-readable; wording may change. Safe to log, not to parse. |
| `suggestion` | What to do next. |
| `retryable` | `true` when the same request may succeed later without changes. |
| `requestId` | Quote it to support — we can find the exact request. |
| `errors` | For `request.validation_failed`: field → messages, e.g. `"passengers[0].birthDate": ["…"]`. |
| `context` | Extra data for some codes (shortfall amounts, current status, allowed actions). |
| `type` | Link to this code's documentation page. |

## Handling errors — a checklist

1. **4xx with `retryable: false`** — fix the request or the flow. Retrying the same call will fail the same way.
2. **429** — wait `Retry-After` seconds. See [Rate limits](https://quickota.com/developers/guides/rate-limits-and-look-to-book.md).
3. **5xx or `retryable: true`** — retry with exponential backoff (e.g. 1 s, 2 s, 4 s … max 5 tries). For writes, **send the same `Idempotency-Key`** so a retry can never book or ticket twice. See [Idempotency](https://quickota.com/developers/guides/idempotency.md).
4. **Timeout with no response on `issue`** — do **not** call issue with a new key. `GET` the booking: if it is `issuing` or `ticketed`, the first call worked. See [Managing booking states](https://quickota.com/developers/guides/managing-booking-states.md).
5. Log `requestId` for every error.

We never put supplier names, stack traces or internal messages in errors; support can see them from the `requestId`.

## Machine-readable catalog

- `https://quickota.com/developers/errors.json` — every code with status, title, description, resolution and `retryable`.
- `https://quickota.com/developers/errors/{code}` — one page per code.

The full list follows.


## All error codes

### `auth.*`

| Code | HTTP | When it happens | What to do | Retry |
|---|---|---|---|---|
| [`auth.endpoint_not_allowed`](https://quickota.com/developers/errors/auth.endpoint_not_allowed) | 403 | API keys can only call documented public endpoints under the public base URL. | Check the path against the API reference. | no |
| [`auth.insufficient_scope`](https://quickota.com/developers/errors/auth.insufficient_scope) | 403 | The key does not carry the scope this endpoint needs (for example `flights:book` to create bookings). | Edit the key in the panel and add the scope named in `context.requiredScope`, or use a key that has it. | no |
| [`auth.invalid_key`](https://quickota.com/developers/errors/auth.invalid_key) | 401 | The key is malformed, was mistyped, or never existed. | Copy the key again from the panel. A key is shown only once when it is created — if it is lost, create a new one and revoke the old one. | no |
| [`auth.ip_not_allowed`](https://quickota.com/developers/errors/auth.ip_not_allowed) | 403 | The key has an IP allow-list and the request came from an address that is not on it. | Call from an allowed server, or add this server's public IP (or CIDR range) to the key's allow-list in the panel. | no |
| [`auth.key_expired`](https://quickota.com/developers/errors/auth.key_expired) | 401 | The key had an expiry date and it has passed. | Create a new key (or rotate the expired one) in the panel. | no |
| [`auth.key_revoked`](https://quickota.com/developers/errors/auth.key_revoked) | 401 | The key was revoked by your agency or by platform support, or it was replaced by a rotation whose grace period ended. | Use the current key from the panel, or create a new one. | no |
| [`auth.missing_key`](https://quickota.com/developers/errors/auth.missing_key) | 401 | The request has no `Authorization: Bearer <key>` header, or it carries a panel login token instead of an API key. The public API only accepts API keys. | Create a key in your panel under Developers → API keys and send it as `Authorization: Bearer tam_live_…`. | no |

### `account.*`

| Code | HTTP | When it happens | What to do | Retry |
|---|---|---|---|---|
| [`account.agency_inactive`](https://quickota.com/developers/errors/account.agency_inactive) | 403 | Your agency account is deactivated on the platform. | Contact platform support. | no |
| [`account.api_suspended`](https://quickota.com/developers/errors/account.api_suspended) | 403 | API access for your agency was suspended — usually because usage limits or the look-to-book ratio were breached, or on request of platform support. | Contact platform support. Your panel stays available; only API access is paused. | no |
| [`account.insufficient_balance`](https://quickota.com/developers/errors/account.insufficient_balance) | 402 | The wallet balance does not cover the amount due. `context` carries `required`, `available` and `shortfall`. The booking hold is kept until it expires. | Top up the wallet (panel → Finance) and call the same endpoint again before the hold expires. Check `GET /account/balance` before issuing to avoid this. | no |
| [`account.module_not_installed`](https://quickota.com/developers/errors/account.module_not_installed) | 403 | Your agency has not activated the module this endpoint belongs to, or the Developer API module itself. | Activate the module from your panel's module store, or contact your account manager. | no |
| [`account.wallet_currency_mismatch`](https://quickota.com/developers/errors/account.wallet_currency_mismatch) | 409 | The booking currency differs from your wallet currency. | Search with `currency` set to your wallet currency (see `GET /account/balance`). | no |
| [`account.wallet_unavailable`](https://quickota.com/developers/errors/account.wallet_unavailable) | 503 | The payment could not be taken from your wallet for a reason other than balance (wallet missing or temporarily unavailable). Nothing was charged. | Retry later; contact support if it persists. | yes |

### `request.*`

| Code | HTTP | When it happens | What to do | Retry |
|---|---|---|---|---|
| [`request.conflict`](https://quickota.com/developers/errors/request.conflict) | 409 | Another request changed the same resource at the same moment. | Fetch the resource again and retry if the action is still needed. | yes |
| [`request.invalid_state_transition`](https://quickota.com/developers/errors/request.invalid_state_transition) | 409 | The action does not apply to the resource's current status (for example issuing a booking that is already cancelled). `context.currentStatus` and `context.allowedActions` describe what is possible now. | Fetch the resource and follow its `nextActions`. | no |
| [`request.malformed_json`](https://quickota.com/developers/errors/request.malformed_json) | 400 | The body is not valid JSON or does not match the expected shape. | Send `Content-Type: application/json` with a body matching the API reference. | no |
| [`request.not_found`](https://quickota.com/developers/errors/request.not_found) | 404 | The id does not exist, or it belongs to another agency. | Check the id, and that it was created with a key of the same agency. | no |
| [`request.validation_failed`](https://quickota.com/developers/errors/request.validation_failed) | 422 | One or more fields are missing or invalid. `errors` maps each field path to its messages. | Fix the listed fields and send the request again. | no |

### `idempotency.*`

| Code | HTTP | When it happens | What to do | Retry |
|---|---|---|---|---|
| [`idempotency.in_progress`](https://quickota.com/developers/errors/idempotency.in_progress) | 409 | The first request with this key has not finished yet. | Wait a few seconds and retry with the same key — you will receive the original result. | yes |
| [`idempotency.key_required`](https://quickota.com/developers/errors/idempotency.key_required) | 400 | This endpoint creates something that must never happen twice (a booking, a ticket), so it requires an `Idempotency-Key` header. | Send a unique value (a UUID is ideal) in `Idempotency-Key`. Reuse the same value when you retry the same operation. | no |
| [`idempotency.key_reused`](https://quickota.com/developers/errors/idempotency.key_reused) | 422 | The key was already used for a request with a different method, path or body. | Generate a new key for every new operation; reuse a key only to retry exactly the same request. | no |

### `rate_limit.*`

| Code | HTTP | When it happens | What to do | Retry |
|---|---|---|---|---|
| [`rate_limit.exceeded`](https://quickota.com/developers/errors/rate_limit.exceeded) | 429 | Too many requests in a short time for this key. `X-RateLimit-*` headers show the limit and when it resets. | Wait for the number of seconds in `Retry-After`, then retry. Spread calls out and cache search results. | yes |

### `quota.*`

| Code | HTTP | When it happens | What to do | Retry |
|---|---|---|---|---|
| [`quota.look_to_book_exceeded`](https://quickota.com/developers/errors/quota.look_to_book_exceeded) | 429 | Your agency searches far more than it books. Searching is paused until the ratio recovers; booking endpoints keep working. | Reduce repeated or automated searches (cache results, avoid price scraping) and convert more searches into tickets. Contact support if you believe this is wrong. | yes |
| [`quota.searches_daily_exceeded`](https://quickota.com/developers/errors/quota.searches_daily_exceeded) | 429 | Your agency used all searches allowed for today. | Wait until the quota resets (00:00 UTC) or ask your account manager for a higher quota. Cache results instead of repeating identical searches. | yes |
| [`quota.searches_monthly_exceeded`](https://quickota.com/developers/errors/quota.searches_monthly_exceeded) | 429 | Your agency used all searches allowed for this month. | Wait until the next month or ask your account manager for a higher quota. | yes |

### `flights.*`

| Code | HTTP | When it happens | What to do | Retry |
|---|---|---|---|---|
| [`flights.booking_not_serviceable`](https://quickota.com/developers/errors/flights.booking_not_serviceable) | 409 | Post-booking requests need a ticketed booking that has not been fully cancelled or flown. | Check the booking's `status` and `nextActions`. | no |
| [`flights.cancellation_not_allowed`](https://quickota.com/developers/errors/flights.cancellation_not_allowed) | 409 | The fare rules do not allow cancelling or refunding at this time. | Check the fare rules. A no-show refund of taxes may still be possible through a post-booking request. | no |
| [`flights.duplicate_booking`](https://quickota.com/developers/errors/flights.duplicate_booking) | 409 | An identical booking already exists (same offer or same request). | Look the existing booking up with `GET /flights/bookings?agencyReference=…` before booking again. | no |
| [`flights.exchange_rate_unavailable`](https://quickota.com/developers/errors/flights.exchange_rate_unavailable) | 503 | No exchange rate is configured between the fare currency and your wallet currency, so the fare cannot be priced safely. | Search in your wallet currency, or contact support to configure the rate. | no |
| [`flights.fare_not_available`](https://quickota.com/developers/errors/flights.fare_not_available) | 409 | The seats at this fare were sold between search and booking. | Search again and pick another flight or fare. | no |
| [`flights.hold_expired`](https://quickota.com/developers/errors/flights.hold_expired) | 409 | The seats were released because the booking was not issued before `deadlines.holdExpiresAt`. The booking is now `cancelled`. | Search and hold again, then issue before the hold expires. | no |
| [`flights.hold_not_supported`](https://quickota.com/developers/errors/flights.hold_not_supported) | 409 | The airline issues this fare the moment it is booked (typical of low-cost carriers), so it cannot be held and paid later. The price check shows it as `hasFlightHold: false`. Nothing was booked. | Pick a fare with `hasFlightHold: true`, or book this one through the agency panel, which books and pays in one step. | no |
| [`flights.invalid_search`](https://quickota.com/developers/errors/flights.invalid_search) | 422 | The search criteria are incomplete or impossible: missing airports, a date in the past or more than a year ahead, no adult, more infants than adults, more than 9 seated passengers, or more than 6 legs. | Fix the fields listed in `errors` and search again. | no |
| [`flights.itinerary_not_bookable`](https://quickota.com/developers/errors/flights.itinerary_not_bookable) | 409 | The airline refused the itinerary (schedule changed, flight closed or cancelled). | Search again. | no |
| [`flights.offer_already_booked`](https://quickota.com/developers/errors/flights.offer_already_booked) | 409 | Each offer can be held once. This one already produced a booking (see `context.bookingId`). A retry of the same hold should reuse the same `Idempotency-Key`, which returns the original booking instead of this error. | Use the existing booking, or search again for a new offer. | no |
| [`flights.offer_expired`](https://quickota.com/developers/errors/flights.offer_expired) | 410 | The offer's validity window (`expiresAt`) has passed, or the airline no longer recognises it. Fares change constantly, so offers are short-lived. | Search again. Price (`POST /flights/offers/{offerId}/price`) and hold soon after searching. | no |
| [`flights.offer_not_found`](https://quickota.com/developers/errors/flights.offer_not_found) | 404 | The `offerId` is unknown. Offers belong to the agency that searched them and are kept for at most 30 minutes. | Search again and use an `offerId` from the new results. | no |
| [`flights.passenger_data_invalid`](https://quickota.com/developers/errors/flights.passenger_data_invalid) | 422 | A passenger is invalid: no adult on the booking, a child or infant without birth date, a passenger type that does not match the age, a missing or expired passport on an international trip (a national ID is accepted on domestic trips only), or a placeholder name such as TEST. Names must be in Latin letters exactly as on the travel document. | Correct the passenger listed in `detail` and hold again with a new `Idempotency-Key`. | no |
| [`flights.price_changed`](https://quickota.com/developers/errors/flights.price_changed) | 409 | The airline repriced the fare above the accepted tolerance between search and booking. Nothing was booked. | Price the offer again (or search again), show the new price to your customer and book only after they accept it. | no |
| [`flights.quote_expired`](https://quickota.com/developers/errors/flights.quote_expired) | 409 | The post-booking quote was not accepted before its `expiresAt`. | Wait for a new quote (support may re-quote) or create a new request. | no |
| [`flights.schedule_change_not_supported`](https://quickota.com/developers/errors/flights.schedule_change_not_supported) | 409 | The airline for this booking does not accept schedule-change replies through the API. | Contact support to handle this schedule change. | no |
| [`flights.servicing_request_open`](https://quickota.com/developers/errors/flights.servicing_request_open) | 409 | This booking already has an open post-booking request for these passengers. | Follow the open request (`openServicingRequests` on the booking) to its end, or withdraw it first. | no |
| [`flights.supplier_timeout`](https://quickota.com/developers/errors/flights.supplier_timeout) | 504 | The airline's reservation system was too slow. For a hold, nothing was booked unless the booking appears in `GET /flights/bookings?agencyReference=…`. | Retry with the same `Idempotency-Key` after a few seconds. | yes |
| [`flights.supplier_unavailable`](https://quickota.com/developers/errors/flights.supplier_unavailable) | 503 | The airline connection is temporarily unavailable or refused the request for a reason on its side. | Retry later, or search again for other options. | yes |
| [`flights.ticket_not_issued`](https://quickota.com/developers/errors/flights.ticket_not_issued) | 409 | The e-ticket document exists only once the booking is `ticketed`. | Wait for the booking to become `ticketed`, then download the ticket. | no |
| [`flights.ticketing_pending`](https://quickota.com/developers/errors/flights.ticketing_pending) | 409 | The airline is still issuing the tickets. This is normal for some airlines and can take up to several minutes. | Do not call `issue` again. Wait for the `flight.booking.ticketed` webhook, or poll `GET /flights/bookings/{id}` every `pollAfterSeconds`. | yes |
| [`flights.use_post_booking_request`](https://quickota.com/developers/errors/flights.use_post_booking_request) | 409 | A ticketed booking cannot be cancelled directly: the airline's refund or void conditions apply and must be quoted first. | Create a post-booking request (`POST /flights/bookings/{id}/post-booking-requests`) of type `refund` or `void`, wait for its quote, then accept it. | no |

### `webhooks.*`

| Code | HTTP | When it happens | What to do | Retry |
|---|---|---|---|---|
| [`webhooks.endpoint_not_found`](https://quickota.com/developers/errors/webhooks.endpoint_not_found) | 404 | The webhook endpoint id does not exist in this environment. | List your endpoints with `GET /webhook-endpoints`. | no |
| [`webhooks.url_not_allowed`](https://quickota.com/developers/errors/webhooks.url_not_allowed) | 422 | Webhook URLs must be public `https://` addresses. Private, loopback and internal network addresses are refused. | Use a publicly reachable HTTPS URL. | no |

### `server.*`

| Code | HTTP | When it happens | What to do | Retry |
|---|---|---|---|---|
| [`server.internal_error`](https://quickota.com/developers/errors/server.internal_error) | 500 | Something failed on our side. Nothing about your request is wrong. | Retry later. If it persists, contact support and quote the `requestId`. | yes |
| [`server.maintenance`](https://quickota.com/developers/errors/server.maintenance) | 503 | The API is in maintenance or overloaded. | Retry after the number of seconds in `Retry-After`. | yes |

