Travelity OCTO API (1.0.0)

Download OpenAPI specification:

The OCTO 1.0 supplier API Travelity hosts for its resale partners.

Travelity hosts this API for resellers who speak OCTO - the Open Connectivity for Tours, Activities and Attractions standard. It is the supplier side of the protocol: you call us, we answer with our operators' inventory and take bookings against it.

Conformance target: OCTO 1.0. Where the published core specification is silent or where shipping implementations disagree with it, this document describes what Travelity actually sends and accepts. Those places are called out explicitly below. Everything here is generated from, and continuously tested against, the running service.

Getting access

Credentials are issued per supplier connection by Travelity - one Bearer key identifies exactly one operator's inventory on exactly one mount. To set up a connection, or to replace a key that was lost or compromised, contact Travelity at vahagns@bravecrew.io.

Send it on every request:

Authorization: Bearer <your api key>

The two failures are distinguished:

  • 401 UNAUTHORIZED - nothing to authenticate with: no Authorization header, a scheme other than Bearer, or an empty token.
  • 403 FORBIDDEN - a token was presented but matched no active connection on this mount. A key issued for one mount answers 403 on another mount's prefix; it is not a shared credential.

401 is also what an unauthenticated request to an unknown path under the prefix gets - the credential is checked before the route.

Capabilities

OCTO capabilities are negotiated per request. Travelity supports exactly one: octo/pricing. Ask for it with a header:

Octo-Capabilities: octo/pricing

or, if your client cannot set headers, with the _capabilities query parameter. The header wins when both are present. Parsing is case-insensitive and comma-separated, so OCTO/Pricing matches and unknown ids are ignored rather than rejected.

Every successful response carries an Octo-Capabilities response header naming what actually activated - the empty string when nothing did. That header is how you confirm pricing is on; it is set from the same resolution that gates the pricing fields, so a capability can never activate without being acknowledged.

Without octo/pricing, every pricing field is absent - not null, not zero. No other capability (octo/content, octo/pickups, octo/offers, octo/questions, octo/notifications) is implemented.

Products and options

One OCTO product is one Travelity option. productId is the option's identifier in our system, and it is what you will see in every payload and send in every request.

Each product carries exactly one option, with id: "DEFAULT". Travelity models what OCTO calls options as separate products, so there is nothing to choose between. Every request that takes an optionId requires it and it must equal DEFAULT; anything else is INVALID_OPTION_ID and the response names the value you sent.

Products are START_TIME products: they publish departures, never opening hours. allowFreesale is false on every product - a booking always requires an availabilityId obtained from POST /availability first.

Every product listed here confirms instantly. An operator's on-request options, where a person decides whether to accept each request, are not offered on this channel at all. Neither is a private option without a maximum party size: this channel reports a private departure in participants, and that number is what states how many a vehicle holds.

Private products sold by the group

A private product whose unit is a FAMILY unit is sold by the unit: one unit is one group is one vehicle, and paxCount on that unit is the group size. Everything this API reports for such a product is a count of units, not of participants: Option.restrictions is { minUnits: 1, maxUnits: 1 }, Availability.capacity is the number of vehicles, vacancies the vehicles still free, and maxUnits is 1 while a vehicle is free and 0 once none is. A reservation or amendment takes exactly one FAMILY unit item; a second one is UNPROCESSABLE_ENTITY carrying restrictions: { minUnits: 1, maxUnits: 1 }. A private product sold per person keeps the participants reporting described above.

Which maxUnits to trust

Two fields carry a party ceiling, and they answer different questions:

  • Option.restrictions.maxUnits is the operator's per-booking party ceiling, null when none is set. It is a static property of the option and says nothing about any one departure.
  • Availability.maxUnits is authoritative. It is the live per-departure number and is a promise: every value it can name is one that POST /bookings will accept. It is the smaller of the operator's ceiling and what the departure still has free, and on a bracketed rule it never exceeds the highest priced bracket.

On a private option the two are the same number while a vehicle is free, because one booking takes the whole vehicle: Availability.maxUnits is the vehicle's party size, and 0 once none is free.

Option.restrictions.minUnits is the option's minimum party size, 1 when the operator set no floor, and it is enforced - see Participant bounds.

Availability

POST /availability takes exactly one of three request forms:

  1. localDate - a single day.
  2. localDateStart + localDateEnd - an inclusive range.
  3. availabilityIds - specific departures you already hold ids for.

Sending none, or more than one, is BAD_REQUEST. A range wider than one year (366 days) is BAD_REQUEST. Dates are strict YYYY-MM-DD.

The availability id

An availabilityId is an ISO 8601 local datetime, not an opaque row id:

2026-07-01T09:00:00+04:00

It is the departure's local date and start time with the operator's UTC offset. Do not build one by string concatenation: the product's availabilityLocalStartTimes are normalised to HH:MM ("09:00") while the id carries seconds ("...T09:00:00+04:00"). Ids come from POST /availability, which is the only place they are minted.

What is, and is not, returned

  • A sold-out departure is returned with vacancies: 0 and status: "SOLD_OUT".
  • A departure already inside its booking cutoff is returned, also at vacancies: 0. The row still exists; you simply cannot sell it.
  • A closed departure is omitted entirely.
  • A date the operator has not priced for your channel and currency is omitted entirely - see the next section.

capacity is the departure's own maximum; vacancies is what is genuinely sellable right now. Both are counted in participants, on every option. status is SOLD_OUT at zero vacancies, LIMITED at or below half of capacity, and AVAILABLE otherwise. A departure that runs past midnight has a localDateTimeEnd on the following local date - it never wraps back before its own start.

On a private option one booking takes a whole vehicle, so a sale of any size drops vacancies by the vehicle's party size, and maxUnits is that party size while a vehicle is free and 0 once none is.

POST /availability/calendar collapses the same departures into one entry per local date, summing vacancies and capacity across the day. It is deliberately narrower than POST /availability: no id, no allDay, no maxUnits, no utcCutoffAt. Use it to paint a month; use POST /availability to quote and to obtain ids. Both range dates are required.

Dates we cannot price are not offered

A date this channel cannot put a price on is not returned, and is refused if you name one anyway. Four causes:

Cause Offered? If you reserve against it
No pricing rule covers the date for your channel and currency No UNPROCESSABLE_ENTITY
The operator prices that date per group in brackets (a shape OCTO's unit-item contract cannot express) No UNPROCESSABLE_ENTITY
The operator prices that date per person in brackets, and no bracket covers the smallest party the product accepts (Option.restrictions.minUnits) No UNPROCESSABLE_ENTITY, as for a date no rule covers
The operator prices that date per person in brackets, and no bracket covers your party size Yes. Without units, unitPricing is the price for the smallest party the product accepts; with units, it is the bracket your party falls into. A party no bracket covers gets neither unitPricing nor pricing, and maxUnits never exceeds the top bracket UNPROCESSABLE_ENTITY carrying restrictions set to the smallest and largest party that date's brackets price

The practical consequence: you can sell exactly as far ahead as the operator has priced. An operator who has published prices for the next 90 days offers you 90 days here, however much further ahead their schedule runs. Extending the horizon is a pricing change on their side, not a scheduling one.

Pricing

Everything in this section needs the octo/pricing capability negotiated. Without it none of these fields appear at all.

All amounts are integers in the currency's minor unit, with currencyPrecision telling you where the decimal point goes - 2 for most currencies, 0 for JPY, KRW, VND and the other zero-decimal currencies. 10000 at precision 2 is 100.00.

The currency is the one configured on your connection, never the operator's own default. Every product reports it as defaultCurrency, and availableCurrencies always contains exactly that one value. You may send currency on a reservation as an assertion; if it does not match the connection currency the reservation is BAD_REQUEST rather than silently re-quoted.

Every pricing object carries the same three tiers. original and retail always carry the same value: on a booking, its price in your connection's currency, set at reservation. It changes only when a hold is re-seated onto a different party, departure or product - by a PATCH, or by a confirmation whose unitItems differ from the held party - which re-prices it at the target date's prices; the same party on the same departure keeps its price. Confirmation, extension and cancellation otherwise leave it as it is, and a confirmed booking's price never changes. net is null - no wholesale rate is negotiated on this channel. includedTaxes is always []; taxes are not itemised on this channel.

Where pricing appears:

  • Products - defaultCurrency, availableCurrencies, pricingPer, and a pricingFrom on each unit that has a price. A unit with no published price carries no pricingFrom at all, rather than one reading zero.
  • Availability - unitPricing[] when the departure resolves per-unit prices, plus a total pricing object only when your request carried units, since a total needs quantities. A per-group product sends unitPricing: [] with the group total in pricing.
  • Calendar - unitPricingFrom[] and pricingFrom{}, taken from the day's first departure. A calendar gives a "from" price for a date, not a quote for a departure.
  • Bookings - a total pricing on the booking, and a per-item pricing on each unit item.

pricingPer is BOOKING for a product the operator prices per group, and UNIT for everything else. A BOOKING-priced product emits no unitItems[].pricing at all - the group total on the booking is the whole price. It follows how the operator's option is priced, per participant or per group, and does not change from date to date: a product does not price per head on one day and per party on the next.

Tiered prices are resolved before they reach you. OCTO has no bracket concept, so a rule priced per person in brackets is flattened to per-unit prices. Wherever no party is stated - a product's pricingFrom, and unitPricing or unitPricingFrom requested without units - the price is the bracket of the smallest party the product accepts (Option.restrictions.minUnits), which is not necessarily the lowest price the product offers. A larger party is priced at its own bracket: send units to receive that bracket's unitPricing and the total pricing, so a quote and the reservation that follows it agree. The booking's pricing is authoritative.

Booking lifecycle

POST /bookings          -> ON_HOLD     (capacity is taken here)
POST /bookings/{uuid}/confirm -> CONFIRMED (tickets are issued here)
POST /bookings/{uuid}/cancel  -> CANCELLED

Holds

A reservation holds capacity for expirationMinutes, clamped into [5, 30]. Out-of-range values are clamped, never rejected - a 0 becomes a 5-minute hold rather than an error. POST /bookings/{uuid}/extend adds the requested minutes, clamped the same way, to the hold's current expiry - so an extension moves utcExpiresAt later by the clamped amount, and 0 adds five minutes rather than expiring the hold on the spot. Only an unexpired hold can be extended: two extensions sent together both count, and an extension that arrives after the booking was confirmed or cancelled is INVALID_BOOKING_UUID and changes nothing.

A hold has a maximum lifetime: nothing carries it more than 180 minutes past the reservation. An extension the ceiling cuts short moves utcExpiresAt onto the ceiling; once the hold is there, a further extension is UNPROCESSABLE_ENTITY with errorMessage exactly The hold has reached its maximum lifetime - confirm it or let it expire. A PATCH that sets expirationMinutes is held to the same ceiling but is never refused for it: the new expiry is cut to the ceiling silently.

Every utc* timestamp in this API is UTC at seconds precision with the Z designator (2026-06-24T09:15:32Z) - no milliseconds.

Booking uuid and idempotency

Send a uuid on a reservation and it becomes the booking's identifier and its idempotency key. Repeating a reservation with the same uuid replays the booking that already exists instead of taking capacity twice - including for two requests racing each other, one of which will replay the other's booking. The key is scoped to the operator behind your key: two different operators may use the same uuid without colliding.

Omit uuid and the booking's own identifier is returned as uuid. Either way, the value you get back is the one to use in every later path.

unitItems[].uuid works the same way: supply one per head and it is echoed on every later read; omit it and we mint one. Unit items keep their ids and their order from the hold through confirmation and cancellation.

Participant bounds

The operator's minimum and maximum party size are enforced when you reserve. The check is on the participant count, not on seats consumed - which matters for a private departure, where a party of any size takes the whole vehicle and the capacity check alone would never notice a party too large for it.

A party outside the bounds is UNPROCESSABLE_ENTITY carrying restrictions: { minUnits, maxUnits } - the same shape the option advertised its bounds in, so you can retry inside them. This is what makes Availability.maxUnits and Option.restrictions.minUnits guarantees rather than hints. A party inside the bounds that no price bracket covers on that date is refused the same way, with restrictions carrying the smallest and largest party that date's brackets price.

Confirmation

contact is optional on our side, although OCTO 1.0 marks it required. A confirm with no contact is accepted; the booking then reads back with the contact it had - all null if none was ever sent. A sent contact is stored whole, all nine fields, and echoed as sent on every later read: a field you did not send reads back null (locales reads []), and a name sent one way is derived the other - firstName and lastName from a lone fullName, fullName from firstName and lastName. contact.notes is the contact's own field and is never merged into the booking's notes. Per-unit-item contacts and references are accepted for conformance and not stored; every unit item reads back an all-null contact.

unitItems on a confirmation is the final party. When it names the heads the hold already carries - the same count of each unit, in any order - the hold is confirmed exactly as it stands: nothing is re-priced and the departure is not checked again, so a hold whose departure has since passed its cutoff or closed still confirms, and only the uuids you send are taken. A different set is an amendment: the hold is re-sized, re-priced and re-seated before it is confirmed, exactly as PATCH would do it (see Amendments), refusals included - every unitId must belong to the product (INVALID_UNIT_ID), an empty list is refused with the option's bounds, the departure must still be open and before its cutoff, and a larger party must fit it. One ticket is issued per final head. Unit items keep their ids for the heads that stay; a new head gets a fresh one. Omit unitItems and the held party is confirmed as it stands. A confirmation that sends unitItems sells exactly that party even when an amendment of the same booking lands while it is being applied; one that omits them sells the party the hold carries once that amendment is in.

Confirming is idempotent: a repeat confirm issues no second set of tickets and ignores any unitItems it carries. A repeat carrying a different resellerReference replaces the stored one - the reference is yours and the latest one wins - while one that repeats it, omits it, or sends it null or empty changes nothing (clearing a reference is a PATCH); a repeat carrying a different contact replaces the stored contact whole. A repeat that changes nothing writes nothing, and utcUpdatedAt stays where it was.

notes behaves the same way. The booking carries the notes sent at reservation; a confirmation that sends notes replaces them, one that omits them leaves them unchanged, and an empty string clears them.

Amendments

PATCH /bookings/{uuid} amends a booking in place: id, uuid, supplierReference and utcCreatedAt never change, utcUpdatedAt moves, and every field you omit keeps its value. An empty body answers the booking unchanged, and so does a body that asks for what the booking already holds: a repeated amendment with the same body converges and writes nothing, so retrying a PATCH after a timeout neither moves nor re-sizes the hold a second time, and utcUpdatedAt stays where the first one left it (a body carrying expirationMinutes re-holds from now on every send). The amendments, extensions, confirmation and cancellation of one booking never interleave: each is applied to the booking as the one before it left it.

The state gate comes first. An unknown uuid, an expired hold or a cancelled booking answers INVALID_BOOKING_UUID. A confirmed booking takes only resellerReference (null clears it), notes (an empty string or null clears them), contact (replaced whole) and emailReceipt (accepted and ignored); if the body carries productId, optionId, availabilityId, unitItems or expirationMinutes the whole request is UNPROCESSABLE_ENTITY with errorMessage exactly Confirmed bookings cannot be re-dated or re-sized through this channel - cancel and rebook, and nothing is written.

A hold takes everything, in one transaction:

  • productId / optionId move the hold onto another product mapped on this connection (INVALID_PRODUCT_ID otherwise; optionId must be DEFAULT). The hold's own departure exists only on its own product, so a product change with no availabilityId is INVALID_AVAILABILITY_ID carrying the departure it sits on.
  • availabilityId moves the hold onto another departure. Omitted, the hold stays where it is.
  • unitItems is the final party; omitted, the current party is kept and re-checked against the target product.
  • The target is then validated exactly as a reservation is, with the same errors in the same order: the departure (existence, closed, past, cutoff, unpriced date, on-request, private without party size), the units (INVALID_UNIT_ID), an empty party (UNPROCESSABLE_ENTITY with restrictions), the participant bounds, the price, re-resolved for the new party and date, and the capacity - on the same departure a smaller party gives the difference back and a larger one needs room for the added heads only; on another departure the whole party has to fit, and the old one is released in the same transaction and only on success. The same heads on the same departure are not re-priced.
  • expirationMinutes re-holds the departure for that many minutes from now, clamped into [5, 30] and never past the hold's maximum lifetime (see Holds) - cut to it silently, never refused; contact, notes and resellerReference behave as on a confirmed booking. No tickets are issued until the hold is confirmed.

Unit items keep their ids for the heads that stay; a uuid you send is honoured, a new head gets a fresh one, and a change of product renews them all. The response is the full booking exactly as GET /bookings/{uuid} returns it after the change. Amendments send no notification to the operator.

Test bookings

testMode is optional on a reservation and echoed on every later read. Test bookings are real bookings - they consume capacity, issue tickets and are visible to the operator like any other booking. There is no auto-cleanup; cancel them when you are done certifying.

Booking status, and how EXPIRED is derived

Status Meaning
ON_HOLD A live reservation, within its expiry
CONFIRMED A sale
EXPIRED A hold that ran out of time
CANCELLED Something somebody cancelled

A hold reads EXPIRED as soon as its expiry passes, and it still reads EXPIRED afterwards. A hold you cancel while it is still live reads CANCELLED, as does any cancelled confirmed booking. An EXPIRED booking is never cancellable.

Cancellation

The cancellation policy does not gate the call. The only guard is the departure date: a booking whose departure day has already passed in the operator's timezone is UNPROCESSABLE_ENTITY, unless you send force: true. Same-day cancellations are allowed. A late cancellation is priced by its refund tier, not refused.

  • cancellable is true while the booking is ON_HOLD or CONFIRMED and its departure day has not passed. EXPIRED and CANCELLED are always false.
  • cancellation.refund is the tier that applied at the moment of cancellation, not at read time - so a booking cancelled well inside a full-refund window keeps reading FULL after its departure passes. A booking that never reached CONFIRMED paid nothing and reads FULL.
  • Cancelling an already-cancelled booking is a successful no-op that returns the booking.

Option.cancellationCutoff, cancellationCutoffAmount and cancellationCutoffUnit come from the operator's cancellation policy: the earliest hours-before-departure at which the policy still refunds something, i.e. the boundary past which it pays nothing. No policy, or no refundable tier, reports 0 / hour. A whole-day figure is rendered in days (24 becomes 1 day). This is not the booking cutoff the departure advertises for sales - that one is Availability.utcCutoffAt.

Errors

Unlike some supplier APIs, this one uses real HTTP status codes. Every business rejection is HTTP 400 with a body of exactly:

{ "error": "INVALID_PRODUCT_ID", "errorMessage": "...", "productId": "..." }

errorMessage is always a single string, never an array. The third property, when present, names what was wrong.

The complete catalogue is the ten codes OCTO 1.0 defines, and nothing else:

HTTP error Extra
400 INVALID_PRODUCT_ID productId
400 INVALID_OPTION_ID optionId
400 INVALID_UNIT_ID unitId
400 INVALID_AVAILABILITY_ID availabilityId
400 INVALID_BOOKING_UUID uuid
400 UNPROCESSABLE_ENTITY restrictions, only for a participant-bounds failure or a party no price bracket covers
400 BAD_REQUEST -
401 UNAUTHORIZED -
403 FORBIDDEN -
500 INTERNAL_SERVER_ERROR -

An authenticated request to an unknown path under the prefix answers 404 with error: "BAD_REQUEST" - the OCTO body shape, with the HTTP status that tells you the route does not exist.

Which error you get first

A reservation is validated in a fixed order, so a request wrong in more than one way gets the most specific answer:

  1. the product, option and availability id must resolve - INVALID_PRODUCT_ID, INVALID_OPTION_ID, INVALID_AVAILABILITY_ID;
  2. unitItems must name at least one head - UNPROCESSABLE_ENTITY with restrictions (a missing list counts as empty);
  3. every unitId must belong to the option - INVALID_UNIT_ID;
  4. the party must sit inside the option's bounds - UNPROCESSABLE_ENTITY with restrictions;
  5. the date must be priceable for that party - UNPROCESSABLE_ENTITY, carrying restrictions with that date's bracket span when no price bracket covers the party;
  6. the departure must have room - UNPROCESSABLE_ENTITY.

Nothing is written before all six pass. A refused reservation consumes no capacity. An amendment is validated in the same order.

Two 500s that are not the same

A 500 carrying {"error": "INTERNAL_SERVER_ERROR", "errorMessage": "Unexpected error"} is a failure on our side; retry after a short delay. A 500 with any other body shape is a transient platform error; retry, and contact Travelity if it persists.

Supplier

Who you are connected to.

Get supplier details

The operator behind this connection. contact is the operator's support contact, not a booking contact; every field in it is nullable and only email has a value today. endpoint is this mount's own base URL.

Authorizations:
bearerAuth
query Parameters
_capabilities
string
Example: _capabilities=octo/pricing

The same list as the Octo-Capabilities header, for clients that cannot set headers. Ignored when the header is present.

header Parameters
Octo-Capabilities
string
Example: octo/pricing

Capabilities to activate, comma-separated and case-insensitive. Only octo/pricing is supported; unknown ids are ignored. Takes precedence over _capabilities.

Responses

Response samples

Content type
application/json
{
  • "id": "3f1a9e2c-7d45-4b8a-9c61-0e2d4f6a8b10",
  • "name": "Yerevan Walking Tours",
  • "contact": {
    }
}

Products

The inventory this connection may sell.

List every product on this connection

Every active product this connection may sell. Pricing fields appear only with octo/pricing negotiated.

Authorizations:
bearerAuth
query Parameters
_capabilities
string
Example: _capabilities=octo/pricing

The same list as the Octo-Capabilities header, for clients that cannot set headers. Ignored when the header is present.

header Parameters
Octo-Capabilities
string
Example: octo/pricing

Capabilities to activate, comma-separated and case-insensitive. Only octo/pricing is supported; unknown ids are ignored. Takes precedence over _capabilities.

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Get one product

Authorizations:
bearerAuth
path Parameters
productId
required
string
Example: 9f8b1d24-5c3e-4a71-b0d2-6e5f7a8c9012

The product id, as returned by GET /products.

query Parameters
_capabilities
string
Example: _capabilities=octo/pricing

The same list as the Octo-Capabilities header, for clients that cannot set headers. Ignored when the header is present.

header Parameters
Octo-Capabilities
string
Example: octo/pricing

Capabilities to activate, comma-separated and case-insensitive. Only octo/pricing is supported; unknown ids are ignored. Takes precedence over _capabilities.

Responses

Response samples

Content type
application/json
{
  • "id": "9f8b1d24-5c3e-4a71-b0d2-6e5f7a8c9012",
  • "internalName": "Old Yerevan Walking Tour - Morning Departure",
  • "reference": null,
  • "locale": "en",
  • "timeZone": "Asia/Yerevan",
  • "allowFreesale": false,
  • "instantConfirmation": true,
  • "instantDelivery": true,
  • "availabilityRequired": true,
  • "availabilityType": "START_TIME",
  • "deliveryFormats": [
    ],
  • "deliveryMethods": [
    ],
  • "redemptionMethod": "DIGITAL",
  • "defaultCurrency": "EUR",
  • "availableCurrencies": [
    ],
  • "pricingPer": "UNIT",
  • "options": [
    ]
}

Availability

Departures, their remaining capacity and their prices.

Check availability for a date, a range, or specific departures

Send exactly one of localDate, localDateStart + localDateEnd, or availabilityIds. Include units to receive a total pricing object and, on a bracketed rule, the prices for that specific party.

Closed departures and dates the operator has not priced are omitted. A departure inside its booking cutoff is returned at vacancies: 0.

Authorizations:
bearerAuth
query Parameters
_capabilities
string
Example: _capabilities=octo/pricing

The same list as the Octo-Capabilities header, for clients that cannot set headers. Ignored when the header is present.

header Parameters
Octo-Capabilities
string
Example: octo/pricing

Capabilities to activate, comma-separated and case-insensitive. Only octo/pricing is supported; unknown ids are ignored. Takes precedence over _capabilities.

Request Body schema: application/json
required
productId
required
string
optionId
required
string
Value: "DEFAULT"
localDate
string^\d{4}-\d{2}-\d{2}$

A single local date.

localDateStart
string^\d{4}-\d{2}-\d{2}$

First date of an inclusive range. Requires localDateEnd.

localDateEnd
string^\d{4}-\d{2}-\d{2}$

Last date of an inclusive range. At most a year after localDateStart.

availabilityIds
Array of strings non-empty

Specific departures, by the ids a previous call returned.

Array of objects (AvailabilityUnit)

The party you intend to book. Supplying it returns a total pricing object and, on a bracketed rule, the prices for that exact party. Every id must be one of the product's unit ids; any other is INVALID_UNIT_ID.

Responses

Request samples

Content type
application/json
{
  • "productId": "9f8b1d24-5c3e-4a71-b0d2-6e5f7a8c9012",
  • "optionId": "DEFAULT",
  • "localDate": "2026-07-01",
  • "units": [
    ]
}

Response samples

Content type
application/json
[
  • {
    }
]

One entry per local date over a range

For painting a calendar. vacancies and capacity are the day's sums across every departure, and the prices are the day's first departure - a "from" figure, not a quote. Both range dates are required, and the range may not exceed a year.

A date shown as open always has a real, priced departure behind it that POST /availability will return an id for.

Authorizations:
bearerAuth
query Parameters
_capabilities
string
Example: _capabilities=octo/pricing

The same list as the Octo-Capabilities header, for clients that cannot set headers. Ignored when the header is present.

header Parameters
Octo-Capabilities
string
Example: octo/pricing

Capabilities to activate, comma-separated and case-insensitive. Only octo/pricing is supported; unknown ids are ignored. Takes precedence over _capabilities.

Request Body schema: application/json
required
productId
required
string
optionId
required
string
Value: "DEFAULT"
localDateStart
required
string^\d{4}-\d{2}-\d{2}$
localDateEnd
required
string^\d{4}-\d{2}-\d{2}$

At most a year after localDateStart.

Array of objects (AvailabilityUnit)

Responses

Request samples

Content type
application/json
{
  • "productId": "9f8b1d24-5c3e-4a71-b0d2-6e5f7a8c9012",
  • "optionId": "DEFAULT",
  • "localDateStart": "2026-07-01",
  • "localDateEnd": "2026-07-31",
  • "units": [
    ]
}

Response samples

Content type
application/json
[
  • {
    }
]

Bookings

Reserve, confirm, read, extend and cancel.

Reserve a departure (creates an ON_HOLD booking)

Takes capacity and returns an ON_HOLD booking. Send one unitItems entry per head - the array's length is the party size.

Supply uuid to make the call idempotent: a repeat returns the same booking without taking capacity twice.

Nothing is written unless the whole request is acceptable; a refusal consumes no capacity.

Authorizations:
bearerAuth
query Parameters
_capabilities
string
Example: _capabilities=octo/pricing

The same list as the Octo-Capabilities header, for clients that cannot set headers. Ignored when the header is present.

header Parameters
Octo-Capabilities
string
Example: octo/pricing

Capabilities to activate, comma-separated and case-insensitive. Only octo/pricing is supported; unknown ids are ignored. Takes precedence over _capabilities.

Request Body schema: application/json
required
productId
required
string
optionId
required
string
Value: "DEFAULT"
availabilityId
required
string

A departure id from POST /availability.

required
Array of objects (ReservationUnitItem)

One entry per head. Two adults and a child is three entries. A missing or empty list is UNPROCESSABLE_ENTITY carrying the option's restrictions.

uuid
string <uuid>

Your identifier for this booking, and its idempotency key - repeating it replays the existing booking instead of taking capacity twice. Scoped to your connection's operator.

expirationMinutes
integer

How long to hold. Clamped into [5, 30] rather than rejected, so 0 becomes a five-minute hold. Defaults to the maximum.

notes
string

Free text kept with the booking and echoed on every read. A later confirmation may replace it.

currency
string

Optional assertion. Must equal your connection's currency; a mismatch is BAD_REQUEST rather than a silent re-quote.

testMode
boolean

Marks the booking as a test. It still consumes capacity and issues tickets - cancel it when you are done.

Responses

Request samples

Content type
application/json
{
  • "productId": "9f8b1d24-5c3e-4a71-b0d2-6e5f7a8c9012",
  • "optionId": "DEFAULT",
  • "availabilityId": "2026-07-01T09:00:00+04:00",
  • "unitItems": [
    ],
  • "uuid": "b0a1c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
  • "expirationMinutes": 30,
  • "notes": "Please meet at the Cascade steps.",
  • "currency": "EUR",
  • "testMode": false
}

Response samples

Content type
application/json
{
  • "id": "7a6b5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
  • "uuid": "b0a1c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
  • "testMode": false,
  • "resellerReference": null,
  • "supplierReference": "YOUR-MOUNT-1782292532418",
  • "status": "ON_HOLD",
  • "utcCreatedAt": "2026-06-24T09:15:32Z",
  • "utcUpdatedAt": "2026-06-24T09:15:32Z",
  • "utcExpiresAt": "2026-06-24T10:15:32Z",
  • "utcRedeemedAt": null,
  • "utcConfirmedAt": null,
  • "productId": "9f8b1d24-5c3e-4a71-b0d2-6e5f7a8c9012",
  • "product": {
    },
  • "optionId": "DEFAULT",
  • "option": {
    },
  • "cancellable": true,
  • "cancellation": null,
  • "freesale": false,
  • "availabilityId": "2026-07-01T09:00:00+04:00",
  • "availability": {
    },
  • "contact": {
    },
  • "notes": "Please meet at the Cascade steps.",
  • "deliveryMethods": [
    ],
  • "voucher": null,
  • "unitItems": [
    ],
  • "pricing": {
    }
}

Find bookings by filter

At least one filter is required - resellerReference, supplierReference, localDate, localDateStart or productId. localDateEnd on its own does not count: it bounds a range whose start is missing.

Results are always scoped to your connection. A product mapped to two resellers still lists each booking once.

OCTO 1.0 lists productId and optionId as narrowing parameters; here productId on its own also satisfies the filter requirement.

Authorizations:
bearerAuth
query Parameters
resellerReference
string
Example: resellerReference=VOUCHER-88213

The reference you supplied at confirmation.

supplierReference
string
Example: supplierReference=YOUR-MOUNT-1782292532418

Our order number, as returned in supplierReference.

localDate
string^\d{4}-\d{2}-\d{2}$
Example: localDate=2026-07-01

All bookings departing on this local date.

localDateStart
string^\d{4}-\d{2}-\d{2}$
Example: localDateStart=2026-07-01

First local date of a range.

localDateEnd
string^\d{4}-\d{2}-\d{2}$
Example: localDateEnd=2026-07-31

Last local date of a range. Not a filter on its own.

productId
string
Example: productId=9f8b1d24-5c3e-4a71-b0d2-6e5f7a8c9012

Restrict to one product.

optionId
string
Example: optionId=DEFAULT

Must be DEFAULT when sent.

Value: "DEFAULT"
_capabilities
string
Example: _capabilities=octo/pricing

The same list as the Octo-Capabilities header, for clients that cannot set headers. Ignored when the header is present.

header Parameters
Octo-Capabilities
string
Example: octo/pricing

Capabilities to activate, comma-separated and case-insensitive. Only octo/pricing is supported; unknown ids are ignored. Takes precedence over _capabilities.

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Read one booking

Authorizations:
bearerAuth
path Parameters
uuid
required
string <uuid>
Example: b0a1c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d

The booking's uuid - the one you supplied at reservation, or the value the reservation returned if you supplied none. Not the id field.

query Parameters
_capabilities
string
Example: _capabilities=octo/pricing

The same list as the Octo-Capabilities header, for clients that cannot set headers. Ignored when the header is present.

header Parameters
Octo-Capabilities
string
Example: octo/pricing

Capabilities to activate, comma-separated and case-insensitive. Only octo/pricing is supported; unknown ids are ignored. Takes precedence over _capabilities.

Responses

Response samples

Content type
application/json
{
  • "id": "7a6b5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
  • "uuid": "b0a1c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
  • "testMode": false,
  • "resellerReference": "VOUCHER-88213",
  • "supplierReference": "YOUR-MOUNT-1782292532418",
  • "status": "CONFIRMED",
  • "utcCreatedAt": "2026-06-24T09:15:32Z",
  • "utcUpdatedAt": "2026-06-24T09:22:07Z",
  • "utcExpiresAt": null,
  • "utcRedeemedAt": null,
  • "utcConfirmedAt": "2026-06-24T09:22:07Z",
  • "productId": "9f8b1d24-5c3e-4a71-b0d2-6e5f7a8c9012",
  • "product": {
    },
  • "optionId": "DEFAULT",
  • "option": {
    },
  • "cancellable": true,
  • "cancellation": null,
  • "freesale": false,
  • "availabilityId": "2026-07-01T09:00:00+04:00",
  • "availability": {
    },
  • "contact": {
    },
  • "notes": "Please meet at the Cascade steps.",
  • "deliveryMethods": [
    ],
  • "voucher": null,
  • "unitItems": [
    ],
  • "pricing": {
    }
}

Amend a booking

Amends the booking in place - see Amendments above for the rules. A hold takes every field; a confirmed booking takes resellerReference, notes, contact and emailReceipt only, and refuses a re-seat with UNPROCESSABLE_ENTITY. An empty body answers the booking unchanged, and so does a repeat of an amendment that already applied: it converges and writes nothing.

The seating fields are validated exactly as a reservation is, with the same error codes in the same order, and nothing is written unless the whole amendment is acceptable: a refused amendment leaves the booking exactly as it was.

Authorizations:
bearerAuth
path Parameters
uuid
required
string <uuid>
Example: b0a1c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d

The booking's uuid - the one you supplied at reservation, or the value the reservation returned if you supplied none. Not the id field.

query Parameters
_capabilities
string
Example: _capabilities=octo/pricing

The same list as the Octo-Capabilities header, for clients that cannot set headers. Ignored when the header is present.

header Parameters
Octo-Capabilities
string
Example: octo/pricing

Capabilities to activate, comma-separated and case-insensitive. Only octo/pricing is supported; unknown ids are ignored. Takes precedence over _capabilities.

Request Body schema: application/json
optional
resellerReference
string or null

Replaces the stored reference; null clears it.

productId
string

Another product mapped on this connection. Requires availabilityId.

optionId
string
Value: "DEFAULT"
availabilityId
string

The departure to move the hold onto, from POST /availability.

expirationMinutes
integer

Re-holds the departure for this many minutes from now, clamped into [5, 30].

notes
string or null

Replaces the booking notes; an empty string or null clears them.

emailReceipt
boolean

Accepted for OCTO conformance and ignored.

Array of objects (ConfirmationUnitItem)

When present, the final party - one entry per head.

object (ConfirmationContact)

Replaces the stored contact whole.

Responses

Request samples

Content type
application/json
Example
{
  • "availabilityId": "2026-07-02T09:00:00+04:00",
  • "unitItems": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "7a6b5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
  • "uuid": "b0a1c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
  • "testMode": false,
  • "resellerReference": null,
  • "supplierReference": "YOUR-MOUNT-1782292532418",
  • "status": "ON_HOLD",
  • "utcCreatedAt": "2026-06-24T09:15:32Z",
  • "utcUpdatedAt": "2026-06-24T09:15:32Z",
  • "utcExpiresAt": "2026-06-24T10:15:32Z",
  • "utcRedeemedAt": null,
  • "utcConfirmedAt": null,
  • "productId": "9f8b1d24-5c3e-4a71-b0d2-6e5f7a8c9012",
  • "product": {
    },
  • "optionId": "DEFAULT",
  • "option": {
    },
  • "cancellable": true,
  • "cancellation": null,
  • "freesale": false,
  • "availabilityId": "2026-07-01T09:00:00+04:00",
  • "availability": {
    },
  • "contact": {
    },
  • "notes": "Please meet at the Cascade steps.",
  • "deliveryMethods": [
    ],
  • "voucher": null,
  • "unitItems": [
    ],
  • "pricing": {
    }
}

Confirm a held booking

Turns an unexpired hold into a sale and issues one ticket per unit item. Only a hold can be confirmed; anything else - confirmed, cancelled, expired, or a cancellation that raced this call - answers INVALID_BOOKING_UUID.

A unitItems set that differs from the held party is applied as an amendment first, so it can be refused exactly as a PATCH changing the party would be - see Confirmation above.

Confirming twice is safe and issues no second set of tickets.

Authorizations:
bearerAuth
path Parameters
uuid
required
string <uuid>
Example: b0a1c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d

The booking's uuid - the one you supplied at reservation, or the value the reservation returned if you supplied none. Not the id field.

query Parameters
_capabilities
string
Example: _capabilities=octo/pricing

The same list as the Octo-Capabilities header, for clients that cannot set headers. Ignored when the header is present.

header Parameters
Octo-Capabilities
string
Example: octo/pricing

Capabilities to activate, comma-separated and case-insensitive. Only octo/pricing is supported; unknown ids are ignored. Takes precedence over _capabilities.

Request Body schema: application/json
optional
object (ConfirmationContact)

Stored whole and echoed as sent. Every field is optional; a name sent one way is derived the other.

resellerReference
string

Your booking reference. Sending a different one on a repeat confirmation replaces the stored value; repeating or omitting it changes nothing.

notes
string

Replaces the notes sent at reservation. Omitting the field leaves them unchanged; an empty string clears them.

Array of objects (ConfirmationUnitItem)

When present, the final party: the hold is re-sized, re-priced and re-seated before it is confirmed - see Amendments. Omitted, the held party is confirmed as it stands.

Responses

Request samples

Content type
application/json
{
  • "contact": {
    },
  • "resellerReference": "VOUCHER-88213"
}

Response samples

Content type
application/json
{
  • "id": "7a6b5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
  • "uuid": "b0a1c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
  • "testMode": false,
  • "resellerReference": "VOUCHER-88213",
  • "supplierReference": "YOUR-MOUNT-1782292532418",
  • "status": "CONFIRMED",
  • "utcCreatedAt": "2026-06-24T09:15:32Z",
  • "utcUpdatedAt": "2026-06-24T09:22:07Z",
  • "utcExpiresAt": null,
  • "utcRedeemedAt": null,
  • "utcConfirmedAt": "2026-06-24T09:22:07Z",
  • "productId": "9f8b1d24-5c3e-4a71-b0d2-6e5f7a8c9012",
  • "product": {
    },
  • "optionId": "DEFAULT",
  • "option": {
    },
  • "cancellable": true,
  • "cancellation": null,
  • "freesale": false,
  • "availabilityId": "2026-07-01T09:00:00+04:00",
  • "availability": {
    },
  • "contact": {
    },
  • "notes": "Please meet at the Cascade steps.",
  • "deliveryMethods": [
    ],
  • "voucher": null,
  • "unitItems": [
    ],
  • "pricing": {
    }
}

Cancel a booking

Releases the capacity and, for a confirmed booking, retires its tickets. The cancellation policy does not gate this call - a late cancellation is priced by its refund tier, not refused, and same-day cancellations are allowed.

The one guard is a departure day that has already passed; send force: true to cancel one anyway. Cancelling an already-cancelled booking succeeds and returns it unchanged.

Authorizations:
bearerAuth
path Parameters
uuid
required
string <uuid>
Example: b0a1c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d

The booking's uuid - the one you supplied at reservation, or the value the reservation returned if you supplied none. Not the id field.

query Parameters
_capabilities
string
Example: _capabilities=octo/pricing

The same list as the Octo-Capabilities header, for clients that cannot set headers. Ignored when the header is present.

header Parameters
Octo-Capabilities
string
Example: octo/pricing

Capabilities to activate, comma-separated and case-insensitive. Only octo/pricing is supported; unknown ids are ignored. Takes precedence over _capabilities.

Request Body schema: application/json
optional
reason
string

Kept on the booking and echoed in cancellation.reason.

force
boolean

Cancel even though the departure day has already passed.

Responses

Request samples

Content type
application/json
{
  • "reason": "Customer changed plans"
}

Response samples

Content type
application/json
{
  • "id": "7a6b5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
  • "uuid": "b0a1c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
  • "testMode": false,
  • "resellerReference": "VOUCHER-88213",
  • "supplierReference": "YOUR-MOUNT-1782292532418",
  • "status": "CANCELLED",
  • "utcCreatedAt": "2026-06-24T09:15:32Z",
  • "utcUpdatedAt": "2026-06-25T11:04:18Z",
  • "utcExpiresAt": null,
  • "utcRedeemedAt": null,
  • "utcConfirmedAt": "2026-06-24T09:22:07Z",
  • "productId": "9f8b1d24-5c3e-4a71-b0d2-6e5f7a8c9012",
  • "product": {
    },
  • "optionId": "DEFAULT",
  • "option": {
    },
  • "cancellable": false,
  • "cancellation": {
    },
  • "freesale": false,
  • "availabilityId": "2026-07-01T09:00:00+04:00",
  • "availability": {
    },
  • "contact": {
    },
  • "notes": "Please meet at the Cascade steps.",
  • "deliveryMethods": [
    ],
  • "voucher": null,
  • "unitItems": [
    ],
  • "pricing": {
    }
}

Extend a hold

Adds the requested minutes, clamped into [5, 30], to the hold's current expiry - so expirationMinutes: 0 adds five minutes rather than expiring the hold on the spot. Omitting it adds the maximum. Extensions stop at the hold's maximum lifetime, 180 minutes after the reservation: an extension the ceiling cuts short lands on it, and one made at the ceiling is UNPROCESSABLE_ENTITY. Two extensions sent together both count.

Only an unexpired hold can be extended: a confirmed, cancelled or already-expired booking has nothing left to hold and answers INVALID_BOOKING_UUID.

Authorizations:
bearerAuth
path Parameters
uuid
required
string <uuid>
Example: b0a1c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d

The booking's uuid - the one you supplied at reservation, or the value the reservation returned if you supplied none. Not the id field.

query Parameters
_capabilities
string
Example: _capabilities=octo/pricing

The same list as the Octo-Capabilities header, for clients that cannot set headers. Ignored when the header is present.

header Parameters
Octo-Capabilities
string
Example: octo/pricing

Capabilities to activate, comma-separated and case-insensitive. Only octo/pricing is supported; unknown ids are ignored. Takes precedence over _capabilities.

Request Body schema: application/json
optional
expirationMinutes
integer

Minutes added to the current expiry, clamped into [5, 30]. Omitting it adds the maximum. Never past the hold's maximum lifetime, 180 minutes after the reservation.

Responses

Request samples

Content type
application/json
{
  • "expirationMinutes": 30
}

Response samples

Content type
application/json
{
  • "id": "7a6b5c4d-3e2f-4a1b-8c9d-0e1f2a3b4c5d",
  • "uuid": "b0a1c2d3-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
  • "testMode": false,
  • "resellerReference": null,
  • "supplierReference": "YOUR-MOUNT-1782292532418",
  • "status": "ON_HOLD",
  • "utcCreatedAt": "2026-06-24T09:15:32Z",
  • "utcUpdatedAt": "2026-06-24T09:38:45Z",
  • "utcExpiresAt": "2026-06-24T10:45:32Z",
  • "utcRedeemedAt": null,
  • "utcConfirmedAt": null,
  • "productId": "9f8b1d24-5c3e-4a71-b0d2-6e5f7a8c9012",
  • "product": {
    },
  • "optionId": "DEFAULT",
  • "option": {
    },
  • "cancellable": true,
  • "cancellation": null,
  • "freesale": false,
  • "availabilityId": "2026-07-01T09:00:00+04:00",
  • "availability": {
    },
  • "contact": {
    },
  • "notes": "Please meet at the Cascade steps.",
  • "deliveryMethods": [
    ],
  • "voucher": null,
  • "unitItems": [
    ],
  • "pricing": {
    }
}