Integrating
Errors and versioning
One error shape for everything the gateway answers, one table of the status codes it really produces, and an honest account of what the version promise is backed by.
The envelope
Every failure the gateway itself produces — validation, authorisation, supplier, ours — comes back in the same shape, with the HTTP status carrying the category.
{
"error": {
"type": "price_changed",
"message": "The airline now prices this offer at 141.00 USD.",
"requestId": "a4f1c358-6b2e-5acb-8ef5-0c3d0bcc35f5",
"previousTotalCents": 13500,
"currentTotalCents": 14100
}
}Two things the envelope does not cover
502 or an HTML error page — are not in this shape. Guard your parsing: check the content type and fall back on the status code rather than assuming error.type is there. And never parse message: messages get rewritten, translated and clarified, and that is not a breaking change.Status codes
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. Note that a 5xx releases the key, so on the booking endpoints it is externalReference that keeps a retry from becoming a second operation.
Known gap: a reused externalReference on travel
externalReference that already belongs to an operation is answered correctly on POST /v1/esim/orders — the existing order comes back, or you get a 409. On hotel, flight and transfer bookings the same situation currently surfaces as 500 internal_error. It is a caller error wearing a server error's clothes: retrying will not change it. If a booking returns 500, read the operation back before you retry more than once, and check whether you reused a reference.If you use the TypeScript SDK
errorFrom() maps the known types to error classes and everything else by status code. price_changed is not among the mapped types today, so it arrives as an IdempotencyError by virtue of being a 409. Branch on error.type, not on the class, and you are unaffected.Versioning
The version is in the path: every partner route lives under /v1. The unauthenticated probes /health, /health/ready and /health/full sit outside it and are in the same OpenAPI document. 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 — including new error types.
- 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
type enum is already narrower than what the gateway can send, so a generated client with a closed union needs a fallback today, not one day.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 — is not something we do in /v1. It belongs in a new major version at a new path.
What backs that promise, and what does not
/v2, no version registry and no contract test that compares today's schema against a frozen published baseline. Keep your own snapshot tests on the responses you depend on. If a field you rely on disappears, that is a bug on our side — tell us, quoting a requestId.Staying informed
Changes are written up in the changelog, which is maintained by hand. There is no feed, no API for it and no automatic notification: responses carry no Deprecation or Sunset header, and nothing emails your partner admins when the contract moves. Do not build alerting on a signal we do not send — watch the changelog, and talk to your Vacabee contact before you plan a migration around a date.

