Integrating

TypeScript SDK

Everything the other pages of this documentation ask you to get right — the host that belongs to your key, an Idempotency-Key that survives a retry, the quota headers, the constant-time signature check on your webhook endpoint — the SDK already does. It is thin on purpose: typed calls over fetch, no runtime dependencies, and the same /v1 underneath.

Installing

Today the SDK is not publicly installable — and you do not need it

@vacabeedev/partner-api 0.11.0 (published 16 September 2026) exists only on GitHub Packages. GitHub Packages never serves npm packages anonymously — not even public ones. And we issue no GitHub credentials as part of the partner relationship. So unless you already have a GitHub account with read:packages access to the vacabeedev organisation, npm install cannot succeed — and the right move is not to chase the token but to integrate over plain HTTPS against /v1. Every operation in the reference carries a curl example beside the TypeScript one, and the API is the same either way: the SDK saves you work, it unlocks nothing. Publication to npmjs.com, where neither a token nor a GitHub account is involved, is planned for general availability; the changelog will announce it.

If you do have that access, two steps. The first is an .npmrc that points this one scope at GitHub Packages. The package name is @vacabeedev/partner-api.

.npmrc — only the @vacabeedev scope is redirected
# .npmrc — next to your package.json
@vacabeedev:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}

Keep the token out of the file itself: npm expands ${GITHUB_TOKEN} from the environment, so the same .npmrc can be committed and your build server can inject its own token. Everything else in your project keeps resolving through npmjs.com as before.

Shell
npm install @vacabeedev/partner-api

If the install fails

Read the status code and the host in npm's own error line before you change anything: the registry distinguishes all four cases, and three of them are not about your token.

What the registry answersWhat it means
401 authentication token not providednpm sent no token at all: the .npmrc is missing or not being read, or GITHUB_TOKEN is not in the environment npm runs in. Nothing here says the package or the token is wrong.
403 permission_deniedOften worded “The token provided does not match expected scopes”. The token reached GitHub and is valid; it carries no read:packages scope, or the account behind it has no access to the vacabeedev organisation. This is the answer you will see most often — not a 404.
404 from registry.npmjs.orgThe @vacabeedev scope was not redirected, so npm asked the wrong registry. Fix the .npmrc. It says nothing about the package.
404 from npm.pkg.github.comThat version was never published. 0.11.0 exists (16 September 2026); a later one exists only once the changelog announces it. No amount of fixing your token changes this answer — a version that has not been through a release does not exist in the registry.

The first call

One import, one option. The key is the only thing the client needs, and it is also the only thing that decides which environment you are talking to.

TypeScript
import { VacabeeClient } from '@vacabeedev/partner-api';

const vacabee = new VacabeeClient({ apiKey: process.env.VACABEE_API_KEY! });

const account = await vacabee.account();
console.log(account.partner.slug, account.scopes);

The host is not something you configure. The SDK reads the prefix — vcb_test_ goes to the sandbox host, vcb_live_ to the live one — and picks the base URL itself. That removes the most common mix-up of the whole integration: you cannot point a test key at production by leaving a stale URL in your config, because there is no URL in your config. Passing baseUrl explicitly is still possible for a local stub, and it never switches environments silently.

GET /v1/account is deliberately the example. It is the call that proves a key: it returns your partner identity, your scopes, your settlement modes and your limits in one answer.

The typed verticals

Each bookable vertical hangs off the client as its own namespace, with the request and response types generated from the same OpenAPI document the reference is built from. A field that changes shape breaks your build rather than your booking. That document is downloadable — openapi.json — so you can generate your own client in any language and be on the same types without this package.

TypeScript
// Hotels: search is a session — open it, read it, refine it, book a rate.
const search = await vacabee.hotels.search({ /* … */ });
const booking = await vacabee.hotels.book({
  rateId,
  externalReference: `order-${orderNumber}`, // your own order number
  guests,
  contact: { email: 'ada@example.com' },
});

// Flights and transfers: synchronous search, then book.
const offers = await vacabee.flights.search({ /* … */ });
const transfer = await vacabee.transfers.search({ /* … */ });

// eSIM: catalogue at your purchase prices, orders, usage.
const plans = await vacabee.esim.plans({ /* … */ });
NamespaceWhat it covers
vacabee.hotelsDestinations, search as a session (open, read, refine, load more), book, read a booking, cancel.
vacabee.flightsSynchronous search, re-price a single offer, book, read an order.
vacabee.transfersSynchronous search, book, read an order, cancellation quote, cancel.
vacabee.esimCountries and regions, plans at your purchase prices, orders and usage.
vacabee.webhooksYour delivery targets: create, change, rotate the secret, send a test.
vacabee.deliveriesThe delivery log: what went out, what came back, what to retry.

The booking methods derive the Idempotency-Key from your externalReference, so the same order number cannot become two bookings — not across retries, not across processes, not across days. Pass { idempotencyKey } yourself only if you deduplicate on something else. The rules behind that are on the idempotency page; the SDK is what saves you from implementing them.

Seeing the quota coming

Every response carries the X-Search-Quota-* headers. Reading them by hand means threading them through every call site, so the client offers two ways instead: a callback that fires on every response, and a per-call variant that hands you the parsed values alongside the data.

TypeScript
const vacabee = new VacabeeClient({
  apiKey: process.env.VACABEE_API_KEY!,
  onQuota: (q) => {
    // The free daily SEARCH allowance. Not `remaining` — that one is the
    // rate limit, a different number that runs out for a different reason.
    if (q.freeSearchesRemaining !== undefined && q.freeSearchesRemaining < 50) {
      logger.warn('Free searches are running low today', q);
    }
  },
});

// Or per call:
const { data, quota, requestId } = await vacabee.accountWithResponse();

Two different numbers, one object

The Quota object carries both allowances and they are not interchangeable. freeSearchesRemaining comes from X-Search-Quota-Remaining and is the free daily search allowance — the one that costs money once it is gone. limit, remaining and resetAt come from the X-RateLimit-* headers and are how fast you may call, which refills within the hour and costs nothing. Alerting on remaining when you meant the searches is the easy mistake here, and it is quiet: the alert fires, just for the wrong reason and at the wrong time.

accountWithResponse() returns { data, quota, requestId }. Every namespace method has the plain form; the onQuota callback is the one to reach for when you want a single place that watches the allowance. What counts as a billable search — and what does not — is on the search quota page.

Receiving webhooks

Your endpoint is public, so the signature is the only thing that tells a genuine delivery apart from an invented one. verifyWebhook is the primitive: it compares in constant time, rejects deliveries older than five minutes, and returns the parsed event.

TypeScript — the primitive
import { verifyWebhook } from '@vacabeedev/partner-api';

app.post('/hooks/vacabee', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const event = verifyWebhook({
      payload: req.body.toString('utf8'),   // the RAW body
      signature: req.header('X-Vacabee-Signature')!,
      secret: process.env.VACABEE_WEBHOOK_SECRET!,
    });
    handle(event);
    res.sendStatus(200);
  } catch {
    res.sendStatus(400);
  }
});

The raw body, always

What is signed is what came over the wire. JSON.parse followed by JSON.stringify almost always produces a different string and therefore a different signature. In Express that means express.raw(…), never express.json(…).

One level up sits webhookHandler, and it is the layer most integrations actually want: it reads the header, verifies, parses, routes by event type and deduplicates. That last part matters — a delivery we do not see a 2xx for is retried, so the same event can legitimately arrive twice.

TypeScript — the handler
import { webhookHandler, memoryDeduplicator } from '@vacabeedev/partner-api';

const hook = webhookHandler({
  secret: process.env.VACABEE_WEBHOOK_SECRET!,
  seen: memoryDeduplicator(),        // survives duplicates, not restarts
  on: {
    'booking.confirmed': async (event) => queue.add(event),
  },
});

app.post('/hooks/vacabee', express.raw({ type: 'application/json' }), hook.express);
  • hook.express, hook.fetch (anything with Request/Response — Cloudflare Workers, Bun, Deno) and hook.node for Node without a framework. Same logic, three transports.
  • memoryDeduplicator() survives duplicates, not restarts. Across several instances, hand in a seen backed by Redis or your database.
  • respondFirst: true answers 200 and then runs your handler. We wait ten seconds; a handler that books, emails and only then returns exceeds that, so we count the delivery as failed and retry work that was long since done. The price is honest: a crash after answering is lost, because we saw a 2xx.
  • onError fires on a bad signature, on a handler that threw, and on an event type you have no handler for. Unknown types are acknowledged with 200 and never thrown — otherwise we could break your receiver simply by adding an event.
  • If none of the three adapters fit — a queue consumer, a test, another framework — receiveWebhook(options, rawBody, headers) is the same logic without the transport. It returns { status, body, reason }, and reason is worth logging: body-already-parsed is the most common webhook bug there is, and it looks exactly like a wrong secret.

Webhook verification needs node:crypto

The client itself — requests, retries, idempotency, errors, quota — is nothing but fetch and runs anywhere. The verification layer uses node:crypto and Buffer: fine on Node and Bun, fine on Deno through its node: compatibility layer, and on Cloudflare Workers it needs compatibility_flags: ["nodejs_compat"]. In a browser it does not run, and a signing secret does not belong there anyway. Moving that layer to WebCrypto is planned.

Versioning

The SDK version follows the contract, not the calendar. Every changelog entry that moves the SDK names the version it ships under, and the classification decides the number: additive is a minor release, a changed answer or a changed shape is a major one, and an SDK-only improvement is a patch. CI refuses a publish whose version disagrees with the changelog or already exists in the registry.

Below 1.0, pin an exact version

The package is still on 0.x, and that is not a formality: until 1.0 a minor release may still change the shape of something. Pin the exact version and read the changelog before you move it. Partner admins are emailed for every entry that changes the API surface, so there is nothing to poll.

The version the client is running travels with every request as its user agent, which is how support can tell from a log line which build produced a call. That string is generated from the package version — it is never typed in twice, so it cannot lie.

  • Per-operation snippets are on each page of the API reference, next to the curl equivalent.
  • A route the typed layer does not cover yet is still reachable: vacabee.request(method, path, body, options) is the untyped escape hatch, and there you set the idempotency key yourself.
  • The generated half of the SDK is regenerated from the OpenAPI document on every CI run and the result compared byte for byte, and the document itself is regenerated from the gateway's own routes and compared the same way. Both run before a merge, so an SDK cannot go stale against the contract without the build going red.
NextErrors and versioningThe error envelope, retry rules and how /v1 evolves.