Every error response carries a stable machine-readable code, and every status
code maps to a defined condition.
Error envelope
Every error on this API has the same shape. One handler covers the whole
surface, including authentication failures and unrouted paths. The single
exception is the OAuth token endpoint, which follows the RFC 6749 error body
(error, error_description) as the OAuth standard requires. See
Authentication.
{
"error": {
"code": "BOOKING_NOT_CANCELLABLE",
"message": "booking is not cancellable",
"trace_id": "0f2c9d6e-7b1a-4c3e-9a2f-1d6e7b1a4c3e",
"retryable": false
}
}
| Field | Type | Notes |
|---|---|---|
code |
string | A stable, machine-readable UPPER_SNAKE error code. Branch on this, not on message. |
message |
string | A human-readable description. Wording may change; do not parse it. |
trace_id |
string | Always present. Correlates the response with our logs; quote it in support enquiries. |
retryable |
boolean | Whether a retry (after re-prebooking where relevant) can succeed. |
field |
string | Optional. Present when the endpoint can unambiguously identify the invalid request member; use it for form feedback, not error classification. |
code, message, trace_id, and retryable are always present. When the
request omits X-Request-Id, the API generates trace_id. When the request
includes X-Request-Id, the API echoes that value as trace_id for
correlation.
Status-code reference
| Status | Meaning | How to react |
|---|---|---|
400 Bad Request |
The request is malformed: bad JSON, an unknown body field or query parameter (field names it), a value outside a published minimum / maximum / pattern, an invalid date, a failed field validator, a forbidden search breadth field, more than 200 bulk properties, a max_offers_per_property outside 1..250 or a string other than "all", a missing selection proof (SELECTION_TOKEN_REQUIRED), an invalid proof (INVALID_SELECTION_TOKEN), a quoted block disagreeing with the quote sealed in an otherwise valid proof (QUOTED_MISMATCH, with field naming the member), a prebook_token that is unreadable, tampered with, or minted for a different caller (INVALID_TOKEN), a missing Idempotency-Key on book / package cancel (IDEMPOTENCY_KEY_REQUIRED), or a request carrying both a partner credential and Adrasis's internal service headers (AMBIGUOUS_AUTH). Place search adds PLACE_TYPE_UNSUPPORTED (an airport or landmark place, or a near place without a point), PLACE_TOO_BROAD (the place or radius exceeds the limits described below) and INVALID_CURSOR (a cursor that is tampered with, issued to another caller, or sent with a changed field). GET /catalog/changes answers INVALID_CURSOR or INVALID_REFERENCE_FILTER (an unsupported resource_type). The webhook endpoints answer INVALID_WEBHOOK_ENDPOINT for a malformed URL, cursor, subscription, property, profile or body. |
Fix the request. Do not retry unchanged. On INVALID_TOKEN the credential is not the problem, so refreshing it changes nothing: prebook again for a usable token. |
401 Unauthorized |
The credential is missing, malformed, expired, revoked, or signature-invalid. Codes: CREDENTIAL_MISSING, CREDENTIAL_MALFORMED, CREDENTIAL_UNAUTHORIZED. Carries a WWW-Authenticate: Bearer challenge. A rejected prebook_token or selection_token is a body or quote error, not a credential failure: malformed proofs return 400, expired selection proofs 410, and expired prebook tokens 409. |
Obtain a fresh token (see Authentication) and retry. |
403 Forbidden |
The token is valid but lacks the required scope (INSUFFICIENT_SCOPE), or an explicitly selected profile is inactive (PARTNER_PROFILE_INACTIVE). |
Use a credential/token with the required scope, or ask the account owner to select or activate the intended profile. Do not retry unchanged. |
404 Not Found |
The addressed resource is outside the authenticated account/profile boundary: an unknown property (PROPERTY_NOT_FOUND), room or rate plan on an availability check (ROOM_NOT_FOUND, RATE_PLAN_NOT_FOUND), booking (BOOKING_NOT_FOUND), trip (TRIP_NOT_FOUND), service zone, vehicle type, tour or flight (CATALOG_RESOURCE_NOT_FOUND), or an unknown country_code on GET /reference/place-types (COUNTRY_NOT_FOUND); a place public ID that is unknown or deleted, or one that exists only as an administrative area (PLACE_NOT_FOUND), a webhook endpoint or delivery that is absent or belongs to another account (WEBHOOK_ENDPOINT_NOT_FOUND), or the path is not an endpoint on this API (NOT_FOUND). An unknown or cross-account profile returns PARTNER_PROFILE_NOT_FOUND with field=profile_public_id when the selector is a query field. |
Verify the public ID and selected profile, or check the path against this reference. Do not retry blindly. |
405 Method Not Allowed |
The path exists but does not accept that method. Code: METHOD_NOT_ALLOWED. Answered by the router before any handler runs, which is why no single operation lists it. |
Use the method documented for the endpoint. |
409 Conflict |
The request conflicts with current state: an Idempotency-Key reused with a different body (IDEMPOTENCY_KEY_CONFLICT), an exact partner reference already owned by another booking in the same account namespace (AGENCY_REFERENCE_CONFLICT), an expired or spent prebook token, a selection proof for a property no longer open to the profile (SELECTION_RESELECT_REQUIRED), a booking in a terminal status (BOOKING_NOT_MODIFIABLE), a booking already in an active trip (BOOKING_ALREADY_LINKED), a duplicate or over-limit webhook endpoint (WEBHOOK_ENDPOINT_ALREADY_EXISTS, WEBHOOK_ENDPOINT_LIMIT_REACHED), a webhook delivery that is not retryable (WEBHOOK_NOT_RETRYABLE), a catalogue or place-search walk whose catalogue changed underneath it (CATALOG_VERSION_CHANGED), a place-search cursor whose kept search set has expired (SEARCH_EXPIRED), a test event asked of an endpoint that has not been verified (WEBHOOK_ENDPOINT_NOT_ACTIVE), an amendment of an external stay, which only its supplier can change (EXTERNAL_STAY_NOT_MODIFIABLE), an external stay's supplier refusing a cancellation or its quote (SUPPLIER_REJECTED), a package whose live flight the airline refused at booking, its other items cancelled (LIVE_FLIGHT_REFUSED), a package cancellation that reaches a live flight whose order still holds seats at the airline (LIVE_FLIGHT_ORDER_OPEN), or a flight or tour prebook of a tour operator's offer that it sells inside a package only (ENROLLED_OFFER_PACKAGE_ONLY). |
Resolve the named conflict: reuse the existing resource, compose a package-only offer into a package, use a new key for a new operation, re-prebook, verify the endpoint before testing it, or stop when the state is terminal. For CATALOG_VERSION_CHANGED and SEARCH_EXPIRED, discard the partial walk and search again from the first page. |
410 Gone |
The selected proof expired (SELECTION_TOKEN_EXPIRED), prebook found the exact slots sold out, or the rate disappeared between prebook and book (SOLD_OUT, RATE_NOT_AVAILABLE). |
Return to search for a fresh selection; do not retry the same token. |
412 Precondition Failed |
The If-Match ETag for a booking or webhook endpoint is stale. Codes: BOOKING_VERSION_CONFLICT, WEBHOOK_VERSION_CONFLICT. |
Re-read the resource, re-apply the change, and retry with the new ETag. |
422 Unprocessable Entity |
The request is well-formed but cannot be fulfilled: an unsupported currency pair, a rejected selling-price override, an unsupported payment intent, a child the property does not accept (GUEST_AGE_NOT_ACCEPTED), a single-property search whose best allocation or complete room list exceeds the work budget (SEARCH_WORK_LIMIT_BREACH), a prebook mixing pay-at-property and prepaid rooms (MIXED_SETTLEMENT), a webhook URL that resolves to a forbidden destination (WEBHOOK_DESTINATION_BLOCKED), a booking by a sub-agency whose account the provider suspended (ACCOUNT_SUSPENDED) or whose net is more than its account still has available (CREDIT_LIMIT_EXCEEDED), or a package whose live flight refuses a passenger's details (FLIGHT_PASSENGER_INVALID). |
Adjust the inputs. Nothing was written and no inventory was held. |
428 Precondition Required |
A conditional booking or webhook endpoint write omitted If-Match. Code: PRECONDITION_REQUIRED. |
Read the resource, then retry with its current ETag. |
415 Unsupported Media Type |
A request carrying a body declared a Content-Type other than application/json. Code: INVALID_REQUEST. Sending no Content-Type at all is accepted. |
Send Content-Type: application/json with the body. |
429 Too Many Requests |
The edge throttled general traffic, or the webhook API returned RATE_LIMITED after an account exhausted its published management or manual-delivery budget. Webhook responses use the normal error envelope. Both may carry Retry-After. |
Back off with jitter and honor Retry-After. Reuse the same idempotency key for a webhook command retry. See Rate limits and usage. |
500 Internal Server Error |
An unexpected server-side error. | Retry only when retryable is true. If it persists, capture the trace_id and contact support. |
503 Service Unavailable |
Search capacity is exhausted (SEARCH_OVERLOADED), a search exceeded its server deadline (SEARCH_TIMEOUT), /book did not confirm an outcome inside its 50-second application deadline (BOOKING_TIMEOUT), profile resolution is temporarily unavailable (PARTNER_PROFILE_UNAVAILABLE), the supplier of an external stay cannot be asked now (SUPPLIER_UNAVAILABLE, with Retry-After), or the webhook API cannot count a management request against its budget (SERVICE_UNAVAILABLE). Search failures carry Retry-After; branch on code. |
Honor Retry-After when present and retry when retryable is true. For BOOKING_TIMEOUT, resend the exact body with the exact same Idempotency-Key; the API timeout is 50 seconds. |
Response handling
On GET /properties and GET /bookings, an invalid or obsolete cursor is
400 INVALID_REQUEST with field=cursor; restart the lookup from its first
page. On GET /catalog/changes, an invalid cursor is 400 INVALID_CURSOR,
while a cursor that can no longer continue is 409 CATALOG_VERSION_CHANGED:
rebuild the property snapshot and resume from its terminal changes_cursor.
- Branch on
code, never onmessage. Codes are stable; messages are not. - Retry only the retryable. Retry eligible
429,500, and503responses with bounded backoff and jitter; honorRetry-Afteron429and503.401is retryable after refreshing the token.400,403,404,409,410, and422indicate a request or state problem and will not be fixed by an unchanged retry.PREBOOK_EXPIRED(409) succeeds after a fresh prebook. Every error carries aretryableboolean stating this directly, so prefer it over inferring from the status code. - Retry writes with the same idempotency key. When retrying
book, re-send the sameIdempotency-Keyand identical body so the retry replays rather than duplicates. See Idempotency. - Keep the
trace_id. Every error response carries one, including the401you get before authenticating; log it so a single request can be traced end to end in a support enquiry.
Per-property search failures
POST /properties/availability returns 200 when an individual property fails. The failure
surfaces inline on results[i].error_code: PROPERTY_NOT_FOUND,
UNSUPPORTED_FX_PAIR, SEARCH_WORK_LIMIT_BREACH, SEARCH_TIMEOUT,
SUBSTRATE_DATA_INVARIANT, or INTERNAL_ERROR. Inspect every result. Two
request-level failures are exceptions: search capacity exhaustion returns
top-level 503 SEARCH_OVERLOADED, and a deadline before any row completes
returns top-level 503 SEARCH_TIMEOUT. Both carry Retry-After.
SEARCH_TIMEOUT is the only retryable inline code; keep every other row and
retry only the affected property IDs with bounded backoff. Preserve their
relative order and all other search fields. For an inline timeout response,
even if the first request omitted it, set request_time to the first
response's request_time on every retry.
If a retry does not reduce the timeout set, partition the remaining IDs into
smaller ordered batches (for example, halves) before the next attempt. Continue
within an overall retry/time budget; a single-property timeout may be retried
with backoff, but SEARCH_WORK_LIMIT_BREACH is deterministic and must not be
retried unchanged.
On an initial availability request, a client-supplied request_time may be at
most 30 days behind or 60 seconds ahead of the server clock; both boundaries
are inclusive. A value outside the inclusive
[server_time - 30 days, server_time + 60 seconds] window is a top-level
400 INVALID_REQUEST with field=request_time; it never appears as a
per-property error_code.
For POST /places/availability, omitting request_time is safe on every page:
the signed cursor restores the first page's server-resolved instant. That valid
cursor-carried fixed instant remains usable until the cursor expires, even if
the server clock crosses the initial 30-day lower boundary during the walk. An
explicit value on a continuation must equal the first response. Do not derive
it from the HTTP Date header.
A top-level 503 has no property rows. Honor Retry-After and branch on its
code. For SEARCH_OVERLOADED, reduce concurrent search pressure and retry the
request. For SEARCH_TIMEOUT, no property completed before the deadline, so
partition the ordered property IDs into smaller batches before retrying.
Preserve the original request_time; if the original request omitted it,
choose one new evaluation instant before the first partitioned retry and reuse
that value across every partition.
Place search has its own request-level failures, all in the normal error envelope, none of them retryable unchanged:
| Code | Status | Meaning |
|---|---|---|
PLACE_NOT_FOUND |
404 |
place_id, or near.place_id, is not a known Adrasis place id or country code, is deleted, or is an administrative-only area. Take it from GET /countries, GET /places or location.destinations[]. |
PLACE_TYPE_UNSUPPORTED |
400 |
place_id is an airport or a landmark, which are not searchable as a place; use it as the centre of a near search instead. With near.place_id (field=near.place_id): the place has no point, a country for one. |
PLACE_TOO_BROAD |
400 |
A sorted or filtered place search exceeds 10,000 visible properties or 1,000 properties still requiring a price after unavailable ones are ruled out. Search a smaller place or add property-level filters. A near search has the same limits inside its radius and names field=near.radius_km; reduce the radius or add property-level filters. Price, board and refundability filters do not narrow these counts. |
INVALID_CURSOR |
400 |
The cursor is tampered with, was issued to another caller, or a repeated field, a near point or radius included, differs from the first request. A continuation may send the cursor alone. |
SEARCH_EXPIRED |
409 |
The cursor or its kept search set outlived the quote window. Search again from the first page. |
CATALOG_VERSION_CHANGED |
409 |
The catalogue changed during the walk. Search again from the first page. |
The single-property detail route uses top-level status codes because it has no
batch isolation boundary: 404 PROPERTY_NOT_FOUND, 422 UNSUPPORTED_FX_PAIR
or SEARCH_WORK_LIMIT_BREACH, and 500 for a server failure.
SEARCH_OVERLOADED and SEARCH_TIMEOUT both use 503; branch on code to
distinguish them. Optional projection failures do not invalidate priced
allocations; they appear as typed projection_issues inside a successful
response. An issue's code is INTERNAL_ERROR when the block failed on the
server, UNSUPPORTED_FX_PAIR when a fixed tax amount is kept in a currency with
no current rate to your currency_code (not retryable; a currency from
GET /reference/currencies may price it), or SUBSTRATE_DATA_INVARIANT when the
property's stored tax data is inconsistent.
Guest age refusals
A property's guest age rules surface in three places, and only one of them is an HTTP error:
| Where | What you receive | Meaning |
|---|---|---|
Search: POST /properties/availability, POST /places/availability and the detail route |
200 with no allocation and unavailable_reason: GUEST_AGE_NOT_ACCEPTED |
A requested child is younger than the youngest age the property accepts. It is not an error, and the same party gets the same answer on a repeat. |
POST /availability/check |
200 with verdict: REJECT, reject_reason GUEST_AGE_NOT_ACCEPTED or CHILD_AGES_REQUIRED, and failing_gate: 0 |
The age rule failed before any gate ran. CHILD_AGES_REQUIRED means the party has children without child_ages at a property whose minimum guest age is 1 to 17; send the ages to resolve it. An adults-only property answers GUEST_AGE_NOT_ACCEPTED for any child, ages sent or not. |
POST /prebook, POST /package/search and POST /package/prebook |
422 with error.code GUEST_AGE_NOT_ACCEPTED and retryable: false |
The property no longer accepts a child in the sealed party. Search again: the property now comes back unavailable for the same reason. |
Branch on the code, never on the message. Offer the traveller another property,
or a party the property accepts, instead of retrying the same request. A
property's min_guest_age is in Property content, so a
client can show the rule before it searches.
Mixed room settlement
422 MIXED_SETTLEMENT with field=selection_tokens refuses a selection containing both pay-at-property and prepaid rooms. Choose rooms with the same settlement route. No inventory is held.
Next steps
- Authentication: resolving
401and403. - Idempotency: resolving
409on writes. - The booking flow: the endpoints these codes apply to.
- Booking webhooks: webhook-specific conflict and retry handling.