# OCTO API reference (/docs/plugins/octo-reference)



This page is for the developer **on the reseller side** — an OTA, marketplace, or channel manager connecting to a Resytech operator through [OCTO](https://octo.travel), the open connectivity standard for tours, activities, and attractions. Operator-side setup lives on the [plugin page](/docs/plugins/octo).

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 [#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, availability
* `octo:book` — the entire `/bookings` surface

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.

<Callout type="warn">
  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.
</Callout>

Errors [#errors]

Failures return the OCTO envelope:

```json
{ "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 [#products]

```
GET /supplier
GET /products
GET /products/{id}
```

A **product** is one bookable activity. Mapping notes:

* `availabilityType` is always `START_TIME`; `availabilityRequired` is always `true` — you must obtain an `availabilityId` before 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 as `unitId`); flat-priced products expose a single unit with id `guest`.
* `availabilityLocalStartTimes` lists 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`/`maxUnits` on the option reflect the operator's per-booking guest minimum/limit.
* `deliveryMethods` is `["VOUCHER"]` with `redemptionMethod: "MANIFEST"` — the traveler is on the operator's manifest; there are no per-ticket barcodes.

Availability [#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 [#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 [#reserve]

```json
{
  "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": "…" }
}
```

* `uuid` is **your idempotency key**. Retrying with the same uuid returns the reservation's current state and never double-books. Always send one.
* One `unitItems` entry per traveler. `unitId` is a unit id from the product (`guest` for single-unit products).
* `expirationMinutes` is clamped to **5–1440**; default **30**. The response's `utcExpiresAt` is authoritative.
* `contact` is 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 `utcExpiresAt` and the inventory frees itself; the booking then reads `EXPIRED`.

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-extend-cancel]

* **Confirm** finalizes the booking (`CONFIRMED`). Include `contact` — 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 an `ON_HOLD` booking. Expired holds cannot be extended — reserve again.
* **Cancel** (`{ "reason": "…" }`) works on holds and confirmed bookings. The response's `cancellation.refund` is 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 [#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) [#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.
