Integrating

Webhooks

Most of what happens to a booking happens after your request returns — a supplier confirms, a payment settles, a traveller's data allowance runs low. Webhooks are how you hear about it without polling.

Registering an endpoint

Add an HTTPS endpoint in the portal, or with POST /v1/webhooks/endpoints, and pick the events it should receive. Each endpoint gets its own signing secret, which you can rotate at any time. POST /v1/webhooks/endpoints/{id}/test sends a sample delivery so you can prove the whole path — TLS, signature check, handler — before anything real depends on it.

Events

EventFires when
booking.confirmedA booking is confirmed by the supplier. In mode B this is also the moment the traveller has paid.
booking.failedA booking could not be completed. Any hold placed against your credit has been released.
booking.cancelledA booking was cancelled, by you or by the supplier.
booking.modifiedA booking changed after confirmation.
payment.succeededMode B: the traveller's payment settled.
payment.failedMode B: the traveller's payment failed.
payment.refundedMode B: a refund was issued.
esim.provisionedAn eSIM profile is ready for installation.
esim.usage_thresholdA plan crossed a data usage threshold.
ledger.low_balanceMode A: your available credit fell below the warning level.
search_quota.thresholdYour daily search allowance is close to being used up.
settlement.invoice_issuedA settlement run produced an invoice.
settlement.payment_failedA settlement charge failed. Left unattended, this leads to your account going read-only.
apikey.revokedOne of your keys was revoked, whoever did it.

Two you should not skip

ledger.low_balance and search_quota.threshold are the only warning you get before a 402 or an unexpected invoice line. Subscribe to both even if you skip the rest.

Verifying the signature

Every delivery carries an X-Vacabee-Signature header with a timestamp and an HMAC-SHA256 signature:

Request header
X-Vacabee-Signature: t=1756483200,v1=5f2b8c1d…

To verify, in this order:

  • Take the raw request body — the bytes as received. Parsing and re-serialising the JSON changes them and the signature will not match.
  • Build the signed payload as `${t}.${rawBody}`.
  • Compute HMAC-SHA256 over it with the endpoint's signing secret and compare with v1 using a constant-time comparison.
  • Reject the delivery if t is more than five minutes away from your clock. Without that check a captured delivery can be replayed at any time in the future.
Node.js — Express
import crypto from "node:crypto";

// The raw body is required, so mount the parser accordingly:
app.post("/vacabee/webhooks", express.raw({ type: "application/json" }),
  (req, res) => {
    const header = req.get("X-Vacabee-Signature") ?? "";
    const parts = Object.fromEntries(
      header.split(",").map((p) => p.split("=", 2) as [string, string]),
    );

    const age = Math.abs(Date.now() / 1000 - Number(parts.t));
    if (!Number.isFinite(age) || age > 300) return res.sendStatus(400);

    const expected = crypto
      .createHmac("sha256", process.env.VACABEE_WEBHOOK_SECRET!)
      .update(`${parts.t}.${req.body}`)      // req.body is a Buffer here
      .digest("hex");

    const a = Buffer.from(expected);
    const b = Buffer.from(parts.v1 ?? "");
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.sendStatus(400);
    }

    void enqueue(JSON.parse(req.body.toString("utf8")));  // process async
    res.sendStatus(200);
  });

During a secret rotation both the old and the new secret are valid for the overlap window, so verify against either and accept if one matches.

Delivery and retries

  • Answer with any 2xx as soon as you have durably stored the event. Do the work afterwards — a handler that books, emails and writes to three systems before responding will eventually time out and earn itself a retry it did not need.
  • A non-2xx or a timeout is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours. After the last attempt the endpoint is disabled and your partner admins are emailed.
  • Delivery is at least once. The same event can arrive twice — deduplicate on the event id and make your handler safe to run again.
  • Ordering is not guaranteed. Two events about the same booking can arrive out of order; use the payload's state and timestamp, not arrival order.
  • Attempts are kept visible for 30 days in the portal, with the response we got, and every one can be resent by hand.
  • Webhook deliveries are never billable and never count against your search quota.
NextSandboxFree test environment and the six forced failures.