Skip to main content

OTA channels

Last updated: October 27, 2026

OTA channels

Inbound booking surface for Online Travel Agencies (Viator-class partners). The contract is provider-agnostic and stable — the same paths serve every provider, {provider} selects the payload mapping. Viator ships as a documented mapping skeleton, generic speaks this contract as-is for bespoke integrations, GetYourGuide answers 501 until certification. Inbound only: the app never pushes availability to the partner.

Endpoints

GET  /api/ota/{provider}/availability?product_id={id}&date=YYYY-MM-DD[&to_date=YYYY-MM-DD]
POST /api/ota/{provider}/bookings
POST /api/ota/{provider}/bookings/{external_reference}/cancel

{provider} is one of viator, getyourguide, generic. Unknown or disabled providers return 404.

Rate limit: 60 requests per minute per connection.

Authentication

Every request carries two headers:

  • X-Ota-Connection: {public_id} — the connection's public id, shown on the OTA channels admin page. Never the internal id.
  • X-Ota-Signature: {hex} — hex HMAC-SHA256 keyed with the connection's signing secret (shown once when the connection is created or rotated) over:
{METHOD}\n{REQUEST-URI}\n{RAW-BODY}

REQUEST-URI is the exact request target including the query string — /api/ota/viator/availability?product_id=P-1&date=2027-01-15. RAW-BODY is the unparsed request body (empty string on GET).

$signature = hash_hmac(
    'sha256',
    "POST\n/api/ota/viator/bookings\n".$rawJsonBody,
    $webhookSecret,
);
# worked example (generic provider)
curl -X POST https://{host}/api/ota/generic/bookings \
  -H "X-Ota-Connection: 9f4c…" \
  -H "X-Ota-Signature: $(php -r 'echo hash_hmac("sha256", "POST\n/api/ota/generic/bookings\n".$argv[1], "SECRET");' "$body")" \
  -H "Content-Type: application/json" \
  --data-raw "$body"

Errors: unknown connection 404 {"error":"unknown_connection"}; missing or mismatched signature 401 {"error":"bad_signature"}. The company is always resolved from the connection — payloads cannot select a tenant.

Product mapping

Partners sell external_product_id values. Each id maps in the admin UI to exactly one bookable — an event or an area — plus an optional price_key (null uses the bookable's default price resolution). Unmapped ids return 404 {"error":"unknown_product"}.

Availability

GET /api/ota/generic/availability?product_id=P-1&date=2027-01-15

{
  "product_id": "P-1",
  "days": [
    {"date": "2027-01-15", "available": true, "remaining": 7}
  ]
}
  • available false means closed, sold-out, or outside the sellable window.
  • remaining is the remaining capacity; null means uncapped.
  • Area products additionally return windows: [{from, to, remaining}] — the day's sellable segments. Each segment carries its own remaining (per-segment capacity minus the simultaneous peak, bounded by every daily pool the segment touches); the day-level remaining is the largest party that fits inside ONE segment, not the day's total.
  • An event's remaining is bounded by EVERY date of its run — a booking consumes the pool of each day the event occupies.
  • An event without an end runs on its start day only.
  • to_date extends the range up to 31 days.

Create a booking

POST /api/ota/generic/bookings

{
  "product_id": "P-1",
  "external_reference": "partner-ref-1001",
  "customer": {"name": "Mario Bianchi", "email": "[email protected]", "phone": "+39002"},
  "number": 2,
  "from": "2027-01-15T10:00:00+00:00",
  "to": "2027-01-15T11:00:00+00:00",
  "date": "2027-01-15"
}

from/to apply to area products (sellable windows); date is the calendar day for event products and must fall inside the event's run.

Event products book the leaf the channel can address: a bare event books directly, an event with Times books the Time whose slot starts at from (from is then required — missing or unmatched returns 422), and an event with Seats reports available: false instead of advertising a slot the partner could never book (seat choice stays on the widget).

Response:

{"booking": {"reference": "partner-ref-1001", "status": "confirmed",
             "booking_code": "BK-AB12", "number": 2,
             "created_at": "2027-01-10T09:30:00+00:00"}}

Bookings arrive free — if the mapped bookable would require payment the booking is refused with 422 {"error":"payment_required"} and the held capacity is released. Partner-collected money reconciles outside the app.

Validation failures return 422 with error (booking_failed) and a fields map. Replays of a failed reference return the recorded failure.

Idempotency

external_reference is the idempotency key, unique per connection. A retry re-POSTing the same reference returns the recorded outcome — 200 with the same booking payload — without touching capacity again. Never reuse a reference across different orders.

Cancellation

POST /api/ota/{provider}/bookings/{external_reference}/cancel

Cancels the underlying booking through the normal cancel spine and flips the ledger row to cancelled. Unknown reference → 404 {"error":"unknown_reference"}. Cancelling an already-cancelled reference is a 200 no-op. Settlement of partner-collected money is the partner's business — the endpoint never triggers a refund.

Partner onboarding checklist

  1. Owner/admin creates a connection on the OTA channels page (/ota, ota package feature) and copies the public_id.
  2. Rotate/copy the signing secret once — it is write-only afterwards.
  3. Map each partner external_product_id to an event or area (+ optional price_key).
  4. Partner signs METHOD\nREQUEST-URI\nRAW-BODY per call, sends both headers.
  5. Smoke-test availability → bookings → cancel with a throwaway reference, then go live.

Cookies and privacy

We use essential cookies to keep the application secure and working correctly. Optional cookies will only be used with your consent.

Read the privacy policy Read the cookie policy

Essential

Essential cookies are required for the website to function and cannot be switched off.

Always on

Functional

Functional cookies enable enhanced functionality and personalisation, such as remembering your language and preferences.

Analytics

Analytics cookies help us understand how visitors interact with the application so we can improve it.

Marketing

Marketing cookies are used to track visitors across websites to display relevant and engaging advertisements.