Authentication

API keys

The API authenticates the partner, not the traveller. One credential — an API key — identifies your account, decides what you may call, and is the thing every invoice line traces back to.

Sending a key

Pass the key as a bearer token on every request. There is no session, no login call and no token exchange.

HTTP
GET /v1/account HTTP/1.1
Authorization: Bearer vcb_live_7Kq2xY…
Accept: application/json

There is one way in and it is this header. An earlier version of this page also documented passing the key in a WebSocket subprotocol for a streaming endpoint at /v1/stream. That endpoint does not exist, so neither does that handshake.

Keys never leave your servers

A key is a bearer credential for your whole partner account: it can search, book, cancel and spend against your credit. It does not belong in a browser bundle, a mobile app, a mobile config file, a public repository or a support ticket. If one leaks, revoke it in the portal — revocation takes effect in under a second.

Two environments

Live keyTest key
Prefixvcb_live_…vcb_test_…
Host to useLive base URLSandbox base URL — a preference, not a check: see below
SuppliersReal bookings, real moneyNever contacted
DataReal inventory and real pricesGenerated per request, ids prefixed sbx_
Your accountHolds and charges against your balanceNever touched
Search quotaCounted and billableFree, never counted
Forced failuresNot availableSix triggers planned, none built — see sandbox

Not enforced by the host yet

A key of the wrong kind is refused with wrong_environment, and the refusal names the base URL to send the call to instead of telling you to rotate a key that was never the problem. What decides is the kind of key each deployment is configured to accept — and today a single deployment accepts both, so the check has nothing to refuse. Separating the two hosts is planned; until that is live, a deployment of yours wired with the wrong key will not fail on its first call. Check the key prefix against the base URL in your own start-up configuration — that is the guarantee you are actually relying on.

Scopes

Every key carries an explicit list of scopes, and every endpoint requires one. A key with no scope for a route is rejected before the request reaches any supplier.

Scopes are named <resource>:<action>. There is no generic read or write action — the actions are per resource, and these 11 are the complete list the gateway enforces, with every route each one opens:

ScopeRoutes it opens
account:readGET /v1/accountGET /v1/account/quota
esim:orderPOST /v1/esim/orders
esim:readGET /v1/esim/countriesGET /v1/esim/regionsGET /v1/esim/plansGET /v1/esim/plans/{planId}GET /v1/esim/ordersGET /v1/esim/orders/{id}GET /v1/esim/orders/{id}/usage
flights:bookGET /v1/flights/ordersPOST /v1/flights/ordersGET /v1/flights/orders/{id}GET /v1/flights/orders/{id}/cancellation-quotePOST /v1/flights/orders/{id}/cancelPOST /v1/flights/orders/{id}/change-optionsPOST /v1/flights/orders/{id}/change
flights:searchPOST /v1/flights/searchesGET /v1/flights/offers/{id}
hotels:bookGET /v1/hotels/bookingsPOST /v1/hotels/bookingsGET /v1/hotels/bookings/{id}POST /v1/hotels/bookings/{id}/cancel
hotels:searchGET /v1/hotels/destinationsPOST /v1/hotels/searchesGET /v1/hotels/searches/{id}PATCH /v1/hotels/searches/{id}POST /v1/hotels/searches/{id}/load-moreGET /v1/hotels/{hotelId}/rates
ledger:readGET /v1/account/ledgerGET /v1/account/ledger/entries
transfers:bookGET /v1/transfers/ordersPOST /v1/transfers/ordersGET /v1/transfers/orders/{orderId}GET /v1/transfers/orders/{orderId}/cancellation-quotePOST /v1/transfers/orders/{orderId}/cancel
transfers:searchPOST /v1/transfers/searches
webhooks:manageGET /v1/webhooks/endpointsPOST /v1/webhooks/endpointsPATCH /v1/webhooks/endpoints/{id}DELETE /v1/webhooks/endpoints/{id}POST /v1/webhooks/endpoints/{id}/rotate-secretPOST /v1/webhooks/endpoints/{id}/testGET /v1/webhooks/deliveriesGET /v1/webhooks/deliveries/{id}POST /v1/webhooks/deliveries/{id}/retry

Where the key form and the gateway differ

The key form in the portal also offers bookings:read, bookings:cancel and stream:subscribe. They can be granted and they guard no route — a key holding them gains nothing. Conversely transfers:search and transfers:book are enforced by the gateway but missing from the form, so if you sell the products behind them, ask your Vacabee contact to have them added to your key.

The scopes actually granted to a key are shown on the key in the portal and returned by GET /v1/account. A rejected call names the one it was missing, so you never have to guess:

403 Forbidden
{
  "error": {
    "type": "insufficient_scope",
    "message": "The API key is missing the scope \"hotels:book\".",
    "requestId": "a4f1c358-6b2e-5acb-8ef5-0c3d0bcc35f5"
  }
}

Branch on insufficient_scope — that is the literal value; there is no missing_scope. requestId is a plain UUID with no prefix, and docUrl only appears when the deployment has a documentation base URL configured, so treat it as optional.

Grant the narrowest set that does the job — with one limitation worth designing around: reading a booking back needs the same …:book scope as creating or cancelling one. A reconciliation job that only reads therefore cannot be given a key that is unable to cancel. If that matters, keep the key on a system that cannot issue cancellations rather than expecting the scope to enforce it.

IP allow lists

Each key can be pinned to a list of source addresses or CIDR ranges. A request from anywhere else is refused regardless of how valid the key is. This is the cheapest mitigation there is for a leaked credential, and it costs nothing if your egress addresses are stable.

Leave the list empty and the key is accepted from anywhere. A list that would allow everything is refused rather than stored, so say it by leaving the list empty.

IPv4 ranges only

Single addresses work in both families, and IPv4 CIDR ranges work. An IPv6 range is accepted and displayed by the portal but matches nothing at request time — every call from it is refused with 403 ip_not_allowed, with no hint as to why. If your egress is IPv6, list the individual addresses.

If your egress is dynamic, an allow list is not the tool: keys have no expiry you can set at creation, so there are no short-lived keys to fall back on. Use a stable egress address, or leave the list empty and rely on keeping the secret out of reach.

Rotation

Keys are stored as a SHA-256 hash. We cannot show you an existing secret, recover one, or read one out of a log — the plaintext exists exactly once, in the response that created it. Store it in your secret manager at that moment.

  • Rotating a key issues a new secret and gives the old one an expiry in the near future, so both work while you roll out. Deploy the new secret, then let the old one lapse or revoke it early.
  • Do not use the last-used timestamp to decide that traffic has moved. The field is shown in the portal but nothing updates it, so a key that is still serving live traffic looks unused. Confirm the cutover from your own side — your logs, or the X-Request-Id of calls you know you made with the new secret — before you revoke anything.
  • Revocation is immediate: the gateway caches the key catalogue for 60 seconds but is invalidated on revoke, so a killed key stops working in under a second.
  • Use separate keys per system — checkout, back office, batch jobs. One compromised component then costs you one rotation, not a coordinated one across your estate.
  • Every key operation is recorded in an audit trail with the portal user who performed it. It is not exposed in the portal or over the API today, so if you need it for a compliance review, ask your Vacabee contact for an export rather than promising your auditor self-service access.

Live keys need an approval first

Creating a vcb_live_ key fails with 400 until your partner account has been approved for production keys. That approval is granted by hand on our side, so it is a lead time rather than a click: ask for it days before your launch date, not on the day. Test keys are unaffected.

Travellers

Your travellers do not have Vacabee accounts and never authenticate with us. There is no traveller resource on our side: we hold no per-traveller record you can address, and no identifier of yours is stored against one.

X-Partner-Customer-Ref is not read

An earlier version of this page said you identify travellers with a stable reference in the X-Partner-Customer-Ref header and that we keep the mapping. The header is allowed through CORS and read by nothing — sending it is silently ignored, which is exactly the kind of mistake that surfaces only when you rely on the mapping. There is also no DELETE /v1/customers/{ref}: that route does not exist, so do not put it in your own deletion process or in a data processing agreement.

What does tie your records to ours is externalReference on each booking or order: your own value, echoed back, and the thing to quote in support. What we store per booking is what the supplier needs — guest or passenger names and the contact details you send. If a traveller exercises a deletion request, raise it with your Vacabee contact; there is no self-service endpoint for it today.

NextIdempotencyWhy every POST needs an Idempotency-Key, and how replay works.