OCTO API reference
Authentication, endpoints, booking lifecycle, and error contract for resellers integrating against Resytech's OCTO supplier API.
This page is for the developer on the reseller side — an OTA, marketplace, or channel manager connecting to a Resytech operator through OCTO, the open connectivity standard for tours, activities, and attractions. Operator-side setup lives on the plugin page.
Resytech implements the OCTO core specification — supplier, products, availability, and the full booking lifecycle. If you have an existing OCTO client, point it at the endpoint below with your token and it should work unmodified.
Connection
| Endpoint | https://octo.resytech.com (no trailing slash; also returned by GET /supplier) |
| Authentication | Authorization: Bearer {token} — the operator issues you an rpat_… token from their dashboard |
| Content type | application/json |
| Capabilities | Send Octo-Capabilities if you like; no optional capabilities are implemented yet, so the response header always echoes the empty set. Core fields only. |
Your token is scoped to one operator location — the supplier — and carries scopes granted by the operator:
octo:read— supplier, products, availabilityocto:book— the entire/bookingssurface
All datetimes are ISO 8601. Availability times carry the local UTC offset of the supplier's timezone (e.g. 2026-09-15T14:00:00-04:00); the product's timeZone field is the IANA id. utc… fields are UTC with a Z suffix.
Rate limit: 120 requests per minute per token. Exceeding it returns 429; back off and retry.
Access is controlled by the operator: they can pause your account or disable the plugin at any time, which makes every request fail with 403 FORBIDDEN immediately. Treat a sudden 403 as "contact the operator", not as a retryable error.
Errors
Failures return the OCTO envelope:
{ "error": "INVALID_PRODUCT_ID", "errorMessage": "The productId was not found", "productId": "…" }| Code | HTTP | When |
|---|---|---|
UNAUTHORIZED | 401 | No bearer token sent |
FORBIDDEN | 403 | Invalid/revoked token, paused reseller, plugin disabled, or missing scope |
INVALID_PRODUCT_ID / INVALID_OPTION_ID / INVALID_UNIT_ID / INVALID_AVAILABILITY_ID / INVALID_BOOKING_UUID | 400 | The referenced id doesn't exist or isn't yours |
BAD_REQUEST | 400 | Malformed body, bad dates, oversized range |
UNPROCESSABLE_ENTITY | 400 | Valid request that can't be honored — departure sold out, party too large, hold expired |
TOO_MANY_REQUESTS | 429 | Rate limit |
INTERNAL_SERVER_ERROR | 500 | Our fault — safe to retry with backoff |
Products
GET /supplier
GET /products
GET /products/{id}A product is one bookable activity. Mapping notes:
availabilityTypeis alwaysSTART_TIME;availabilityRequiredis alwaystrue— you must obtain anavailabilityIdbefore booking.- Options are the activity's durations (a 60-minute and a 120-minute variant are two options). One option is flagged
default. - Units are guest types. Per-person products list the operator's demographics (
ADULT,CHILD, …, with the demographic's uuid asunitId); flat-priced products expose a single unit with idguest. availabilityLocalStartTimeslists the schedule's possible local departure times for that option; the availability endpoints are the source of truth for what's actually open.restrictions.minUnits/maxUnitson the option reflect the operator's per-booking guest minimum/limit.deliveryMethodsis["VOUCHER"]withredemptionMethod: "MANIFEST"— the traveler is on the operator's manifest; there are no per-ticket barcodes.
Availability
POST /availability/calendar — one object per day, for range views (≤ 92 days/request)
POST /availability — one object per departure; REQUIRED before booking (≤ 31 days/request)Both take { productId, optionId } plus either localDate, or localDateStart + localDateEnd (yyyy-MM-dd). The availability check also accepts availabilityIds to re-check specific departures.
Departure objects:
| Field | Meaning |
|---|---|
id | The availabilityId you pass to book. Deterministic per departure, but always re-check before booking — it's revalidated server-side either way. |
localDateTimeStart / localDateTimeEnd | Departure window with local offset |
status | AVAILABLE, LIMITED (≤ 3 spots), SOLD_OUT, or CLOSED (operator blackout) |
vacancies | Remaining spots when the product tracks them; null when untracked |
maxUnits | Most units a single booking can carry right now |
utcCutoffAt | Booking deadline (the departure instant — sales cutoffs are already applied to status) |
Calendar responses are cached for up to 60 seconds — fine for range views, which is why booking requires the uncached availability check.
Booking lifecycle
Reserve → confirm, with an expiring hold in between:
POST /bookings octo:book — reserve (ON_HOLD)
POST /bookings/{uuid}/confirm octo:book — ON_HOLD → CONFIRMED
POST /bookings/{uuid}/extend octo:book — push the hold deadline
POST /bookings/{uuid}/cancel octo:book — cancel a hold or a confirmed booking
GET /bookings/{uuid} octo:book
GET /bookings?resellerReference=&localDate= octo:book — your bookings (newest 200)Reserve
{
"uuid": "9be6f9c7-…",
"productId": "…", "optionId": "…",
"availabilityId": "…",
"unitItems": [ { "unitId": "…adult uuid…" }, { "unitId": "…adult uuid…" }, { "unitId": "…child uuid…" } ],
"expirationMinutes": 30,
"resellerReference": "YOUR-ORDER-123",
"contact": { "fullName": "…", "emailAddress": "…", "phoneNumber": "…" }
}uuidis your idempotency key. Retrying with the same uuid returns the reservation's current state and never double-books. Always send one.- One
unitItemsentry per traveler.unitIdis a unit id from the product (guestfor single-unit products). expirationMinutesis clamped to 5–1440; default 30. The response'sutcExpiresAtis authoritative.contactis optional at reserve — send it at confirm if you don't have it yet.- A successful reserve blocks the operator's live inventory immediately. An unconfirmed hold expires automatically at
utcExpiresAtand the inventory frees itself; the booking then readsEXPIRED.
The response is the OCTO booking object: status (ON_HOLD), supplierReference (the operator-side booking id — quote it in support conversations), your resellerReference, utcExpiresAt, availability, and your unitItems.
Confirm, extend, cancel
- Confirm finalizes the booking (
CONFIRMED). Includecontact— the traveler's name, email, and phone attach to the operator's booking and drive their confirmations and manifest. Confirming an already-confirmed booking is idempotent. - Extend (
{ "expirationMinutes": 60 }) pushes the hold deadline of anON_HOLDbooking. Expired holds cannot be extended — reserve again. - Cancel (
{ "reason": "…" }) works on holds and confirmed bookings. The response'scancellation.refundis always"NONE": you are the merchant of record — refunding the traveler is on your side; the cancel releases the operator's inventory and their books.
Statuses
| Status | Meaning |
|---|---|
ON_HOLD | Reserved, inventory blocked, awaiting confirm before utcExpiresAt |
CONFIRMED | Finalized |
EXPIRED | Hold lapsed without confirmation; inventory released |
CANCELLED | Cancelled by you (or by the operator on their side) |
Not implemented (yet)
Optional OCTO capabilities — octo/pricing, octo/content, octo/notifications, pickups — are not yet advertised; requesting them initializes nothing. Availability-change push notifications are on the roadmap; until then, poll the calendar endpoint within the rate limit.
