Hotel Booking
Books the offer selected from a rooms availability response.
Endpoint: POST /api/v3/bookings
Query parameters:
locale(optional): 2-letter language code for supplier-facing texts (e.g.en,fr).
Request body:
| Field | Required | Description |
|---|---|---|
requestId | yes | The requestId from GET /api/v3/hotels/{id}/rooms/availability. Must be used before its expiresAt (about 15 minutes). |
customer | yes | The person or company the booking is for. See below. |
rooms | yes | One entry per requested room. See below. |
internalReference | no | Your own reference for this booking; echoed back as internalReference on the booking. |
tourOperatorName | no | Tour operator name to record on the booking. |
networkName | no | Distribution network name to record on the booking. |
note | no | Free-text note stored on the booking. |
replaceBookingId | no | Existing booking this one replaces. |
customer object — firstName, lastName, email are required. Optional: phoneNumber, address, additionalAddress, city, zipCode, company, fileId (existing customer file to attach the booking to; one is created automatically when omitted).
rooms entries — one per room, in the same order as the availability request:
| Field | Required | Description |
|---|---|---|
bedCompositionId | yes | The chosen bed choice id from the rooms-availability response. |
travelers | yes | Every guest staying in the room. See below. |
promotionId | no | To book the promoted price shown in totalPriceWithPromo, send the bed choice's promotionInfo.promotionId (or the matching promotions[].id) here. Omitted = the room books without the promotion, at totalPriceWithoutPromo. |
optionalPromotionIds | no | Optional promotions to apply, from the bed choice's promotions[].id. |
The requestId + bedCompositionId pair fully identifies the offer — no supplier identifier needs to be sent back.
travelers entries — kind (adult, child or infant), gender (male, female or non_binary), firstName, lastName and birthdate (YYYY-MM-DD) are required; kind and gender are case-insensitive and any other value returns 400 with code INVALID_TRAVELER. Optional: nationality (ISO 3166-1 alpha-2), passportNumber, expirationDate (YYYY-MM-DD), ageIsExact. The traveler set must match the occupancy the availability was priced for: same adult count, and one child per childrenBirthdates entry, of the same age at check-in (see Travelers that differ from the search).
Example
curl -X POST "https://api.koob.tech/api/v3/bookings" \
-H "X-API-Key: your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"requestId": "6f1f9e0c-0a52-4c5e-9c1d-3f2b7a9e8d41",
"internalReference": "AGY-2026-118",
"customer": {
"firstName": "Marie",
"lastName": "Dupont",
"email": "marie.dupont@example.com"
},
"rooms": [
{
"bedCompositionId": "bed-812-double",
"travelers": [
{ "kind": "adult", "gender": "female", "firstName": "Marie", "lastName": "Dupont", "birthdate": "1988-04-17" },
{ "kind": "adult", "gender": "male", "firstName": "Paul", "lastName": "Dupont", "birthdate": "1986-09-02" },
{ "kind": "child", "gender": "female", "firstName": "Emma", "lastName": "Dupont", "birthdate": "2019-05-01" }
]
}
]
}'Response — 201 Created, with the created booking (same shape as GET /api/v3/bookings/{id}):
{
"data": {
"id": "98765",
"internalReference": "AGY-2026-118",
"tourOperatorName": null,
"networkName": null,
"note": null,
"currency": "USD",
"checkIn": "2026-11-12",
"checkOut": "2026-11-16",
"nightsCount": 4,
"state": "confirmed",
"allotmentAvailable": true,
"totalPrice": 1240.5,
"cancellationFees": null,
"hotel": { "id": "4242", "name": "Bali Beach Resort" },
"customer": {
"firstName": "Marie",
"lastName": "Dupont",
"company": null,
"address": null,
"additionalAddress": null,
"zipCode": null,
"city": null,
"email": "marie.dupont@example.com",
"phoneNumber": null,
"fileId": "5510"
},
"rooms": [
{
"id": "31022",
"name": "Deluxe Garden View",
"price": 1240.5,
"travelers": [
{
"id": "71201",
"firstName": "Marie",
"lastName": "Dupont",
"passportNumber": null,
"expirationDate": null,
"kind": "adult",
"gender": "female",
"nationality": "FR",
"birthdate": "1988-04-17",
"ageIsExact": true
}
]
}
],
"hotelConfirmationNumber": null,
"createdAt": "2026-11-05T14:21:00.000Z",
"cancelledAt": null
}
}allotmentAvailable tells you how the booking was taken: true means it was booked on allotment and confirmed at once; false means it went on request to the hotel and state is what to follow. nightsCount is checkOut minus checkIn. cancellationFees is set on cancellation when the rate's conditions charge a fee.
Every field the request accepts comes back on the booking, under the same name, so you can confirm what was booked. The two that do not are the request's own plumbing rather than booking state: requestId names the availability handshake, and replaceBookingId names the booking this one superseded. Room and traveler ids are stable for the lifetime of the booking — use them to match a room or a guest between calls.
The booking is created at the price shown in the availability response. Rooms with instantAvailability: false are created on-request (state: "sent") and confirmed by the supplier afterwards; re-read the booking with GET /api/v3/bookings/{id} to track the state. Which states can follow, and which are terminal, is on Booking states.
Reading a booking
GET /api/v3/bookings/{id} returns the same shape as the POST response. Only bookings of your own organization are visible; a booking of another of your organizations answers 403 OWNED_BY_OTHER_ORGANIZATION naming it (see Authentication), anything else is a 404.
Errors
| Status | Meaning | What to do |
|---|---|---|
404 | The requestId expired (15-minute window), is unknown, or was priced for another organization than the one booking. | Re-run the rooms-availability request as the booking organization and book with the fresh requestId. |
400 | Invalid payload (missing field, malformed date, unknown bedCompositionId). | Check message and details in the error body. |
409 | The offer can no longer be booked (allotment taken since pricing). | Re-run the availability search. |
409 PRICE_CHANGED_FOR_TRAVELERS | The travelers' ages at check-in differ from the searched ones and raise the price. | Show details.totalPrice; to accept, book again with details.requestId and details.rooms[].bedCompositionId. |
Travelers that differ from the search
Suppliers price the party you searched for. Each booked room must fit a searched room: no more adults or children than searched, and each child's age at check-in must be one of the searched ages. You may list fewer travelers than searched (for instance the lead guest only): the booking keeps the searched party and price.
When a traveler's age at check-in differs from the searched birthdate (for instance a child a year older), supported suppliers (currently Koedia) are searched again with the travelers' birthdates, looking for the same offer:
- same or lower total price: the booking goes through at the new price;
- higher total price:
409 PRICE_CHANGED_FOR_TRAVELERS, withdetailsholding a freshrequestId, itsexpiresAt,currency,previousTotalPrice,totalPriceand, in the order you sent them, therooms[].bedCompositionIdto book; - offer gone:
409 OFFER_NO_LONGER_AVAILABLE.
The search runs again only when you list every searched traveler. Other suppliers, and bookings that list fewer travelers than searched while an age differs, are refused with 400 ROOM_OCCUPANCY_MISMATCH: search again with the travelers' birthdates.
Error bodies carry a requestId field — quote it when contacting support.
After booking
Retrieve and manage the booking with the bookings endpoints: details, documents, messages, states and cancellation.