Experiences API overview
Every business capability in Kaptio Experiences is a documented REST/JSON endpoint — the three tenant apps are pure consumers of this API, so anything the UI can do, an integration can do. The surface is organized by persona:
- Guest API — trip context, catalog, orders, payments, vouchers
- Tour Director API — dashboard, assisted sales, cash, refunds, availability, trip operations
- Operations & Finance API — oversight, vouchers, settlements, finance export, reporting, audit
Base path and versioning
/api/optionals/v1
The version lives in the path. Within a version, changes are additive only — new endpoints, new optional fields, new enum values documented as they appear. A breaking change means a new version, with the previous version supported through a communicated migration window.
Tenant resolution
The API is multi-tenant. Every request identifies its tenant with a tenantId — as a query parameter on GET requests, or as a body field on POST/PATCH requests where noted:
GET /api/optionals/v1/config?tenantId=meridian
An unknown tenant returns 404 tenant_not_found; a missing tenant returns 400 tenant_required.
Public tenant config
One endpoint is public (no authentication) — it serves the UI shell its brands, themes, and feature flags, and is safe to call from anywhere:
GET /config?tenantId=meridian
{
"tenantId": "meridian",
"productId": "td-optionals",
"provider": "mock",
"currencies": ["GBP", "USD", "EUR"],
"brands": [ { "id": "meridian_uk", "name": "Meridian Travel UK", "theme": { "...": "..." } } ],
"features": { "gratuities": true, "vouchers": true, "cash_payments": true, "split_payments": true, "pre_trip_purchase": true },
"payment": { "gateway": "simulated" },
"ui": { "title": "Meridian Experiences" }
}
It never includes secrets or integration settings.
Authentication
Access is role-scoped, and every authenticated request carries a short-lived bearer token:
Authorization: Bearer <token>
| Consumer | How they authenticate |
|---|---|
| Guests | Guests never register. They reach their trip-scoped session either through a magic link delivered by email (an opaque single-use code exchanged server-side for the session — the token never appears in the URL; see how magic links work) or by booking reference + surname via POST /auth/guest, matched against the booking records for the trip. Both issue the same short-lived, trip-scoped token (12-hour lifetime — a full touring day). Which entry points are active is agreed per tenant. |
| Tour Directors, Operations & Finance | Keycloak OIDC. Browser clients use the OAuth 2.0 Authorization Code flow with PKCE, with MFA enforced by policy. Keycloak federates to your corporate identity provider (for example Microsoft Entra ID), so staff sign in with their existing SSO accounts. The API validates Keycloak-issued access tokens — it never accepts or stores staff passwords. Issuer, audience, and role mappings for your realm are provisioned at onboarding. |
| Programmatic consumers | OAuth 2.0 client credentials against Keycloak for server-to-server integrations (for example an integration-platform counterparty), over TLS 1.2+. |
Sessions carry the role, tenant, and subject; guest sessions are additionally trip-scoped — a guest token can never read or pay against another party's orders, enforced server-side on every route. Role-based authorization gates every endpoint: a Tour Director token cannot call admin endpoints, and a Tour Director can only operate trips assigned to them (403 not_your_trip otherwise).
GET /me returns the current session's claims — useful for session restoration in integrations and UIs.
Tenants running provider: mock (evaluation and training) replace the Keycloak staff flows with sandbox credential logins so a walkthrough needs no identity setup: POST /auth/td (Tour Director code + pin) and POST /auth/admin (email + password). These endpoints exist only on sandbox tenants and are not part of the production contract. Booking reference + surname guest sign-in works in both modes; the magic-link entry point is provisioned for live tenants.
Collections and pagination
Most collections are naturally bounded: guest and Tour Director endpoints are scoped to a single trip or person, and reports and settlement runs take explicit from/to periods. For the tenant-wide admin collections (trips, vouchers, settlements, the audit trail), scope requests with the documented filters; the audit trail additionally accepts a limit. A uniform pagination convention — limit plus an opaque continuation cursor, with a server-side maximum page size — is being introduced across the tenant-wide collections as an additive change within v1, so treat collection responses as pageable when building integrations. CSV exports are bounded by the same filters and periods.
Conventions
| Convention | Detail |
|---|---|
| Money | All monetary amounts are integers in minor units (pence/cents) of the trip currency. 12500 with currency GBP is £125.00. Refunds and chargebacks are stored as positive amounts with the transaction type set accordingly. |
| Dates and times | Dates are ISO YYYY-MM-DD; timestamps are ISO 8601 UTC (2026-07-12T08:45:00.000Z). |
| Identifiers | Opaque strings. Human references exist alongside ids where people need them — order references like OPT-260712-8341, settlement references like SET-2026W27-TD4471-EUR. |
| CSV exports | Every report endpoint also serves CSV: append ?format=csv and the response switches to text/csv with a download disposition. Tour Director sales export is CSV-native. |
| Validation | Request bodies are schema-validated. Invalid input returns 400 with code: validation_error and a human-readable message for the first failing field. |
Error shape
Errors are uniform across the whole surface:
{ "error": "Units are not available for this experience", "code": "unavailable" }
| Status | Codes you will see | Meaning |
|---|---|---|
400 | validation_error, tenant_required, reason_required | Malformed or incomplete request |
401 | unauthorized, booking_not_found, invalid_credentials | No/expired token, or login failed (login errors are deliberately uniform — they never reveal which credential field was wrong) |
402 | payment_failed, gateway decline codes | Payment was attempted and declined |
403 | not_your_trip | Authenticated, but the resource belongs to another Tour Director's trip |
404 | not_found, tenant_not_found, trip_not_found | Resource does not exist (or is not visible to this session) |
409 | unavailable, confirmation_required | Allotment/cut-off conflict, or a destructive change needs an explicit confirm: true |
422 | voucher validation codes (voucher_expired, voucher_redeemed, …) | The request was understood but the business rule rejected it |
500 | internal_error | Unexpected failure — logged with request context, never with payload details |
Operability
The service exposes the platform-standard endpoints outside the API path: /health (liveness), /ready (readiness), and /metrics (Prometheus — request counts, durations, and error rates per endpoint). Rate limits and quotas for programmatic consumers are agreed per integration contract and enforced with the platform's standard middleware.
Audit
Every state-changing call — logins, orders, payments, refunds, availability changes, voucher actions, settlement actions — writes an audit event with actor type, actor id, action, entity, and timestamp. The trail is queryable via the admin audit endpoint.