# Kibble installation contract: section 22 of 29 (guide version 3)

> This is **one section only**, not the whole contract. Complete document: https://www.usekibble.dev/AI/guide.md

## 22. Website hosts, external connections and first-party gateways {#gateways}

### Two kinds of host

|  | Website host | External connection |
| --- | --- | --- |
| What it is | A host the client controls: `www.example.com`, or `book.example.com` on the client's own DNS. | A booking engine or checkout run by a provider, possibly on a host shared with the provider's other customers (for example `guest.rezstream.com`). It implies **no** ownership of the host. |
| What is accepted | Events from the host. | Events from the host only inside the connection's scope: its **tenant** (`tn`; Kibble shows it to staff as the business ID, or the store address for Shopify) and, when set, its **routes** (shown as pages belonging to the business). |
| What it never gets | Nothing withheld. | Manifest fetching, catalog page or image URLs, and absolute image URLs. A connection never authorizes anything about the provider's host beyond collection. |
| Shared storage | `data-cookie-domain` may share one `_kibble` cookie across website hosts on the same registrable domain. | Never a cookie. Storage is property-scoped (see `data-storage` below). |
| Installed by | The client's developer: the website snippet. | Whoever can add a script on the provider's pages: a tag in the client's own tag manager on that host, the provider's support team, or a platform feature such as a Shopify custom pixel. Each connection has **its own installation** (its own recipe with its own host, tenant and routes) on the property's Data tracking → Setup page (`/manage/data-tracking/setup`). |

A host is a website host or a connection of a property, never both. Do not register a provider's host as a website host: it would accept events from every customer of that provider. If the property's handoff names a connection, install the recipe for that connection, not the website snippet.

### Scope options on the script tag

| Option | Meaning | Rules |
| --- | --- | --- |
| `data-tenant` | The provider's tenant or account id for this business (for rezStream the business slug, for Shopify the shop's `myshopify.com` domain). | `[A-Za-z0-9._-]`, 1 to 128 characters; anything else is ignored. Sent as `tn` on every collect batch, phrase batch, handoff mint and redeem. The collector refuses events from a tenant-scoped connection host when `tn` is missing or different (HTTP 403, and the SDK stops sending on that page). Setting a tenant also turns on property-scoped storage. |
| `data-routes` | Comma-separated route patterns where the SDK may run on this host. Up to eight. | Whole `/`-separated segments only: a literal (compared ignoring case), `{tenant}` (replaced by the tenant), `*` (exactly one segment) or `**` (zero or more segments). A pattern ending in a literal also matches its subtree. On a page outside every pattern the SDK does not start: no events, no storage. The collector also rejects events outside the connection's routes. |
| `data-storage` | `property` | Keeps the browser and stream ids under names scoped to the property key (`_kibble.<key>`, `_kibble_s.<key>`) in `localStorage` and `sessionStorage` and never writes a cookie. Use it on any host that is not a website host. |

A provider page the client can add a script to (generic connection):

```html
<script>window.kibble=window.kibble||function(){(window.kibble.q=window.kibble.q||[]).push(arguments)};</script>
<script async src="https://use.example-kibble-host/sdk/v1/kibble.js"
  data-key="pk_YOUR_PROPERTY_KEY"
  data-endpoint="https://use.example-kibble-host"
  data-consent="manual"
  data-storage="property"
  data-tenant="your-tenant"
  data-routes="/book/{tenant}/**"
  data-connected-hosts="www.example.com"></script>
```

### Carrying one visitor between hosts

The handoff is one mechanism between a website host and a connection (or between website hosts). It never puts the browser id in a URL: a link carries a single-use token, and the destination exchanges it for the id after its **own** consent is granted.

1. Register the connection on the property (provider, host, tenant, routes; staff see them as the booking page, business ID and pages belonging to this business). Kibble then lists its entry for the website snippet.
2. Website snippet: `data-connected-hosts` lists the destinations. An entry is either a whole host (`book.example.com`) or a host plus a route pattern (`guest.rezstream.com/**/your-slug/**`): only links whose path matches the pattern are decorated. Use the entry Kibble shows for the connection; do not shorten it to the bare host when it has a pattern. The pattern in an entry has no `{tenant}`: write the tenant literally.
3. Destination snippet or recipe: `data-connected-hosts="www.example.com"` so links back to the website carry the visitor home.
4. Each destination gets its own token, bound to (property, destination host). A token for one host is useless on another. A mint request names up to four destinations.
5. The destination removes `_kbh` from the address bar (only that parameter; the rest of the URL is untouched) before it reads the page or sends anything, then redeems the token only once its own consent is granted. Its first page view has `nav: handoff`. A token lives about two minutes and works once.

- **When tokens are minted.** On start when eligible links exist, then only after activity: a token is renewed near expiry only if the page is visible and had input in the last 60 seconds. Otherwise the links are cleaned and the next input mints once. At most one mint per 30 seconds and 30 per page load. Nothing polls and nothing is requested on hover.
- **Decoration.** `_kbh` is appended as text: the existing query string, its encoding and the fragment are preserved, and an existing `_kbh` is replaced. Plain links are decorated; so are GET forms (a hidden `_kbh` input added at submit) and form `action` or `formaction` URLs of POST forms to an eligible destination. A programmatic `window.open`, `location` assignment or redirect is not decorated by itself: call `kibble('decorate', url)` and navigate to the result. It returns the same URL unchanged when the URL is not an eligible destination, no fresh token exists, or the SDK has not loaded or has no consent (before the SDK loads the queue stub returns nothing, so call it only after load). Iframes and server-side redirects are not decorated; a booking link that passes through a redirect page on another host loses the handoff.
- **Consent is never inferred.** Not from a click, a link or a token. The destination holds the token until its own consent source grants (the destination's consent platform, the provider's consent tooling, or Shopify's customer privacy API). If the destination has no consent mechanism that can be wired, Kibble collects nothing there and Tracking shows the gap. Withdrawal on either side wipes that side only.
- **Logging limitation.** The token is in the destination's first request URL, so the provider's servers, its CDN and its own scripts that run before Kibble (for example a RUM or tag-manager script) can see it. It is single use and expires in two minutes, so a logged token cannot be redeemed later. Say so in the report; it cannot be removed from the client side.
- **Across website hosts** on one registrable domain, `data-cookie-domain="example.com"` shares the browser identifier through a first-party `_kibble` cookie instead. Check that the value is the real registrable domain (multi-part suffixes such as `co.uk` need care). A connection host never shares a cookie.
- A click on a link to a destination is also recorded automatically as a `click`.
- The collector counts handoffs per destination and day (minted, accepted, failed) without storing tokens, ids or addresses. "Events received", "handoff accepted", "activity linked across hosts", "an outcome observed" and "a business outcome tested" are five separate claims in Kibble; none implies another, and a `_kbh` in a URL proves nothing.

Older SDK builds send one legacy token without a destination; the collector accepts it only for website hosts, never for a connection host. Check `/sdk/v1/features.json` for the features `config.property-scope`, `cross-domain.destination-links` and `cross-domain.decorate` before relying on any of the above ([Versions and compatibility](https://www.usekibble.dev/AI#versions)).

### First-party gateway

> **Status: direct integration.** The real SDK is used as written; exercised in the repository demos and browser tests.

Use a gateway when a booking engine's content security policy blocks the Kibble API host, or when the site wants all requests on its own host. The reference gateway runs in the repository's hotel demo and is covered by its browser and unit tests. It has not been run in front of a live client site.

1. Serve the SDK from the gateway path, and set `data-endpoint` to the gateway **base**, not just the origin. Without `data-endpoint` the SDK would send events to the page origin and lose the path.
2. Forward only the allowlisted paths below. Everything else must return 404.
3. Forward only these request headers: `Content-Type`, `Origin`, `User-Agent` (and the CORS preflight request headers). Never forward cookies, authorization, or the visitor's IP address, and do not add them.
4. Keep the real `Origin` and a normal `User-Agent` intact, because collection checks the origin against the property's registered hosts and drops automated user agents.
5. Mark `/v1/*` responses `no-store`. Static SDK files may be cached for a few minutes; the settings keep the collector's cache time.
6. Register the host that serves the page (not the gateway upstream) on the property.

Snippet through a gateway at /measure:

```html
<script>window.kibble=window.kibble||function(){(window.kibble.q=window.kibble.q||[]).push(arguments)};</script>
<script async src="https://www.example.com/measure/sdk/v1/kibble.js"
  data-key="pk_YOUR_PROPERTY_KEY"
  data-endpoint="https://www.example.com/measure"
  data-consent="manual"></script>
```

| Method | Path (after the gateway base) | Purpose |
| --- | --- | --- |
| GET, HEAD | `sdk/v1/kibble.js` and `sdk/v1/kibble-<12 hex>.js` | the SDK |
| GET, HEAD | `sdk/v1/site-check.js` and `sdk/v1/site-check-<12 hex>.js` | the Validation panel |
| GET, HEAD | `sdk/v1/settings/<key>.json` | installation settings of a key-only installation; pass the collector's own `Cache-Control` back (five minutes) |
| POST, OPTIONS | `v1/collect` | events |
| POST, OPTIONS | `v1/phrases` | site-search phrases |
| POST, OPTIONS | `v1/handoff`, `v1/handoff/redeem` | cross-domain handoff |
| POST, OPTIONS | `v1/erase` | forget my data |
| POST, OPTIONS | `v1/event-names` | names of dataLayer events Kibble does not know yet (names only) |
| POST, OPTIONS | `v1/check/exchange`, `v1/check/heartbeat`, `v1/check/continue`, `v1/check/redeem`, `v1/check/end` | Validation |

The reference implementation is `integrations/examples/gateway/kibble-measure-gateway.mjs` in the Kibble repository. It enforces this allowlist, forwards only the headers above, limits bodies to 64 KiB, and times out upstream requests after ten seconds. Use it, or port its rules exactly. If you cannot, stop and ask the human.

END OF SECTION gateways
