API v2 · Reference
Status codes and what to do about them.
Every error has the same envelope. Branch on error.code; message is for people.
Envelope
One shape for every error.
Validation errors are 400 with details.fields[]. A call your tier doesn't include answers 403 with details.reason: "tier_required" and details.required_tier; Enterprise-only features answer 403 with details.reason: "enterprise_only". Quote request_id when you write to us.
Response
403
{
"error": {
"code": "forbidden",
"details": {
"required_scope": "rates"
},
"message": "This key does not have the 'rates' scope.",
"request_id": "req_8f2c1a9b3d4e5f60"
}
}Codes
Error codes.
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | The request is malformed or fails validation. Validation errors list each field in details.fields[]. |
| 401 | unauthorized | No key, or the key is unknown or revoked. |
| 402 | insufficient_credit | Not enough credit for the call. Checked before any work starts, so nothing was billed. |
| 403 | forbidden | The key lacks the scope the endpoint needs (details.required_scope), the tier doesn't include the call (details.reason: "tier_required" with details.required_tier), or the feature is Enterprise-only (details.reason: "enterprise_only"). |
| 404 | not_found | The hotel, offer, booking, job or webhook doesn't exist for this key. |
| 409 | conflict | The Idempotency-Key was reused with a different body, or the resource is in a state that forbids the change. |
| 429 | rate_limited | Over the per-minute limit for the tier. Wait Retry-After seconds. |
| 502 | upstream_error | A service we depend on failed. Safe to retry with backoff. |
| 503 | unavailable | We're temporarily unable to serve the call. Retry with backoff. |
Limits
Requests per minute, by tier.
Limits are per key. There's no monthly quota: credit is the only ceiling.
Live rates, batch rates, room reconciliation and bookings need Build or above. On Free they answer 403 forbidden with details.reason: "tier_required" and details.required_tier: "build".
| Tier | Requests per minute |
|---|---|
| Free | 10 |
| Build | 120 |
| Scale | 600 |
| Enterprise | Custom |
Headers
What the headers tell you.
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute on this key. |
X-RateLimit-Remaining | Requests left in the current minute. |
X-RateLimit-Reset | Unix time the window resets. |
Retry-After | On 429: seconds to wait before retrying. |
X-Credit-Remaining | Credit left on the account, a USD decimal string. |
Retries
When to try again.
429: waitRetry-Afterseconds.502and503: back off exponentially. A 503 is never billed.402: top up first. We check credit before any work starts, so nothing was spent.400,401,403,404,409: retrying the same request won't help.- Retry POSTs with the same
Idempotency-Keyso a timeout can't start two jobs.
ErrorBody
The error itself: a stable code, a readable message and optional details.
| Field | Type | Description |
|---|---|---|
codeREQUIRED | string | Stable error code; the HTTP status follows from it (bad_request 400, unauthorized 401, insufficient_credit 402, forbidden 403, not_found 404, conflict 409, rate_limited 429, upstream_error 502, unavailable 503). One of bad_request, unauthorized, forbidden, not_found, conflict, rate_limited, insufficient_credit, upstream_error, unavailable. |
messageREQUIRED | string | Human-readable explanation in English; do not parse it, branch on code and details. |
request_id | string | null | Id of the failed request, the same value as the X-Request-Id header. |
details | object | null | Machine-readable context when there is some: fields[] on validation errors, required_scope on a missing scope, and reason elsewhere, for example tier_required (with required_tier) or enterprise_only on a 403, balance or spend_cap on a 402, idempotency_mismatch on a 409, search_expired on a 400. |