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.

StatusCodeMeaning
400bad_requestThe request is malformed or fails validation. Validation errors list each field in details.fields[].
401unauthorizedNo key, or the key is unknown or revoked.
402insufficient_creditNot enough credit for the call. Checked before any work starts, so nothing was billed.
403forbiddenThe 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").
404not_foundThe hotel, offer, booking, job or webhook doesn't exist for this key.
409conflictThe Idempotency-Key was reused with a different body, or the resource is in a state that forbids the change.
429rate_limitedOver the per-minute limit for the tier. Wait Retry-After seconds.
502upstream_errorA service we depend on failed. Safe to retry with backoff.
503unavailableWe'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".

TierRequests per minute
Free10
Build120
Scale600
EnterpriseCustom
Headers

What the headers tell you.

HeaderMeaning
X-RateLimit-LimitRequests allowed per minute on this key.
X-RateLimit-RemainingRequests left in the current minute.
X-RateLimit-ResetUnix time the window resets.
Retry-AfterOn 429: seconds to wait before retrying.
X-Credit-RemainingCredit left on the account, a USD decimal string.
Retries

When to try again.

  • 429: wait Retry-After seconds.
  • 502 and 503: 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.

FieldTypeDescription
code
REQUIRED
stringStable 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.
message
REQUIRED
stringHuman-readable explanation in English; do not parse it, branch on code and details.
request_idstring | nullId of the failed request, the same value as the X-Request-Id header.
detailsobject | nullMachine-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.