Skip to content

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:

WhatEndpoint
The experienceGET / POST /api/v3/experiences, GET / PUT / PATCH / DELETE /api/v3/experiences/{id}
Selling periodsGET / 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-onsGET / POST /api/v3/experiences/{id}/extras, DELETE .../extras/{extraId}
ImagesGET / 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:

json
{
  "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.

json
{
  "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.

json
{ "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.

json
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 }]
}
  • availableDays is seven flags, Monday first: which weekdays the experience runs. It defaults to every day.
  • state is on_request, free_sale, pre_confirmed or stop_sale, and defaults to on_request.
  • adultPrices and childPrices are bands with named bounds. Each list is authoritative: sending it replaces the current set, and leaving the key out of a PATCH leaves the bands alone.
  • allotmentsUsed and currentPaxForDeparture are 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:

json
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 }]
}
  • day in 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.
  • accommodationType is hotel, night_transfer, no_accommodation or free_accommodation.
  • Times are HH:MM.
  • pois are 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.

json
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.

json
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/7802

The 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 ​

v1v3
POST /api/v1/experiencesPOST /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}/listGET /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 bodytaken from your key
empty 200 responsethe 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.