Manage Hotels
Create and edit your own hotels: the property itself, its images, its booking contact and its descriptions. Its promotions are readable here too.
These endpoints are for DMC keys. A hotel belongs to the DMC that owns it, so a tour operator key can read hotels but not change them. A hotel belonging to another organization answers 404, not 403 — we never confirm that a resource you cannot reach exists.
For the full request and response schemas, see the complete API Reference.
The shape of it
One endpoint per thing you edit:
| What | Endpoint |
|---|---|
| The hotel | POST /api/v3/hotels, PUT / PATCH /api/v3/hotels/{id} |
| Its images | GET / POST /api/v3/hotels/{id}/images, PATCH / DELETE .../images/{imageId} |
| Its contact | GET / PUT /api/v3/hotels/{id}/contact |
| Its descriptions | GET / PUT /api/v3/hotels/{id}/descriptions |
| Its promotions (read-only) | GET /api/v3/hotels/{id}/promotions, GET .../promotions/{promotionId} |
The hotel body carries scalars and id arrays only. Nothing that has its own id — an image, a description — travels inside it, so changing one image is one small call rather than a resend of the whole property.
Create a hotel
POST /api/v3/hotels — the owning organization comes from your key, so there is no organizationId in the body.
{
"name": "Bali Beach Resort & Spa",
"cityId": "3411",
"currency": "USD",
"address": "Jl. Pantai 12, Sanur",
"lat": -8.6785,
"lon": 115.2621,
"stars": 5,
"styleIds": ["8"],
"kindIds": ["2"],
"dmcReference": "BALI-SANUR-01",
"timezone": "Asia/Makassar"
}name, cityId and currency are required; everything else is optional. Leave lat/lon out and the hotel takes the city centre, so it still shows up in map searches.
The response is 201 with the created hotel, in the same shape GET /api/v3/hotels/{id} returns:
{
"data": {
"id": "4242",
"name": "Bali Beach Resort & Spa",
"city": { "id": "3411", "name": "Sanur" },
"country": { "id": "102", "name": "Indonesia" },
"stars": 5,
"currency": "USD",
"styles": [{ "id": "8", "name": "Beach" }],
"kinds": [{ "id": "2", "name": "Resort" }],
"images": [],
"state": "in_progress",
"showcased": false,
"showToTa": true,
"priceLevel": null,
"dmcReference": "BALI-SANUR-01",
"timezone": "Asia/Makassar"
}
}A new hotel starts in_progress. It becomes sellable when you set "state": "available" — until then it stays out of the catalog and out of availability, though you can always read it back yourself.
Change a hotel
Two verbs, and the difference matters.
PATCH /api/v3/hotels/{id} changes only the keys you send. An absent key is left alone; an explicit null clears the value. Use this for everyday edits.
{ "stars": 4, "priceLevel": "medium", "timezone": null }That request sets the rating and the price level, clears the timezone, and touches nothing else.
PUT /api/v3/hotels/{id} replaces the whole hotel. Any field you leave out goes back to its default, so send the complete representation or use PATCH.
Prefer PATCH
PUT is there when you keep a full local copy of the hotel and want the server to match it exactly. If you are changing one or two fields, PATCH is what you want — it cannot wipe something you forgot to include.
Both return the updated hotel, so there is no follow-up GET.
Styles, kinds and locations
These are id arrays, and each one is authoritative: the list you send becomes the list on the hotel. Send "styleIds": [] to clear them, or leave the key out (on PATCH) to leave them alone.
Currency
The currency is frozen once the hotel has a published contract — signed rates are denominated in it. Trying to change it then answers 409 with HOTEL_CURRENCY_LOCKED.
Images
One image per call, each addressed by its own id. Send the file as a base64 data URI; a remote URL is rejected rather than silently ignored.
POST /api/v3/hotels/4242/images
{
"name": "lobby.jpg",
"data": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ...",
"primary": false
}PATCH .../images/{imageId}renames an image or makes it the main one. The file itself never changes — to replace a picture, add the new one and delete the old.DELETE .../images/{imageId}removes exactly that image and returns204.primary: truedemotes whichever image was main before, so there is always at most one.
Adding an image never removes another
The v1 attachment endpoints replace the whole collection, so anything you do not resend is deleted. These endpoints do not: adding, renaming or deleting one image leaves the rest untouched.
Contact
The hotel has one booking contact, so PUT /api/v3/hotels/{id}/contact creates or replaces it. emailsCc is a list of addresses copied on booking mail.
{
"firstName": "Made",
"lastName": "Wirawan",
"email": "reservations@bali-beach.example",
"emailsCc": ["ops@bali-beach.example"],
"phoneNumber": "+62 361 123456",
"job": "Reservations manager"
}Being a PUT, this is the whole contact: a field you leave out is cleared, not kept.
Descriptions
Descriptions are grouped by category and written per language. Pass the language in locale; other languages are untouched.
PUT /api/v3/hotels/4242/descriptions?locale=en
{
"descriptions": [
{ "categoryId": "3", "content": "<p>A beachfront resort on the Sanur shore.</p>" }
]
}The list is authoritative for that language: a category you leave out loses its entry. One entry per category — sending the same categoryId twice answers 400 with DUPLICATE_REFERENCE.
GET /api/v3/hotels/{id}/descriptions?locale=en returns them with the category as a { id, name } reference, and that id is what you send back as categoryId.
Promotions
Promotions attached to the hotel are readable, not writable — you still author them in the Koob back-office, where the benefit, the periods and the tour operators they go to are edited together.
GET /api/v3/hotels/4242/promotions
GET /api/v3/hotels/4242/promotions/7781{
"data": [
{
"id": "7781",
"name": "Early booker -15%",
"kind": "early_booker",
"state": "operational",
"status": "active",
"applicableTo": "to",
"benefit": {
"kind": "percent",
"value": 15,
"currency": "EUR",
"unit": "per_night",
"perRoom": false,
"includeExtraBeds": false,
"stayFlexibility": null
},
"minNights": null,
"minRooms": 1,
"minPax": null,
"maxPax": null,
"bookingWindowDays": 60,
"periods": [
{ "id": "9901", "startAt": "2026-11-01", "endAt": "2027-03-31", "weekdays": [1, 2, 3, 4, 5, 6, 7] }
],
"createdAt": "2026-06-02T09:14:00.000Z",
"updatedAt": "2026-08-11T16:02:00.000Z"
}
]
}kind says what triggers the promotion (basic_deal, early_booker, last_minute, stay_flexibility) and benefit.kind what it gives (percent, amount, free_night, upgrade_room, amenity, supplement, supplement_percent). state is what the DMC set; status is where the promotion stands today, derived from its periods: active, planned, expired, archived or not_operational.
Period bounds are inclusive — the promotion still runs on endAt. weekdays holds ISO-8601 day numbers, 1 for Monday through 7 for Sunday.
A promotion id belonging to another hotel answers 404.
Migrating from v1
| v1 | v3 |
|---|---|
POST /api/v1/hotels/create | POST /api/v3/hotels |
PUT /api/v1/hotels/{id} | PATCH /api/v3/hotels/{id} |
hotelInformation.* | top-level fields |
primaryAttachment / secondaryAttachments | /images sub-resource, primary: true for the main one |
contact | /contact sub-resource |
hotelDescriptions | /descriptions sub-resource |
GET /api/v1/promotions/{hotelId}/list | GET /api/v3/hotels/{id}/promotions |
empty 200 response | the updated hotel in { "data": ... } |
The v1 endpoints keep working and are marked deprecated in the reference. Two things they got wrong are worth knowing if you are porting:
PUT /api/v1/hotels/{id}nulls any scalar left out ofhotelInformation.PATCH /api/v3/hotels/{id}leaves absent fields alone.- v1 returned an empty body, so you needed a second call to see the result. v3 returns the hotel.