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
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
Replay includes failures
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.
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.