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

Base URLhttps://sandbox-api.vacabee.com
Keysvcb_test_…
CostFree at any volume — but the rate limiter applies unchanged, so a load test runs into 429 rate_limit_exceeded like any other traffic.
Search quotaNever counted
SuppliersNever contacted
LedgerNever touched — no holds, no charges, no 402

Create a test key in the portal.

The two hosts do not police each other

This page previously said that test keys are refused on the live host and live keys on the sandbox host. They are not: a key is checked against its own recorded environment, and nothing compares it to the host it arrived on. Pinning keys to a host is planned. Until then, crossing the environments is something your own configuration has to prevent — hold the two secrets separately and check the 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 live rate_ 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 back status: "complete" with rateStatus: "available", and a booking is always immediately CONFIRMED.
  • 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 different quoteIds — booking the wrong one is the most common integration mistake, so the sandbox makes it possible to get wrong. An order always comes back with settlement.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

Because nothing reaches a supplier and nothing touches your account, some of what you will meet on day one live simply does not occur here on its own: a search that is still 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.

Shell — asking for a price change on a hotel booking
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_changed
  • X-Sandbox-Trigger on 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-Ignored means 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 answers 400 invalid_request and lists all six in knownTriggers, because a silently ignored sbx_offer_expred would look exactly like a broken feature.
TriggerWhat happensWhere it firesWhat you must handle
sbx_offer_expired410 offer_expired — the rate or offer handle is treated as gone.Booking routesRe-run the search and re-price rather than showing a dead offer.
sbx_price_changed409 price_changed, carrying the previous and the current amount.Booking routesShow the new price and ask, or accept within your own tolerance.
sbx_insufficient_funds402 insufficient_funds before the supplier is contacted.Booking routesMode A only: alert, top up, and keep the traveller informed.
sbx_payment_failed403 account_not_active. The name and the answer do not line up — it is the account-level refusal, not a card decline.Booking routesStop booking, surface it to an operator: this one does not resolve by retrying.
sbx_supplier_error502 upstream_error, naming the service that failed.Every routeFail cleanly, release your own reservation, do not retry blindly.
sbx_slowNo error at all — the handler runs normally and answers late.Every routeYour timeout and retry path — with the same idempotency key.

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.

NextCertificationWhat has to be true before you can create production keys, and how much of it we prove for you.