Integrating

Errors and versioning

One error shape for every endpoint, one rule for what may change without warning, and one mechanism for what may not.

The envelope

Every failure — validation, authorisation, supplier, ours — comes back in the same shape, with the HTTP status carrying the category.

Error response
{
  "error": {
    "type": "price_changed",
    "message": "The rate is now 141.00 USD.",
    "requestId": "req_01JD8ZC4KQ2M…",
    "docUrl": "…/docs/settlement"
  }
}
FieldUse it for
typeBranching. Stable, machine-readable, safe to switch on.
messageLogs and support tickets. Human-readable and subject to change — never match on it.
requestIdCorrelating with our side. Also on every success as X-Request-Id.
docUrlThe page that explains this failure.

Never parse messages

Messages get rewritten, translated and clarified; that is not a breaking change. An integration that branches on message text will break silently one day. Branch on type and the status code.

Status codes

StatusMeaningRetry?
400Malformed request, or a POST without an Idempotency-Key.No — fix the request.
401Missing, unknown, revoked or wrong-environment key.No.
402Mode A: not enough available credit, or the account is read-only.After topping up.
403Key lacks the required scope, the source IP is not allowed, or the settlement mode is not enabled for you.No.
404Unknown resource, or a handle that has expired.No — re-run the search.
409An idempotency key reused with a different body, or price_changed.No — decide first.
422The request is well formed but cannot be fulfilled — occupancy the property does not offer, an unbookable date.No.
429Rate limited, or past a daily search cap you set yourself.Yes — back off, honour Retry-After.
5xxOur fault or an unrecoverable supplier fault.Yes — with backoff and the same idempotency key.

For anything retryable, keep the original idempotency key and the original body. That is what turns a retry into a replay instead of a second booking.

Versioning

The version is in the path: every route lives under /v1. There is no version header and no per-account pinning, because two partners on different behaviour of the same URL is a support problem neither of us wants.

These changes land in /v1 at any time, without notice. Your integration has to tolerate them:

  • New fields in a response object.
  • New optional request parameters and new optional body fields.
  • New values in an existing enum.
  • New endpoints and new resources.
  • New webhook event types, and new fields in existing event payloads.
  • New response headers.
  • Rewritten error messages, at an unchanged type.

Two rules that keep you compatible

Ignore fields you do not recognise instead of failing to deserialise them, and treat an unknown enum value as an unknown rather than a crash. Strict schema validation on our responses will break your integration on a change we are explicitly allowed to make.

Anything that could break a correct integration — removing or renaming a field, tightening a type, changing the meaning of a value, removing an endpoint or an enum value, making an optional parameter required — never happens in /v1. It happens in a new major version at a new path.

If a version is retired

  • /v1 and /v2 run in parallel for at least six months. We expect to leave far more time than that, but six is the floor we commit to.
  • Responses from the retiring version carry a Deprecation header and a Sunset header with the date after which it stops answering. Alert on both — they are the earliest automatic signal you get.
  • Every deprecation is announced in the changelog and by email to your partner admins, with a migration guide.
  • Security fixes are the one exception. If a change is required to close a vulnerability we will make it as narrow as possible and tell you directly, rather than wait out a notice period.