ResytechResytech Docs

OTA Partner Integration

Embed the Resytech booking flow on an OTA partner's site with operator-configured attribution, pricing, and origin-allowlist enforcement.

When an Online Travel Agency (OTA) embeds Resytech booking on its own website, the operator can configure attribution and pricing rules that are applied automatically to every booking made through that OTA's integration. The OTA partner receives a single short code from the operator and adds it to the embed; everything else (markup, revenue share, fee bumps, allowed origins) is enforced server-side.

Prerequisites

The operator configures the integration once via the Resytech dashboard, providing the OTA partner with:

  • A code — a URL-safe slug shared with the partner (e.g. viator, getyourguide)

The partner only needs the code. Everything else — allowed embedding origins, active status, and optional pricing rules (markup, revenue share, platform-fee or location-fee bumps) — is operator-side configuration enforced server-side. Origin and active checks run at init; pricing rules are baked into the session token and applied automatically on every cart and checkout call.

Integration Options

1. Script Tag

Add data-resytech-ota-code to the embed script. Auto-init forwards it on the first /initialize call:

<script
  src="https://js.resytech.com/latest/resytech.js"
  data-resytech-location-id="YOUR_LOCATION_ID"
  data-resytech-base-url="https://booking.youroperator.com"
  data-resytech-ota-code="viator">
</script>

This is the simplest path — no JavaScript required. The auto-init'd ResytechClient instance carries the OTA code on every booking opened via data-resytech-activity-id buttons, window.resytech.showUI(), etc.

2. ResytechClient (programmatic)

Pass otaCode in the constructor, or set it at runtime with setOtaCode():

const client = new ResytechClient({
  locationId: 'YOUR_LOCATION_ID',
  baseUrl: 'https://booking.youroperator.com',
  otaCode: 'viator'
});

// Or change it later — applies to the next showUI() call.
client.setOtaCode('getyourguide');

Use this path when running multiple ResytechClient instances on the same page — e.g. a marketplace embedding several operator partners, each with its own OTA code.

3. ResytechApi (custom flow)

Pass otaCode directly in the initialize request:

const api = new ResytechApi();

const init = await api.initialization.initialize({
  identifier: 'my-session-id',
  otaCode: 'viator'
});

What Happens at Init

The server validates the otaCode at initialize time:

  1. Looks up the OTA by (location, code).
  2. Verifies the OTA is active.
  3. Verifies the embedding origin (the booking-UI-supplied parentOrigin if running inside an iframe, otherwise the request's Origin header) matches one of the OTA's allowed origins — or is localhost (developer convenience).

On failure (unknown code, inactive, or unauthorized origin), the response is:

const init = await api.initialization.initialize({
  identifier: 'session-1',
  otaCode: 'mistype'
});

if (!init.success) {
  console.error(init.message); // e.g. "Unknown OTA." / "OTA is not active." /
                               //      "Origin is not authorized for this OTA."
}

No token is minted on failure — the booking flow can't proceed. Surface the message so misconfiguration is visible during embed setup.

On success, the operator's configured pricing rules are baked into the returned JWT. Every subsequent cart preview, checkout, and booking call automatically applies the OTA's pricing — no per-request forwarding needed.

Iframe Mode and the Origin Header

ResytechClient.showUI() opens the booking flow in an iframe served from the operator's BookingDomain (e.g. booking.youroperator.com). Once the iframe is loaded, the API calls happen from inside it — so the browser sets Origin to the iframe's own host, not the OTA partner's site.

That means an init request from a Viator-launched iframe would naturally arrive with:

Origin: https://booking.youroperator.com

…not Origin: https://www.viator.com. The Origin header alone can't tell us which OTA partner actually embedded the flow.

To bridge this, the booking UI discovers the embedding parent's origin before calling /initialize and forwards it as a parentOrigin field. The server prefers parentOrigin over the Origin header for OTA allowlist enforcement when present.

Detection runs in three layers, first hit wins:

  1. window.location.ancestorOrigins[0] — browser-attested, unforgeable from the parent's side. Chromium and Safari only (Firefox declined for fingerprinting reasons).
  2. document.referrer — set by every browser to the embedding page's URL. The host is extracted and normalized. Best-effort: the embedder can strip it via <iframe referrerpolicy>, a Referrer-Policy header, or <meta name="referrer">.
  3. postMessage handshake — the booking UI sends a resytech:get-parent-origin message to window.parent; the Resytech client lib (running on the OTA partner's page) replies with resytech:parent-origin. The browser stamps the reply's event.origin with the parent's actual origin — the script can't lie about it. Times out after 500ms.

If all three fail, parentOrigin is omitted and the server falls back to the Origin header (which won't pass the OTA allowlist for any non-localhost iframe embed — surfacing the misconfig to the operator).

Embed styleOrigin source usedAllowed by
Iframe via ResytechClient.showUIparentOrigin (discovered)Must be in the OTA's AllowedOrigins
Direct API (ResytechApi from OTA's page)Request Origin headerMust be in the OTA's AllowedOrigins
Local dev (any embed style)parentOrigin if iframed, else Originlocalhost/127.0.0.1/::1 auto-allow

The trust model is the same in both modes: a real browser sets the value honestly; a scripted attacker forging the value gains nothing because the OTA code only inflates customer prices and revenue share goes to the OTA partner, not to the attacker.

Customer Experience

When an OTA-attributed customer browses the booking flow:

  • Marked-up prices are shown throughout cart and checkout if the operator configured a markup percentage.
  • Coupons are disabled for the entire session — operator coupon codes don't apply to OTA-driven bookings.
  • All other UX is identical to a direct booking — same checkout, same payment, same confirmation.

The customer never sees the OTA code; attribution is invisible to them.

Booking Records

Every OTA-attributed booking is stamped with:

  • The source field set to the operator's configured booking source for that OTA (or the code itself if no override is set), so existing reports filtering on Booking.Source continue to work.
  • Snapshots of the OTA UUID, markup %, revenue share %, and any fee bumps at the moment of booking — operator reporting reflects the rates that were active when the customer paid, even if the operator changes them later.

Pricing Math

The three price-affecting knobs compose in a specific order. All bumps are multiplicativenew = base × (1 + percent / 100) — so entering 50 means "multiply by 1.5", not "add 50 percentage points".

For an OTA booking with markup = 20%, customer-fee bump = 50%, application-fee bump = 50% against a $100 base activity, a 6% customer service fee, and a 6% Resytech application fee:

StepFormulaResult
1. Markup applied to subtotal components100 × 1.20$120.00
2. Customer fee scales off marked-up subtotal120 × 6%$7.20
3. Customer fee then bumped by additive7.20 × 1.50$10.80
4. Customer pays120 + 10.80$130.80
5. Resytech app fee on customer total130.80 × 6%$7.85
6. App fee bumped by additive7.85 × 1.50$11.78

Two things to notice:

  1. Markup compounds into Resytech's cut. Inflating the subtotal also inflates the application-fee base. So a 20% markup + 50% application-fee bump means Resytech collects more than (base × 6%) × 1.50 — it collects (marked-up total × 6%) × 1.50. This is intentional: Resytech's cut tracks the actual money flowing through the operator's Stripe account.
  2. Each customer payment carries the bumps. Down-payment balances, addon invoices paid through the customer portal, and post-service charges all collect the bumped application fee. Reschedules to the same OTA continue to apply the markup and customer-fee bump for the new slot. The per-booking snapshot is the source of truth — changing the OTA's live config later doesn't rewrite history.

Coupons are disabled for the entire OTA session. There's no "discount on top of markup" composition.

Common Failure Modes

Init messageCauseFix
Unknown OTA.The otaCode doesn't match any OTA configured for this locationVerify the code with the operator; check for typos / case (codes are lowercased)
OTA is not active.The OTA exists but the operator has disabled itOperator re-enables in the dashboard
Origin is not authorized for this OTA.The discovered parentOrigin (iframe) or request Origin header (direct API) isn't in the OTA's allowed-origins list.Operator adds the embed origin to the allowlist
OTA attribution requires an Origin header.The init request had no Origin headerAlmost always a non-browser caller; browsers always send Origin for cross-origin requests

Caveats

  • Origin enforcement happens at init time only. Once a JWT is minted, subsequent calls trust the token. Misconfigured origins fail at init — the flow won't silently fall back to direct pricing.
  • Mid-session OTA disable doesn't affect existing sessions. Existing tokens remain valid until they expire. Disabling an OTA only blocks new init calls.
  • Markup is applied to the subtotal components. Activity, equipment, and addon prices are inflated; fees, taxes, and trip protection scale naturally because they're calculated against the inflated subtotal.
  • Localhost is always allowed. Origins with hostname localhost, 127.0.0.1, or ::1 bypass the allowlist on any port so developers can test OTA flows without operator-side configuration. Production embeds must still match an entry in the allowlist.
  • The OTA partner's domain belongs in AllowedOrigins regardless of embed style. For direct-API embeds it's matched against the request's Origin header; for iframe embeds it's matched against the discovered parentOrigin.

On this page