Manage Experiences
Create and edit your programs, activities, transfers and extras: the experience itself, its selling periods, its program days, its add-ons and its images.
These endpoints are for DMC keys. An experience belongs to the DMC that owns it, so a tour operator key can read and book experiences but not change them. An experience 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 experience | GET / POST /api/v3/experiences, GET / PUT / PATCH / DELETE /api/v3/experiences/{id} |
| Selling periods | GET / POST /api/v3/experiences/{id}/periods, GET / PUT / PATCH / DELETE .../periods/{periodId} |
| Program days (programs only) | GET / POST /api/v3/experiences/{id}/programs, PUT / PATCH / DELETE .../programs/{programId} |
| Add-ons | GET / POST /api/v3/experiences/{id}/extras, DELETE .../extras/{extraId} |
| Images | GET / POST /api/v3/experiences/{id}/images, PATCH / DELETE .../images/{imageId} |
| Promotions (read-only) | GET /api/v3/experiences/{id}/promotions, GET .../promotions/{promotionId} |
The experience body carries scalars, copy and id arrays. Nothing that has its own id travels inside it, so adding a period is one small call rather than a resend of the whole product. The experience response reports the size of each collection — periodsCount, programsCount, extrasCount, imagesCount — so you know what to fetch:
{
"periodsCount": 2,
"programsCount": 0,
"extrasCount": 1,
"imagesCount": 3
}Create an experience
POST /api/v3/experiences — the owning organization comes from your key, so there is no organizationId in the body.
{
"name": "Ubud Rice Terrace Trek",
"type": "activity",
"cityId": "3411",
"currency": "USD",
"description": "<p>A morning walk through the terraces.</p>",
"durationHours": 4,
"guiding": true,
"guidingLanguages": ["en"],
"themeIds": ["12"]
}name and type are required; everything else is optional. The response is 201 with the created experience, in the same shape GET /api/v3/experiences/{id} returns.
Types
type is one of program, activity, transfer or extra, in lower case. Only a program has program days.
The type is fixed
An experience keeps the type it was created with. Changing it answers 409: the type decides which underlying record the booking engine reads, so a switch would leave the experience pointing at the wrong one. Create a new experience instead.
State
A new experience starts in_progress. It becomes bookable when you set "state": "available". Until then it stays out of listings, though you can always read it back yourself.
Pricing type
pricingType is fit (per traveller, flexible dates), sic (shared departures) or series (scheduled departures). It defaults to fit.
Change an experience
PATCH /api/v3/experiences/{id} changes only the keys you send. An absent key is left alone; an explicit null clears the value. Use this for everyday edits.
{ "state": "available", "difficulty": "beginner", "dmcReference": null }PUT /api/v3/experiences/{id} replaces the whole experience — any field you leave out goes back to its default. Send the complete representation or use PATCH.
Both return the updated experience.
Languages
Text fields — name, summary, description, highlights, includedServices, excludedServices, extraRemarks, cancellationPolicies, locationsExtraInformations — are written for the language in the locale query parameter and read back the same way.
PATCH /api/v3/experiences/9182?locale=fr
{ "name": "Randonnée dans les rizières" }Other languages are untouched, including on a PUT: a full replacement replaces the experience's fields and the copy of that one language, never the translations you did not ask about.
Periods
A period says when the experience sells, at what price, and for how many travellers.
POST /api/v3/experiences/9182/periods
{
"startAt": "2030-06-01",
"endAt": "2030-09-30",
"price": 850,
"minPax": 1,
"maxPax": 4,
"state": "free_sale",
"availableDays": [true, true, true, true, true, false, false],
"allotments": 10,
"adultPrices": [
{ "price": 850, "minPax": 1, "maxPax": 2 },
{ "price": 650, "minPax": 3, "maxPax": 4 }
],
"childPrices": [{ "price": 425, "minAge": 5, "maxAge": 12 }]
}availableDaysis seven flags, Monday first: which weekdays the experience runs. It defaults to every day.stateison_request,free_sale,pre_confirmedorstop_sale, and defaults toon_request.adultPricesandchildPricesare bands with named bounds. Each list is authoritative: sending it replaces the current set, and leaving the key out of aPATCHleaves the bands alone.allotmentsUsedandcurrentPaxForDepartureare read-only — they count what has been booked.
A period with bookings against its allotment cannot be deleted; it answers 409. Set its state to stop_sale instead, which keeps the existing bookings valid.
Program days
Only a program experience has them. Each call appends a day:
POST /api/v3/experiences/9182/programs?locale=en
{
"title": "Day 1: Arrival and city tour",
"program": "<p>Arrive in Ubud and explore the market.</p>",
"startingTime": "08:30",
"endingTime": "17:00",
"accommodationType": "hotel",
"isIncludedAccommodation": true,
"meals": ["breakfast", "lunch"],
"pois": [{ "name": "Ubud Palace", "lat": -8.5069, "lon": 115.2625 }]
}dayin the response is the position among the days, 1-based. It is derived from the order the days were created, so days cannot be reordered, and deleting one shifts the numbers of those after it.accommodationTypeishotel,night_transfer,no_accommodationorfree_accommodation.- Times are
HH:MM. poisare points of interest, sent inline and returned in the order you sent them. The list is authoritative.
Not on this resource yet
A day's included and suggested options, its linked experiences and its alternative hotels carry trip-composition and pricing rules of their own. They keep their v1 endpoints for now.
Extras
An extra is another experience offered alongside this one. The link carries nothing of its own, so there is nothing to update — to change the add-on, delete the link and add another.
POST /api/v3/experiences/9182/extras
{ "experienceId": "9310" }The add-on has to be an experience you can already see; otherwise the call answers 404. Offering the same one twice answers 409, and an experience cannot be its own extra.
DELETE .../extras/{extraId} removes the link. The add-on experience itself is untouched.
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/experiences/9182/images
{
"name": "terraces.jpg",
"data": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ...",
"primary": true
}Adding, renaming or deleting one image leaves the rest alone — see Manage Hotels for the same rules on the hotel side.
Promotions
Promotions attached to the experience are readable, not writable — you still author them in the Koob back-office.
GET /api/v3/experiences/9182/promotions
GET /api/v3/experiences/9182/promotions/7802The shape is the same as the hotel one, minus the fields that only mean something for a stay: applicableTo, minNights and minRooms are always null on an experience promotion.
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 experience answers 404.
Delete an experience
DELETE /api/v3/experiences/{id} archives it and returns 204. Existing bookings keep referring to it; it stops appearing in listings and can no longer be booked.
Migrating from v1
| v1 | v3 |
|---|---|
POST /api/v1/experiences | POST /api/v3/experiences |
PUT /api/v1/experiences/{id} | PATCH /api/v3/experiences/{id} |
periods in the body | /periods sub-resource |
programs in the body | /programs sub-resource |
extras in the body | /extras sub-resource |
pictures in the body | /images sub-resource |
GET /api/v1/promotions/{experienceId}/list | GET /api/v3/experiences/{id}/promotions |
type: "Activity" | type: "activity" |
difficulty: "Beginner" | difficulty: "beginner" |
accomodationType: "noAccomodation" | accommodationType: "no_accommodation" |
paxRange: [1, 2] | minPax: 1, maxPax: 2 |
ageRange: [5, 12] | minAge: 5, maxAge: 12 |
availableDays: [1, 1, 1, 1, 1, 0, 0] | availableDays: [true, true, true, true, true, false, false] |
organizationId in the body | taken from your key |
empty 200 response | the updated experience in { "data": ... } |
The v1 endpoints keep working and are marked deprecated in the reference. Two differences matter most when porting:
- v1 takes the whole product in one body, so changing one period means resending every collection. In v3 each collection has its own endpoint, and touching one never disturbs another.
PUT /api/v1/experiences/{id}returns an empty body, so you needed a second call to see the result. v3 returns the experience.