Changelog

What changed

Everything that changes the API surface is recorded here and emailed to partner admins. Read the policy below once, and you can tell from any entry whether it needs work from you.

Deprecation policy

The contract behind every entry on this page.

  • Additive changes land in /v1 without notice. New fields, new optional parameters, new enum values, new endpoints, new webhook events, new response headers. Your integration must ignore unknown fields and tolerate unknown enum values.
  • Breaking changes only ever appear as a new major version at a new path — /v2. Nothing is removed, renamed, narrowed or given a new meaning inside /v1.
  • A retired version runs in parallel for at least six months. That is a floor, not a plan; in practice we expect to leave considerably longer.
  • The superseded version says so in its responses. A Deprecation header and a Sunset header with the date it stops answering. Alert on them — that is the earliest automatic warning you get.
  • Every deprecation is announced here and by email to your partner admins, with a migration guide, on the day the parallel period starts.
  • Security is the one exception. A change required to close a vulnerability is made as narrowly as possible and communicated directly, without waiting out a notice period.

The full detail, including what counts as additive, is on errors and versioning.

Entries

v1

Sixteen places a working integration could still be built wrong

Mostly documentation, plus three contract corrections and one tightened validation. Every one of them is something a developer building from these pages alone actually got wrong: a fourth cancellation-policy shape nobody mentioned, a header we said was ignored and read on the sandbox, a required request field no response ever hands out, a request field whose published type contradicted its own example, eight POSTs the document said answer 200 while every host answers 201, a sandbox that cannot show you quota accounting or a 404, and an SDK install that needs a credential we do not issue. The contract is now downloadable at /openapi.json.

  • changedEvery POST in this contract answers 201, and the document now says so on all seventeen of them instead of nine. The eight that were declared 200 — flight cancel, change-options and change, transfer search and cancel, webhook rotate-secret, test and delivery retry — have answered 201 on the sandbox and on live since launch; only the published declaration was wrong, and it was wrong in the direction that makes a generated client assert the code and reject a correct answer. Treat any 2xx as success rather than asserting an exact code. Nothing was changed on the wire, deliberately: adding @HttpCode(200) to match the old document would have broken the clients that already tolerate 201.
  • changedchildrenAges on a hotel search and a hotel search refinement was published as an array of STRING with the example [7, 11]. A generated client typed it string[], anybody copying the example sent numbers, and the gateway accepted both and forwarded both — a supplier prices what it can parse, so a room quoted for no children looked like a correct answer. The type is now number, matching the example, and the gateway validates per element: a non-integer or a negative age is 400 invalid_request instead of being passed on. If you were sending ["7", "11"] because the document asked for strings, send integers.
  • changedThe OpenAPI document is downloadable at https://developers.vacabee.com/openapi.json. It is the same document every page of /reference is generated from, the same one the TypeScript SDK's types are generated from, and the one CI compares both against byte for byte — so you can generate a client in any language and be on exactly our types. It existed in our repository and was served nowhere, which was the weakest part of recommending an SDK that not everybody can install.
  • fixedexpired on an eSIM usage response is documented, and it is not the same question as expiresAt. It is our verdict at the time of the call: a profile counts as expired once the supplier reports it as finished or once its data is used up — both of which happen before any printed date — and it is false while the profile is not provisioned yet, even past that date. expiresAt is frequently null, because many plans get no date until activation. Prefer expired over comparing expiresAt with your clock, or you will compute a different answer than we do and report the difference as our bug.
  • fixeddeliveryEnabled says what it is and what it is not. It is read-only and reflects whether our dispatcher has a usable signing key — which is also why POST /v1/webhooks/endpoints answers 503 upstream_error whenever it is false. The two are one cause, not two faults, and /docs/webhooks and /docs/sandbox now say so on both pages instead of describing the symptoms separately. While it is false, treat every delivery-dependent flow as untestable and do not retry; nothing of yours is wrong. Both hosts had it false until 17 September 2026.
  • fixedA cancellation policy arrives in FOUR shapes, not three. The fourth is on a transfer offer: cancellationPolicies is an array of { amountCents, from }. The field is amountCents and NOT chargeCents, there is no until and no refundable flag, the amount carries no currency of its own (it is the offer's), and from is a local date-time at the pickup point with no offset — the same trap as the hotel windows. /docs and the pre-live checklist on /docs/sandbox said three shapes; the reference showed the field as a bare object.
  • fixedX-Partner-Customer-Ref is not stored — and on the sandbox host it IS read. We keep no mapping and no traveller record, and on live the value is not read; the pages said "silently ignored" and stopped there. On the sandbox the prefix sbx_ is reserved: the six documented values force a failure, and any OTHER value starting with sbx_ is refused with 400 invalid_request. A traveller reference of yours that can start with sbx_ will break sandbox calls.
  • fixedPOST /v1/flights/orders/{id}/change-options cannot be driven end to end today, and the reference now says so instead of showing an example id. removeSliceId is required, and no response field of this API carries a slice id — FlightSliceDto has origin, destination, departure, arrival, durationMinutes and stops, and no id. The value comes from the airline itinerary. If you planned a rebooking flow around an order you booked here, plan a conversation with your Vacabee contact instead.
  • fixedexpiresAt on a change option is documented. It is the airline's deadline in UTC and may be null, which means the airline gave us none — treat the option as short-lived. A changeId is also one of our handles and dies about twenty minutes after the call, so the earlier of the two ends it and POST .../change then answers 410 offer_expired. In the sandbox expiresAt is always null. /docs said rate, offer and quote handles last twenty minutes without saying that a changeId carries its own clock.
  • fixedGET /health/full on the sandbox host reports hotels, flights, transfers and esim as not_configured, and that is expected rather than an outage: the sandbox answers from generated data and has no supplier connections. The published enum for a dependency status did not contain not_configured at all, and did contain degraded, which nothing produces. Both are corrected in the contract, and each value is now described where it is declared.
  • fixedX-Search-Billable is always false in the sandbox — on every route, including the ones whose reference page shows "Search quota: Counts". Nothing there reaches a supplier, so nothing would count. Quota accounting cannot be exercised in the sandbox; the reference now says so under the pill, and /docs/search-quota and /docs/sandbox say it in words.
  • fixedTwo more things the sandbox cannot show you, now named on /docs/sandbox: webhooks cannot be exercised there, and the known gap around a reused externalReference cannot be reproduced. Because the sandbox derives the booking id from your reference, a fresh Idempotency-Key with an already-used externalReference answers 201 with the SAME bookingId instead of the 500 internal_error you would get live.
  • fixedRegistering a webhook endpoint is currently refused for everyone with 503 upstream_error, on both hosts, because our signing key is misconfigured — and GET /v1/webhooks/deliveries reports deliveryEnabled: false with it. /docs/webhooks says this out loud rather than describing a flow that cannot be started; recorded events are not lost and go out once the key is fixed. deliveryEnabled is also documented as what it is: dispatch switched off on our side, not a fault on yours and not a reason to re-register.
  • fixedA webhook URL is resolved by DNS while the endpoint is being created, not at first delivery. A host without an A record is refused with 400 invalid_request and a message like "url name not resolvable: DNSException: getaddrinfo ENOTFOUND", and so is a private or loopback target. A placeholder such as webhook.example.com therefore never yields an endpoint id to work with.
  • fixedThe SDK is not publicly installable today, and /docs/sdk opens with that instead of mentioning it further down. 0.11.0 exists only on GitHub Packages, which never serves npm packages anonymously, and we issue no GitHub credentials as part of the partner relationship — so without an account holding read:packages on the vacabeedev organisation, npm install cannot succeed, and the answer is to integrate over plain HTTPS against /v1 rather than to chase a token. Every operation in the reference carries a curl example beside the TypeScript one. The page also stops claiming that a permissions failure and a missing version are indistinguishable: npm sending no token is 401 "authentication token not provided", a token without the scope or without access to the organisation is 403 permission_denied ("The token provided does not match expected scopes") and is the answer you will meet most often, a 404 from registry.npmjs.org means the scope was not redirected, and a 404 from npm.pkg.github.com means that version was never published. Publishing to npmjs.com, where no token is involved, is planned; this entry is where it will be announced.
  • changedThe pagination paragraph on /docs lists all six cursor-based lists — hotel bookings, flight orders, transfer orders, eSIM orders, ledger entries and webhook deliveries — and names the one irregularity in them: the deliveries list calls its array deliveries rather than items. The cursor is the same everywhere; follow nextCursor until it is null and never construct one.
v1

Six published statements that did not match the gateway

Five documentation corrections and one contract addition. Each of the five was a sentence somebody could build the wrong thing from: a status code that is not answered, a scope you were told to write in for, a billable field listed as free, a failure you were told you could not reproduce, and a sandbox read presented as a read-back. The addition declares eleven error fields that have been on the wire the whole time. No call changes its behaviour.

  • fixedupstream_error is 503, not 502. /docs/errors, /docs/sandbox and /docs/certification all said 502. It is 503 on purpose: Cloudflare replaces an origin 502 with its own plain-text error page, which would take error.type, error.message and the requestId away from you — exactly the three things our own pages tell you to read and to quote. upstream_unavailable shares the status, so branch on error.type and never on the status alone. If you special-cased 502 for a supplier failure, that branch has never been taken.
  • fixedtransfers:search and transfers:book can be granted in the portal, and could since 9 September. /docs/authentication said they were enforced by the gateway but missing from the key form and told you to ask your Vacabee contact — that was true for one week and stale for the following one. The key form now offers both; nobody has to write in for them.
  • fixedradiusKm, nearPoi and areaHint DO count against your search allowance. /docs/search-quota listed ten billable PATCH fields and then named these three as refinements that reach the supplier without being counted. There are thirteen, and these are three of them: moving the search radius forces a fresh request at the supplier and is metered. If you sized your daily quota around those PATCHes being free, re-check it against X-Search-Billable, which has always told you the truth per request.
  • fixedPrice drift IS reproducible in the sandbox. /docs/settlement said no trigger reproduced it and sent you to stub the 409 yourself; X-Partner-Customer-Ref: sbx_price_changed on a booking call has produced it since 12 September, with previousTotalCents and currentTotalCents, and X-Sandbox-Trigger on the response as proof. /docs/authentication still described all six triggers as planned and unbuilt; they are built.
  • changedThe sandbox stores nothing, and /docs/sandbox now says so. Every response is generated from the request that asked for it, so a sandbox read is not a read-back: GET on a booking, an order or a search answers 200 with a freshly generated object and a new id — for an id that never existed as well, which is why 404 not_found cannot be provoked there. PATCH and load-more answer with a new searchId, and the listings are fixed sample rows that never contain what you just created. Build the read-back path /docs/idempotency asks for; just do not accept a green sandbox run as proof that it works.
  • addedThe error envelope declares eleven fields it was already sending: violations on a failed body validation (the only field that names the offending property — a 400 message does not), missingScopes and requiredScopes, availableCents, requiredCents and currency on insufficient_funds, previousTotalCents and currentTotalCents on price_changed, accountStatus on account_not_active, service on both upstream types, and knownTriggers on a mistyped sbx_ value. Nothing new is sent; they are typed for the first time, so a generated client stops discarding them. Read each one only under the error.type it belongs to — /reference/schemas names the condition per field.
v1

Production keys now follow a certification, and it is published

Until today the answer to "when can I create a live key" was to ask your Vacabee contact, and the criteria existed only in that conversation. They are now written down as twelve requirements at /docs/certification, and the burden of proof runs the other way round than you would expect: four of them we prove from our own request log, two we test by firing at your system, and six are yours because they happen where we cannot see. Nothing about how you call the API changed.

  • addedThe certification catalogue is published at /docs/certification: what each requirement asks for, which tier it is in, what a good proof looks like, the eight states a submission can be in, and what happens at re-certification. Read it before you build — eight of the twelve are satisfied by an integration that was going to be careful anyway, and the four that are not are much cheaper to plan for than to retrofit.
  • addedPassing a certification switches on your own ability to create production keys in the portal. It creates no key and reveals none, and no key is ever sent by e-mail — every vcb_live_ secret in existence was shown exactly once, in a browser, to somebody signed into your portal account. An approval is good for 365 days; when it lapses, every key you already hold keeps working and only issuing or rotating a new one is blocked.
  • addedThe six sandbox failure triggers now work. Setting X-Partner-Customer-Ref to sbx_offer_expired, sbx_price_changed, sbx_insufficient_funds, sbx_payment_failed, sbx_supplier_error or sbx_slow makes the sandbox produce that failure on demand and deterministically. A fired trigger is confirmed by X-Sandbox-Trigger on the response; a trigger sent to a route where it cannot apply lets the call through and answers X-Sandbox-Trigger-Ignored; any other sbx_ value is refused with 400 invalid_request listing all six. Live keys are unaffected — no header makes a live call fail on purpose.
  • changedThe sandbox and overview pages said the triggers were specified but not implemented. That was true when it was written and is not any more, so the wording is withdrawn rather than quietly edited: if you built your own stub to simulate these failures, you no longer need it, and R2_ERROR_HANDLING of the certification expects each of the six to have been provoked and survived in your sandbox traffic.
v1

The API reference is published, and the TypeScript SDK is documented

Until today the only complete description of the endpoints lived in the gateway's OpenAPI document, which nobody outside the team could read. Every operation now has a page under /reference, generated from that document rather than written next to it. The TypeScript SDK is documented alongside it at /docs/sdk — read it now, but note that the package is not on the registry yet and no version of it has ever been uploaded; the entry announcing the first installable version is still to come. Nothing about the API itself changed — this is the surface becoming visible, not moving.

  • addedThe API reference at /reference covers all 43 operations, grouped by vertical. Each page carries the method and path, the parameters, the request and response schemas with types and required fields, the required scope, the rate-limit class, whether the call consumes a search, whether an Idempotency-Key is mandatory, and the shared error and response-header tables. Every one of those values is derived from the OpenAPI document at build time, so a page cannot describe a route the gateway does not have.
  • addedThe OpenAPI document now states the scope, the rate-limit class, the billability and the idempotency requirement for every operation, machine-readably, as x-required-scopes, x-rate-limit-class, x-billable-search and x-idempotency. Those four rules have been in force exactly like this the whole time — until now they were only readable in the gateway's source. Additive and descriptive only: no call changes its behaviour, no field moves, no status code differs.
  • addedThe TypeScript SDK is documented at /docs/sdk, ahead of its release. The package will be called @vacabeedev/partner-api and will carry version 0.10.0: it picks the host from your key prefix, derives the Idempotency-Key from your own order reference so a retry cannot become a second booking, exposes the quota headers, and verifies webhook signatures in constant time. It has no runtime dependencies. Read the page now — nothing in it changes when the package lands — but do not run the install yet: nothing has been uploaded to GitHub Packages, so npm install answers 404 no matter how correct your token is. When it does land, installing needs a GitHub account and a personal access token with read:packages, because GitHub Packages never serves npm packages anonymously — not even public ones; /docs/sdk gives the .npmrc you need. Publishing to npmjs.com, which would need neither, is planned for general availability.
  • changedThe SDK version follows the contract from now on. Every entry in the API changelog carries the version it was published under, and the classification decides the number: additive is a minor release, a changed answer or a changed shape is a major one, an SDK-only improvement is a patch. The package is still on 0.x, so pin an exact version. The version string the client sends as its user agent is generated from the package version rather than typed in a second time.
v1

Documentation corrected: six things we described but never shipped

An audit against the running code found six places where these docs described behaviour that does not exist. They are corrected below rather than quietly edited away, because anyone who has already built against them needs to know which parts of their integration are aimed at nothing. No API behaviour changed with this entry — only what we claim about it.

  • removedSettlement mode B (`settlement: "vacabee"`, Vacabee as merchant of record) was documented as callable. It is not. No booking endpoint returns the `checkoutUrl` this site promised, no hosted checkout exists behind one, and every key books in mode A today — including keys on accounts whose `settlementModes` lists `vacabee`. The mode B design stays published as a plan, clearly marked. If you built a payment handoff around `checkoutUrl`, it is waiting on a field that has never been sent.
  • changedThe settlement mode is a property of the API key, not a field on the booking request. The old wording ("the settlement mode you pick per booking") was wrong in a way that mattered: the mode decides whether the API quotes you net or retail prices, and the price is fixed at search time, so a per-booking field could not have governed it. Leave `settlement` out of booking bodies: nothing reads it as an instruction, and an unknown property can fail the whole request. The field is planned as an optional assertion checked against the key — a wrong-credential safety catch, not a switch — and the changelog will say when it is accepted.
  • removedThe `await_rates` blocking search mode was not something a caller could ever request. The gateway sets it on the upstream call unconditionally, and the request validator rejects unknown body properties, so sending `await_rates` yourself returns 400. Searches are consumed by polling.
  • removedThe streaming search mode over a WebSocket at `/v1/stream` does not exist — there is no such endpoint, and no connection or subscription limits apply because there is nothing to connect to. The `Sec-WebSocket-Protocol: vcb, <key>` handshake documented on the authentication page is withdrawn with it.
  • changed`GET /v1/ping` is public and requires no key; it never validated one. It was documented as the way to confirm your credentials, which meant a broken key looked fine. Use `GET /v1/account` to prove a key — it returns your identity, scopes, settlement modes and limits in one call.
  • deprecatedThe six sandbox triggers (sbx_offer_expired, sbx_payment_failed, sbx_supplier_error, sbx_price_changed, sbx_insufficient_funds, sbx_slow) are specified but not implemented. Sending one in X-Partner-Customer-Ref today is treated as an ordinary traveller reference and the call proceeds normally. The names and meanings are fixed and stay published so you can write handling against them; the go-live checklist no longer asks you to verify them. Implementation is planned.
  • deprecatedKeys are not bound to a host. We stated that a test key is rejected on the live host and a live key on the sandbox host, with a 401. Neither check exists: a key is validated against its own recorded environment and nothing compares it to the host it arrived on. Host binding is planned. Until it ships, assert the vcb_live_ / vcb_test_ prefix against your configured base URL yourself.
v1

Developer portal and v1 conventions

The portal goes live with the documentation and the cross-cutting rules of the API. The endpoints themselves are still in development — nothing is callable yet, and no key issued today can book anything.

  • addedPublic documentation covering authentication, idempotency, search quota, settlement modes, webhooks, the sandbox and the error envelope.
  • addedThe v1 conventions are now fixed: an Idempotency-Key on every POST, the { error: { type, message, requestId, docUrl } } envelope, X-Request-Id on every response, the X-Search-Quota-* headers, integer minor units with an ISO 4217 currency, and cursor pagination.
  • addedWebhook signatures are defined: X-Vacabee-Signature with t and v1, HMAC-SHA256 over `${t}.${rawBody}`, a five-minute timestamp tolerance and a rotatable secret per endpoint.
  • addedThe sandbox contract is defined, including the six forced failures sbx_offer_expired, sbx_payment_failed, sbx_supplier_error, sbx_price_changed, sbx_insufficient_funds and sbx_slow.
  • addedThe deprecation policy below now applies to everything published on this site.