Integrating

Certification

What has to be true before you can create production keys — and how little of it you have to prove yourself. It is certified per vertical: hotels, flights, transfers and eSIM are reviewed separately, and passing one opens production keys for that vertical's scopes only. Most of each list we read out of our own request log, two of them we test by firing at your system, and only what happens where we cannot see is left for you to show us.

This applies to API partners only

Certification is about an integration you wrote. If you sell through a Vacabee whitelabel app, the code is ours and there is nothing of yours to certify — you will not see a certification page in your portal, and the endpoints behind it refuse a whitelabel partner rather than showing a form that could never be completed.

What passing actually unlocks

Production keys are minted by you, in the portal, on the API keys screen. That button is switched off until a certification has passed. Passing switches it on — and that is the whole of it.

No key is ever created or sent here

A pass records a verdict on one vertical. It does not create a key, it does not reveal one, and no key is ever sent by e-mail — not on approval, not on request, not as an exception. Every vcb_live_ secret in existence was shown exactly once, in your browser, to somebody logged into your portal account. If a message claiming to be from us contains a key, it is not from us.
  • You certify with a test key. The whole procedure runs against https://sandbox-api.vacabee.com with a vcb_test_ key. Nothing is billed, no supplier is contacted, and your ledger balance is untouched — see the sandbox page.
  • It does not expire. A pass stands until somebody withdraws it. There is no renewal date to diarise and no anniversary on which your access quietly stops — an expiry is an outage scheduled in advance, for a reason unconnected to anything you did.
  • Withdrawal never stops your traffic. If a certification is ever withdrawn — by a person, with a written reason — the gate governs ISSUING a key, never USING one. Every production key you already hold keeps working and your customers notice nothing; what you lose is the ability to mint or rotate another one.
  • It is certified one vertical at a time. Hotels, flights, transfers and eSIM each have their own list, their own submission and their own verdict. Passing hotels opens hotels:search and hotels:book on a production key and refuses flights:book by name until flights has passed too — see the next section.
  • The list narrows by how you sell, not by what you sell. Five of the fifteen entries depend on your settlement mode, and the two webhook probes only appear once you have an endpoint to fire at. Nothing narrows by vertical: each vertical gets the whole list, and it is answered for that vertical — what a traveller was shown before he paid is a different screenshot for a hotel than for an eSIM.

One certification per vertical

There is no single verdict that makes you “certified”. Hotels, flights, transfers and eSIM are certified independently — separate attempts, separate evidence, separate decisions — and each pass unlocks exactly its own scopes on a production key. A key that asks for a scope in a vertical you have not passed is refused when you create it, naming the vertical and what to do about it. Your sandbox keys are never affected: that is where the integration is written, before any of this.

VerticalWhat passing it unlocks on a production key
hotelshotels:search, hotels:book
flightsflights:search, flights:book
transferstransfers:search, transfers:book
esimesim:read, esim:order

Six scopes belong to no vertical, so they are not certified. They are governed by your account instead, in two groups, and the split is about whose data they reach:

ScopesWhat they need
account:read, ledger:read, webhooks:manageYour own account approval, and nothing else. Your name, your statement, your webhook endpoint — no traveller and no supplier is involved, so waiting on a vertical review would be a gate with no risk behind it.
bookings:read, bookings:cancel, stream:subscribeAt least one certified vertical. They reach bookings, and a booking always belongs to a vertical: these carry passenger names, dates of birth and document numbers, and a cancellation is a write against a supplier's reservation.

bookings:cancel is not narrowed per vertical

Cancelling a hotel booking is a hotel action, and the scope cannot say so: narrowing it would mean splitting it into four scope names, which is a breaking change to a published contract. It needs one certified vertical rather than the booking's own, and in practice the reach is bounded by what you could create — with no hotels:book there is no hotel booking of yours to cancel.

Your account also carries one coarse approval on top of all of this — whether it may have production keys AT ALL. The first vertical you pass grants it. It is necessary and never sufficient: with it and no certified vertical, the only production key you can create is one carrying the three account scopes above.

The full catalogue

Fifteen entries, in three tiers, and EVERY VERTICAL GETS THE WHOLE LIST. The tier is not a label on the work — it is who does it. You will not see all fifteen: five of them depend on how you sell, so a partner who is merchant of record and a partner who uses our hosted checkout get different lists, and the webhook probes only appear once you have an endpoint to fire at. Your own screen shows exactly yours, and nothing else in this procedure is conditional.

The four AUTO entries read the same in every vertical

Our request log records your traffic per ACCOUNT, not per vertical: idempotency headers, error branching, backoff and route coverage are properties of your HTTP client rather than of one product. So those four rows carry the same finding in each vertical's list, and a tick on them is not a statement about that vertical in particular. The eleven below them are, and they are answered per vertical — the same question about a hotel and about an eSIM has two different answers.
TierWho proves itWhat you do
AUTOWe do, from our own request log.Nothing. It is already ticked when you open the screen, with the request ids it was ticked from.
PROBEWe do, by firing at your system.Register a webhook endpoint and press the button.
EVIDENCEYou do — it happens where we cannot see.Write it down, and attach a screenshot, a recording or a document.
KeyRequirementTier
R1_IDEMPOTENCYRepeating a booking request never buys twice.AUTO
R2_ERROR_HANDLINGAll six sandbox failures were provoked AND survived.AUTO
R3_RETRY_DISCIPLINETerminal errors are not retried.AUTO
R4_SURFACE_COVERAGEEvery route you plan to use in production was exercised in the sandbox — once successfully and once failing.AUTO
R5_WEBHOOK_SIGNATUREDeliveries with a wrong signature are rejected.PROBE
R6_DELIVERY_DEDUPThe same delivery twice has no second effect.PROBE
R7_PRICE_DISPLAYThe price the customer sees is the price the API quoted.EVIDENCE
R7B_HOSTED_HANDOFFHosted checkout only: the traveller knows he is about to pay us, and coming back after abandoning does not leave a confirmed-looking order.EVIDENCE
R8_CANCELLATION_TERMSRefundability is visible before the booking, penalties before the cancellation.EVIDENCE
R8B_CONFIRMATION_DELIVERYThe traveller receives, without asking, a reference, what was booked, and how to reach support.EVIDENCE
R9_BALANCE_MONITORINGMerchant of record only: you reconcile settlement.funds and somebody is alerted when a hold never resolves.EVIDENCE
R10_PERSONAL_DATATraveller data: what is stored, where, for how long, and who deletes it.EVIDENCE
R11_KEY_HYGIENEThe production key is not in client code, and someone owns its rotation.EVIDENCE
R12_ESCALATION_CONTACTA traveller in trouble reaches a human, and you reach us with the requestId.EVIDENCE
R13_GO_LIVE_PLANDate, volume, which verticals first, and how you would turn it off again.EVIDENCE

Two entries can be waived by a reviewer, and only two: R4_SURFACE_COVERAGE and R13_GO_LIVE_PLAN. A waiver always carries a written reason, because a growing list of waivers is visible and a growing silence is not. Everything else cannot be waived at all — including R8_CANCELLATION_TERMS, which is the entry a reviewer would most like to be able to wave through on a Friday and the one whose absence a traveller finds out about at an airport.

What we see ourselves — the four you do not submit

Every call you make against the sandbox is already logged with its route, its status, your idempotency key and — this is the part that makes the rest work — which failure trigger it carried. So four of them are answered from traffic you were going to produce anyway. You do not upload anything for these; you open the screen and they are already ticked, each with the request ids behind it.

EntryWhat the log has to show
R1_IDEMPOTENCYThree things, not one: one Idempotency-Key that came back successful TWICE — only a stored replay can do that, because a key is executed once — a 409 on a repeated key that your integration carried on from rather than hammered, and an Idempotency-Key on every single state-changing request. What the log does NOT hold is the request body, so bodies are never compared here — only keys are.
R2_ERROR_HANDLINGEach of the six triggers observed, confirmed by the X-Sandbox-Trigger response header — AND a successful call on the same route afterwards. Provoking an error and stopping proves that you can send a header.
R3_RETRY_DISCIPLINENo key that received the same terminal status twice — 400, 402 or 410. Those say: THIS request will never succeed, change something first. 409 is deliberately NOT counted, in either direction: the log stores a status and not an error type, and idempotency_key_reuse (repeating it is a loop) and idempotency_key_in_progress (repeating it is CORRECT) are the same 409. Runs of 409 are reported to the reviewer as an observation and never as a violation.
R4_SURFACE_COVERAGEEvery route you declared for production, seen at least once with a success and at least once with a failure. A route that has only ever returned 200 has an untested error branch, and the error branch is the one that runs on a bad day.

A retry with a FRESH key is invisible to us — and it is the one that double-books

Everything above is read out of the log, and the log is grouped by idempotency key. A client that answers a timeout by sending the same booking again under a NEW key produces two unrelated rows, two successes and two hotel rooms, and no signal here can tell that from two ordinary bookings. R1_IDEMPOTENCY therefore proves that you CAN repeat safely; it cannot prove that your timeout path DOES. Nor does it count bookings: the third signal is "every state-changing request carried a key", not "nothing was bought twice". That one is yours, and the way to test it is sbx_slow plus a client timeout below the response time.

You declare the routes. R4_SURFACE_COVERAGE is measured against the list of routes you say you will call in production, not against every operation in the contract. A narrow integration produces a short list and a short list is easy to cover; declaring routes you do not use only makes work for yourself.

The six failure triggers are values of the request header X-Partner-Customer-Ref, and they are the whole reason R2_ERROR_HANDLING is answerable at all. In the sandbox this header is how you ask for a specific failure; a value that does not begin with sbx_ is an ordinary traveller reference and is passed through untouched. Against a vcb_live_ key nothing of this applies — no header makes a live call fail on purpose.

TriggerWhat comes backWhere it fires
sbx_offer_expired410 offer_expiredBooking routes
sbx_price_changed409 price_changed, with both amountsBooking routes
sbx_insufficient_funds402 insufficient_funds, before any supplier is contactedBooking routes
sbx_payment_failed403 account_not_activeBooking routes
sbx_supplier_error502 upstream_error, naming the serviceEvery route
sbx_slowNo error — the answer is deliberately late.Every route
Shell — provoking a price change on a hotel booking
curl -sS -D - "https://sandbox-api.vacabee.com/v1/hotels/bookings" \
  -H "Authorization: Bearer $VACABEE_TEST_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
#
# The header is the proof. R2_ERROR_HANDLING counts a trigger as observed only
# when it is on the response, so a call that did not actually simulate anything
# cannot be mistaken for one that did.
  • A trigger on the wrong kind of route is announced, not swallowed. Four of the six only make sense where money moves, so on a search or a read they let the call proceed and the response carries X-Sandbox-Trigger-Ignored instead. That header is your signal to move the trigger to a booking call — silence would look like a broken feature.
  • 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. sbx_offer_expred would otherwise behave exactly like a working trigger that decided to do nothing.
  • sbx_slow is the one worth real time. Combine it with a short client timeout and a retry that reuses the same idempotency key: that is the situation — a booking that may or may not have happened — which produces duplicate reservations in integrations that skipped it, and it is what 409 idempotency_key_in_progress exists for.

The proof is frozen when you submit

Request logs are kept for 30 days and then purged. So a submission stores a COPY of what the log said, not a link into it — otherwise a review three weeks later would read an empty window and conclude you had tested nothing. The window it was computed over, and the date after which it can no longer be reproduced, are printed on the submission.

What we test against your system

Webhook receiving is the one thing that is neither visible to us nor genuinely invisible: it happens on your server, but we can knock on it. So we do, rather than asking you to declare it. The failure this catches is not somebody who knows he skipped signature verification — it is an integration that reads the signature header, logs it, and processes the body regardless, run by people who will tell you sincerely that they verify.

We will send you a deliberately wrong signature

Announced here so that nobody spends an evening on it: when you run the R5_WEBHOOK_SIGNATURE check, one delivery arrives with a signature that is correctly shaped, correctly timed and cryptographically WRONG. It is us. Your endpoint is expected to refuse it with any non-2xx. It is sent only to a webhook endpoint registered in the TEST environment — never to a production receiver.
EntryWhat we sendWhat passes
R5_WEBHOOK_SIGNATUREA correctly signed delivery, then one whose HMAC does not match.2xx for the valid one and a non-2xx for the invalid one. The valid delivery is there to make the invalid one mean something: an endpoint that is simply down would otherwise "pass" by rejecting everything.
R6_DELIVERY_DEDUPThe same x-vacabee-delivery id a second time.The repeat is accepted without error. We see your HTTP status, not your database — so this proves the weaker, still worthwhile thing: your receiver does not fall over on a duplicate. That the duplicate changes nothing is the half you describe yourself.
  • A probe that cannot run is a failure. No endpoint registered, endpoint disabled, DNS does not resolve, TLS handshake fails, connection refused, timeout — all of it fails the entry, and none of it is recorded as skipped. A check that silently did not run looks exactly like a check that passed.
  • Every probe is in your own delivery history. Request, response, status and timing are on the delivery like any other, so you can check a finding instead of having to believe it. Signature verification itself is described on the webhooks page.

What you hand in

Six entries are about things we have no way of seeing: your screens, your storage, your people. Each one asks for something specific, and the difference between an accepted answer and a rejected one is almost always whether it names a person and a number instead of a department and an intention.

EntryWhat a good proof looks like
R7_PRICE_DISPLAYA short screen recording of one booking: the amount the API quoted and the amount on your checkout page side by side, in the END CUSTOMER'S currency — plus the price_changed case, where the new price is shown and confirmed BEFORE the booking, never after. A single screenshot of a confirmation page cannot show that the price was the same one call earlier, which is why this one is usually a recording.
R8_CANCELLATION_TERMSA screenshot of the selected rate or fare with its refundability visible before purchase, and — for transfers — the cancellation quote with penalty, refund and currency shown before the cancellation is confirmed. Sandbox rate 2 of every hotel is non-refundable and the transfer cancellation quote carries a 25 % penalty precisely so that you can screenshot both.
R9_BALANCE_MONITORINGHow the ledger balance is watched, at what threshold an alert fires, and WHO receives it — a name and a channel. "The team is notified" is not an answer; it is the reason nobody looked. A monitor screenshot with the threshold visible plus the on-call name is enough.
R10_PERSONAL_DATAThe data categories you store from a booking, the storage location and region, the retention period, and a named contact for deletion requests. We never see your end customer, so a deletion request that has nowhere to go becomes everyone's problem when a regulator asks.
R11_KEY_HYGIENEA statement that vcb_live_ keys live only server-side and in no repository, a NAMED person responsible for rotation, and a yes/no on the IP allowlist. The allowlist is the one control that makes a leaked key useless, and it is off unless you ask for it.
R12_ESCALATION_CONTACTA named contact on a channel that is answered outside office hours, and the go-live window you are aiming at. The second one so that you do not switch on real traffic during a release freeze — which we have watched happen, and which looks from the outside like our outage.

Files are stored privately and are only ever served through short-lived signed links — never a public URL. The portal names the accepted formats and the size limit next to each entry.

The states, and what each one means

StateWhat it meansWhose move
DRAFTOpen, editable, nothing has been handed in. The automatic entries are already computed and visible.Yours
SUBMITTEDHanded in. The catalogue and the evidence are frozen at this moment; the automatic and probe entries are decided on the spot.Ours
IN_REVIEWA reviewer has picked it up and is judging the entries that are left.Ours
CHANGES_REQUESTEDSent back with a reason. You edit the SAME attempt and resubmit — a round of corrections is not a failed certification.Yours
PASSEDEvery entry carries a verdict and none of them is a failure. You can create production keys from that moment on, in the portal — nothing is mailed to you.
FAILEDRejected. This attempt stays readable forever; the next try is a new attempt, numbered.Yours
REVOKEDWithdrawn by a person, with a written reason. Keys you already hold keep working; no new one can be created until you are certified again.Yours

A rejection always names the entry and why

A verdict of anything other than a pass is refused without a written reason — on the individual entry AND on the submission as a whole. "Not yet approved" with nothing attached is not a state this procedure can produce, so you never have to write in and ask what was meant. The same holds for a waiver: it is only possible on the two waivable entries, and it always carries the reason it was granted.

Only one certification is open at a time. A new attempt can be started once the previous one has ended — passed, failed or revoked — because two open submissions could both reach a reviewer and the second one would set an approval the first reviewer never agreed to.

How it runs

  1. 1Build against the sandbox with a vcb_test_ key, reading this catalogue as you go. The four automatic entries fill themselves in from that traffic.
  2. 2Declare what you are certifying: the verticals you sell, the routes you will call in production, and the webhook endpoint we should probe.
  3. 3Provoke the six failures and recover from each of them, on the routes you declared. This is the one part that does not happen by itself.
  4. 4Run the probes from the certification screen and fix anything they find. You can run them as often as you like before submitting.
  5. 5Answer the six evidence entries and attach what belongs to them. Answers are saved as you go — nobody finishes a catalogue this long in one sitting.
  6. 6Submit. The catalogue as it reads today and the evidence as the log sees it today are frozen onto the submission, so the review judges what you handed in rather than what either of them has become since.
  7. 7A reviewer decides each remaining entry and then the submission: passed, sent back for changes, or failed — each with a reason.
  8. 8Create your production key yourself on the API keys screen. Plan a supervised first day: the response shapes are identical to the sandbox, the behaviour is not.

And then it stands. There is no re-certification on a timer. If your integration changes materially — a new vertical, a change of settlement mode, a rebuild of the checkout — tell your Vacabee contact and we will look at the parts that changed rather than at all of it again. A certification is withdrawn only by a person, only with a written reason, and you are told when it happens.

What the sandbox cannot prove — and what you still need

Three situations cannot be produced in the sandbox at all, because the sandbox deliberately touches no money and no supplier. They are not quietly dropped from the catalogue: they are the legitimate reason to waive R4_SURFACE_COVERAGE, the waiver has to say which of the three it is, and the report prints how many waivers there were.

Not reachable in the sandboxWhyWhat you must still have
429 search_quota_exceededSandbox calls are free at any volume, so the daily search counter never moves and a cap can never be reached.A branch that reads resetAt from the error and stops searching until then, rather than retrying into a wall. The quota headers are on every response and are your early warning.
A rate or offer that runs out on its ownThe sandbox never mints a real handle, so nothing ages out of the 20-minute window. Expiry is reachable there only by asking for it.Re-search and re-price on 410 offer_expired, at any point in the flow — including between the price your customer confirmed and the booking call.
settlement.funds other than capturedA sandbox transfer order always reports captured, because no money was ever held.All three branches of the field: captured, released and still_held. On still_held, poll the order — do not invoice.

These belong in the first supervised hours of production, not in the certification. Everything else in the catalogue is reachable before a single euro moves, which is the point: a gate that costs money is a gate that eventually gets skipped.

NextTypeScript SDKInstall it, make the first call, and let it handle keys, idempotency and webhook signatures.