Integrating
Sandbox
Its own keys, where every call is free, nothing reaches a supplier, nothing touches your account and nothing is ever billed. Build here first — and know what the data is, because it is generated, not borrowed from production.
Getting on it
Create a test key in the portal.
The two hosts do not police each other
vcb_live_ / vcb_test_ prefix on start-up.What the data is
Every sandbox response is generated inside the gateway from your own request. It is deterministic — the same request gives the same answer, which is what makes it usable in a test suite — and it follows the published response schemas field for field, which is what makes it usable for building your client. It is not a copy of production data and no number in it means anything.
- Ids. Everything is prefixed
sbx_—sbx_htl_…,sbx_rate_…,sbx_qte_…,sbx_plan_…. Never copy one into a fixture you also use against live, and do not pin a regex to the liverate_shape. - Hotels. A search returns exactly three generated properties named after the destination you asked for, with prices hashed from your criteria and always in
EUR. Rates are two per hotel, one refundable and one not, with cancellation windows that are fixed dates in 2026, so they will read as being in the past once that year is gone. The search always comes backstatus: "complete"withrateStatus: "available", and a booking is always immediatelyCONFIRMED. - Flights. Three generated offers from a made-up carrier (
SB, Sandbox Airways) with fixed departure times, each with exactly one slice. This is not the supplier's test mode — no Duffel call is made, so multi-slice and connection handling cannot be exercised here. - Transfers. Two offers per search, in
USD, with differentquoteIds — booking the wrong one is the most common integration mistake, so the sandbox makes it possible to get wrong. An order always comes back withsettlement.funds: "captured", and the cancellation quote is deliberately a partial refund. - eSIM. Countries and regions are a fixed short list, but plans are generated per country or region you ask for — including for a country that is not in the list, so do not read a plan coming back as proof that we cover that country. Prices are hashed and the currency arrives lower-case (
"usd"). Orders complete and produce a profile; nothing is provisioned on a real network.
What the sandbox cannot show you
partial and needs re-reading, a booking that stays PENDING, and a rate that ages out of its own accord. The failure triggers below cover most of that on demand — but a handle that times out naturally, a daily search cap and a transfer whose funds are still held cannot be produced here at all, and certification says so out loud rather than pretending otherwise. Write those paths from the error reference and test them against your own stub.Forcing failures
The paths that break in production are the ones a happy-path integration never exercises. Setting X-Partner-Customer-Ref to one of six reserved values makes the sandbox produce that failure on demand, deterministically. Any other value is an ordinary traveller reference and is passed through untouched, and nothing of this applies to a vcb_live_ key: no header makes a live call fail on purpose.
curl -sS -D - "https://sandbox-api.vacabee.com/v1/hotels/bookings" \
-H "Authorization: Bearer $VACABEE_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-Partner-Customer-Ref: sbx_price_changed" \
-H "Content-Type: application/json" \
-d '{
"rateId": "sbx_rate_…",
"externalReference": "booking-2026-0915-abc",
"guests": [ { "firstName": "Ada", "lastName": "Lovelace" } ],
"contact": { "email": "ada@example.com" }
}'
# HTTP/1.1 409 Conflict
# X-Sandbox-Trigger: sbx_price_changedX-Sandbox-Triggeron the response is the proof it fired. Read it rather than inferring from the status code — it is also what certification counts.X-Sandbox-Trigger-Ignoredmeans right trigger, wrong route. The first four below only make sense where money moves, so on a search or a read the call proceeds and carries this header instead. Move the trigger to a booking call.- A typo is refused, loudly. Any value starting with
sbx_that is not one of the six answers400 invalid_requestand lists all six inknownTriggers, because a silently ignoredsbx_offer_expredwould look exactly like a broken feature.
sbx_slow together with idempotency is the pairing worth spending real time on: it is the exact situation — a timed-out booking that may or may not have succeeded — that produces duplicate reservations in integrations that skipped it. Shorten your own client timeout, let it fire against a sbx_slow call and retry with the SAME idempotency key: a retry that lands while the first attempt is still in flight answers 409 idempotency_key_in_progress, which is the answer your code has to survive.
Before you switch to live
What you can verify here, and what certification will ask you to have shown:
- A booking retried after a timeout, with the same idempotency key, results in exactly one booking.
- Each of the six failure triggers was provoked once AND followed by a successful call on the same route. Provoking a failure and stopping proves only that you can send a header.
- Every error path in your code reads the { error: { type, message, requestId, docUrl } } envelope and branches on type, not on a parsed message string.
- Your webhook endpoint verifies signatures, rejects stale timestamps and survives the same event twice. Remember that a test delivery carries the type webhook.test and will not reach your booking.confirmed handler — exercise that with a sandbox booking.
- You store X-Request-Id for every call you make.
- Your rendering copes with all three shapes a cancellation policy arrives in: an array of windows on a rate, a single string on a confirmed hotel booking, and a conditions object on a flight offer.
- Your keys live in a secret manager, never in the repository or a client bundle.
- Your configuration asserts the key prefix matches the host you point it at — nothing on our side does this for you yet.
Then swap the host and the key — but do not expect that to be the whole change. The response shapes are identical, and that is what the sandbox is for. The behaviour is not: live you will meet partial searches, pending bookings, expired handles, an account that can run out of credit, real currencies other than the sandbox's EUR and USD, and ids that do not start with sbx_. Plan a supervised first day rather than a silent cutover.

