Requests

Idempotency

Every POST requires an Idempotency-Key header. Without one the request is rejected. This is the single rule most likely to save you a duplicate booking, so it is enforced rather than recommended.

Why it is mandatory

A booking call talks to a supplier. Suppliers are slow, and slow calls time out. When your HTTP client gives up after 30 seconds you do not know whether the booking was created — and the natural reaction, a retry, is exactly what turns one reservation into two, with two charges and one very unhappy traveller.

An idempotency key removes the ambiguity. The retry either returns the original result or tells you the two requests disagree. It never creates a second booking.

Sending a key

HTTP
POST /v1/hotels/bookings HTTP/1.1
Authorization: Bearer vcb_live_7Kq2xY…
Idempotency-Key: 6a4b1f2e-6c3a-4f1c-9e64-8b0f2d5a7c11
Content-Type: application/json

{ "rateId": "rate_01JD…", "settlement": "partner", … }
  • Generate one key per logical operation, not per HTTP attempt. All retries of the same booking reuse the same key. A fresh booking always gets a fresh key.
  • A UUIDv4 is the obvious choice. Anything unique and hard to collide works; a value derived from your own order id is fine and makes your logs easier to read.
  • Keys are scoped to your partner account and the route, so your key space never collides with another partner's.
  • Persist the key with your order before you send the request. A key you only hold in memory is gone precisely when you need it — after a crash mid-call.

What each case returns

SituationResult
POST without the header400 idempotency_key_required. No supplier call is made.
Same key, same bodyThe stored response is replayed byte for byte, with the original status code. No second supplier call, no second charge.
Same key, different body409 idempotency_key_reused. We do not guess which body you meant. Use a new key for a new operation.
Different key, same booking intentTreated as a genuinely new request. Idempotency protects you from your own retries; it does not deduplicate distinct requests for you.

Replay includes failures

If the first attempt failed with a definitive error, that error is what the replay returns. A key is consumed by the outcome, not by success. To try again after fixing the cause, issue a new key.

Retry policy

Retry on 429 and on 5xx, with exponential backoff and jitter, reusing the same idempotency key every time. Do not retry other 4xx responses — they describe something wrong with the request, and repeating it unchanged will not fix it.

TypeScript
const idempotencyKey = crypto.randomUUID();
await orders.update(orderId, { idempotencyKey }); // persist first

for (let attempt = 0; attempt < 5; attempt++) {
  const response = await fetch(`${apiUrl}/v1/hotels/bookings`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Idempotency-Key": idempotencyKey,
      "Content-Type": "application/json",
    },
    body,                       // byte-identical on every attempt
  });

  if (response.status !== 429 && response.status < 500) return response;

  await sleep(2 ** attempt * 500 + Math.random() * 250);
}

Note that body is computed once, outside the loop. A body that is rebuilt per attempt and contains a timestamp or a regenerated id will differ from the stored one and earn you a 409 instead of the replay you wanted.

Other methods

GET and DELETE are idempotent by definition and take no key. PATCH on a search session is a refinement of state you already own and takes no key either. Only POST creates, and only POST requires the header — including POST …/cancel and POST …/modify, where a double submission is just as expensive as a double booking.

When a call still leaves you uncertain, read the resource back before you retry anything: your own booking list at /v1/bookings is the authority on what exists. And if you are unsure how to interpret a status code, errors and versioning lists them all.

NextSearch quotaFree allowance, per-search fee and the quota headers.