Tracking contract v1.0
View .mdContract version: 1.0. Applies to: every server-side adapter (Next.js, Node, Django, WordPress, hand-wired) and to the Kismet edge worker, which is the reference emitter.
This document is the wire contract between a site that runs a Kismet tracking adapter and the Kismet platform. It says what an adapter reads, what it sets, what it sends, and what it may never do. It does not describe any one adapter’s API; each adapter has its own install page that points back here.
The words MUST, SHOULD and MAY are used in the RFC 2119 sense. Anything marked “conformance” is asserted by the shared conformance suite that every adapter ships with.
1. Scope and terms
Section titled “1. Scope and terms”- Adapter. The code that runs on the site’s own server on every page request: Next.js middleware, an Express middleware, a Django middleware, the WordPress plugin, or something a client wires by hand from this page.
- Authority. Kismet’s identity service at
https://api.ksmt.app. It is the one place a session id is confirmed and a visitor id is issued. Adapters adopt what the authority (or the visitor) already has; they mint only on a cold visit, and then reconcile. - Relay.
https://kismet.travel, which fronts the event ingest for adapters and for k.js. Adapters MAY post events to the API origin directly with a service key; the default is the relay. - k.js. Kismet’s browser tracker, loaded from
https://kismet.travel/k.js?c=<collectionSlug>. It handles the client plane (human interactions). The adapter’s job is to make sure k.js adopts the server-resolved session instead of minting its own. - Session id (
kid_sid). The 90-day identity carrier. One visitor, one browser, onekid_sidacross every Kismet surface on the domain. - Visitor id (
kid_vid). A longer-lived hint the authority returns. Adapters never mint one. - Server plane. One content event per page or agent request, emitted by the adapter with
trackingMode: "server". This is the only record of AI agents and crawlers, which never run k.js. - Client plane. Events k.js emits from the browser with
trackingMode: "client". Not the adapter’s job. - Collection. The Kismet tenant the site belongs to, identified by
collectionSlug.
2. Credentials and configuration
Section titled “2. Credentials and configuration”| Setting | Required | What it is |
|---|---|---|
collectionSlug |
yes | The collection this site’s pages belong to. Sent on every resolve and every event. |
Tracking key (ctk_…) |
yes | The collection’s transport credential. Sent as the X-Kismet-Tracking-Key header on resolve-anchor, content events, booking bridge and quote capture. Server-side only. It MUST NOT be inlined into any page, script or cookie. |
| Authorized domains | yes, on the collection | The hostnames k.js may beacon from. Configured on the Kismet side, not by the adapter. In client mode the request Origin is the credential, so an unregistered hostname means client-plane events are rejected once enforcement is on. |
KISMET_TRACKING_ENDPOINT |
no | Where content events POST. Default https://kismet.travel/api/track. |
KISMET_RESOLVE_ANCHOR_URL |
no | Override for the authority. Default https://api.ksmt.app/v1/identity/resolve-anchor. |
KISMET_API_ORIGIN |
no | Override for every direct API call (booking bridge, quote capture). Default https://api.ksmt.app. |
Recommended environment variable names for adapters: KISMET_COLLECTION_SLUG, KISMET_TRACKING_KEY, plus the three optional overrides above. The WordPress plugin stores the same three values as options with the key encrypted at rest.
Where to get the key: a collection admin reveals or rotates it with GET or POST /v1/collections/<slug>/tracking-key?reveal=true. Developer-portal self-serve issuance is planned and is not part of this contract version.
3. Identifiers
Section titled “3. Identifiers”| Identifier | Grammar | Notes |
|---|---|---|
kid_sid, read grammar |
^kid_[A-Za-z0-9]{6,40}$ |
What an adapter accepts from a ?kid_sid= parameter or the _kid_sid cookie. Anything else is treated as absent. |
kid_sid, mint grammar |
^kid_[A-Za-z0-9]{8}$ |
What an adapter mints and what the authority adopts as a proposed id. Exactly eight characters after the prefix. A proposed id of any other length is silently re-minted by the authority, which forks the visitor into two sessions. |
kid_vid |
Legacy: ^[A-Za-z0-9_]{6,64}$; visitor-recognition extension: ^vid_[a-f0-9]{64}$ (68 characters total) |
Authority-issued recognition hint; adapters never mint one. |
| Mint alphabet | A-Z a-z 0-9 |
Eight bytes from a cryptographic random source, each reduced modulo 62. |
Conformance: the suite mints 1,000 ids and asserts every one matches the mint grammar; it presents a 7-char and a 9-char id and asserts the adapter never proposes them.
4. Cookies
Section titled “4. Cookies”| Cookie | Max-Age | Set when | Attributes |
|---|---|---|---|
_kid_sid |
7,776,000 s (90 days) | The resolved id was not already on the visitor (threaded id differs from the cookie, or a cold mint). Refreshing on every request is allowed but not required. | Path=/; SameSite=Lax; Secure when the request is HTTPS; not HttpOnly (k.js reads it through document.cookie) |
_kid_vid |
34,560,000 s (about 400 days) | When the authority returns a valid token after consent; the optional visitor follow-up may refresh the cookie. | Same as above |
Domain rule. The cookie MUST be set with Domain=.<serving domain> where the serving domain is the request host with a leading www. and any port removed (the same normalization the serving domain on events uses, section 8). Host-only cookies (no Domain attribute) are permitted only when the host is a single label such as localhost or an IP address, which browsers reject a Domain on. Adapters MUST let the operator override the domain for sites whose registrable domain is deeper than the serving domain (for example book.example.com sharing a session with example.com).
Why the dotted domain: a session has to survive www.example.com to example.com and a WordPress root to a Next.js app on the same host. A host-only cookie set by one surface is invisible to another on a sibling host. This is the one place the shipped Next reference (host-only) differs from the edge worker and the WordPress plugin (dotted), and the contract sides with the worker and plugin.
Consent and geography. An adapter MUST NOT set either cookie, and MUST NOT mint or resolve a session, for a visitor in a cookie-consent jurisdiction unless the site has signalled consent. The default gate is a country deny list read from the platform’s country header (cf-ipcountry, x-vercel-ip-country, or the adapter’s own geo source): the EU 27, Iceland, Liechtenstein, Norway, the United Kingdom and Switzerland. Starting with core/Next.js/WordPress 1.1.1 and Node 1.0.2, an unknown country (missing, malformed, XX, T1) is denied. Sites without trusted country headers therefore remain denied until their consent hook explicitly permits the visitor. Older releases allowed unknown geography and must be upgraded or configured with an explicit consent hook. Adapters MUST therefore expose a hook that replaces the decision with the site’s actual consent state (read from the consent manager’s cookie or API), and a site in a consent jurisdiction MUST wire it. The hook is the consent mechanism; the geography list is only the fallback.
What consent gates and what it does not. Consent gates the session: the cookies, the mint, the resolve call, the seed id, and therefore journeys, the guestbook and booking attribution. It does not gate the server-plane content event (section 8), which an adapter sends for every request regardless, with clientSessionId null when no session was allowed. Crawler and agent fetches, page views, referrers and countries are recorded either way; that is what the install report reads. A visitor who declines is measured as an anonymous visit, not lost.
Bots. A request whose user agent matches the shared bot vocabulary (section 8) or has no user agent MUST NOT receive a cookie, a mint, or a resolve call. Bots are not guests.
Conformance: cookie names, ages, attributes and the domain rule are asserted from the Set-Cookie header on a cold human request; absence is asserted on a bot request and on a request from a denied country with no consent signal.
5. Resolution order
Section titled “5. Resolution order”For every page request the adapter resolves the visitor in this order and stops at the first match. Assets, the adapter’s own API routes, and anything that is not a page or an agent surface are excluded before this runs.
-
Suppressed. Bot user agent, missing user agent, or a consent-denied visitor: no id, no cookie, no network. The page seed (section 7) carries the suppression flag so k.js will not mint either.
-
Threaded.
?kid_sid=<id>on the URL and the id matches the read grammar: adopt it verbatim. Set the cookie if it differs from the current cookie. Send a non-blocking resolve-anchor withthreadedKidSid(andcookieKidSidwhen a different cookie was present) so the authority receives the session identifiers for the navigation. This is how a session crosses domains or arrives from an email link. -
Cookie.
_kid_sidpresent and valid: adopt it. No network. -
Cold human. No carrier. The adapter MUST produce an id without blocking the response. The v1 default is:
- mint an id in the mint grammar,
- set the cookie,
- seed the page with it,
- post the server-plane event under it,
- reconcile it with the authority after the response is sent (
proposedKidSid), bounded at 3,000 ms, result ignored.
An adapter MAY offer an “authority first” mode that calls resolve-anchor before responding, with no proposed id, to request a session identifier from the authority. If offered it MUST be opt-in, MUST be bounded at 1,500 ms, and MUST fall back to the local mint on any failure. It is not the default because a cold visitor should never wait on Kismet.
Fail-open means “never null for a human”, not “no id”. A consent-permitted human leaves with a kid_sid; what varies is whether the authority has heard about it yet.
The resolved decision SHOULD be passed to the page render as request-scoped data (the Next reference uses the request headers x-kismet-kid-sid, x-kismet-sid-suppressed: 1 and x-kismet-anchor-tier: threaded | cookie | authority | minted | suppressed). The tier header on the response is useful for verification and harmless to expose.
Conformance: each of the four branches is exercised against a local stub of the authority; the cold branch asserts a response was produced before the stub was called.
Optional returning-visitor extension (package 1.1.0)
Section titled “Optional returning-visitor extension (package 1.1.0)”The 1.1.0 core, Next.js and WordPress releases add a bounded visitor-cookie follow-up. This is an optional extension to Contract 1.0, separate from the planned Contract 1.1 property/conversion changes. It requires an explicit consent hook or consent-cookie configuration, adapter opt-in and collection enablement at Kismet. Geography alone does not enable it.
The follow-up sends visitorConsent: true, the established page session as proposedKidSid, and cookieKidVid when present to the tracking-key-authenticated resolve-anchor endpoint. It waits at most one second and persists the returned visitor token only when the returned session matches the page session. It does not block page rendering or replace that session. The normal page-resolution path above remains asynchronous.
A valid visitor token can link a new session after loss of the session cookie; it does not authenticate a guest or authorize wallet access. Token expiry is 400 days from issuance, and browser policies or user deletion can shorten cookie persistence. Consent withdrawal clears both cookies on the next adapter request; immediate in-page withdrawal must invoke the site’s consent refresh flow.
6. Resolve-anchor (the authority)
Section titled “6. Resolve-anchor (the authority)”POST https://api.ksmt.app/v1/identity/resolve-anchor
Headers: Content-Type: application/json, X-Kismet-Tracking-Key: ctk_….
Body. Every field is optional except collectionSlug and origin; the authority silently drops anything not listed, so an adapter MUST send exactly these names. Values are the visitor’s, never the adapter’s own request headers. Send the original values in the documented fields without hashing them.
{ "collectionSlug": "sea-view-stays", "vrSlug": null, "origin": "https://example.co.uk", "proposedKidSid": "kid_Ab3dE9xZ", "threadedKidSid": null, "cookieKidSid": null, "cookieKidVid": null, "ip": "203.0.113.7", "userAgent": "Mozilla/5.0 …", "acceptLanguage": "en-GB,en;q=0.9", "referrer": "https://www.google.com/", "gclid": null, "gbraid": null, "wbraid": null, "gadCampaignId": null, "fbclid": null, "landingUrl": "https://example.co.uk/stays/porthleven?gclid=…"}Field rules:
originis the site origin (scheme and host) or, from the WordPress plugin, the bare host. Either is accepted.ipis the first hop ofX-Forwarded-For, orcf-connecting-ip, or the socket address, in that order of preference.- Click ids (
gclid,gbraid,wbraid,gadCampaignIdfromgad_campaignid,fbclid) are read off the landing URL and MUST match^[A-Za-z0-9._-]{1,512}$or be sent as null. This is first-touch capture: Google’s offline conversion upload is keyed on these, so an adapter that drops them makes the site’s paid bookings unattributable. landingUrlis the full URL of this request as the visitor saw it (public scheme and host reconstructed from forwarded headers when the server sits behind a proxy).proposedKidSidis set only on a cold-mint reconcile.threadedKidSidandcookieKidSidonly on a threaded hop.
Response, 200:
{ "ok": true, "kid_sid": "kid_Ab3dE9xZ", "kid_vid": "vid_…", "isNew": true, "tier": "proposed" }An adapter MUST validate kid_sid against the read grammar and kid_vid against its grammar before using either. Any non-2xx, timeout, malformed body or ok: false is treated as “no answer”: the adapter keeps the id it already has.
Errors: 401 { "ok": false, "error": "INVALID_TRACKING_KEY" } when the key is missing, wrong, or not for collectionSlug; 400 with an error code for a malformed body; 500 RESOLVE_FAILED.
Timeouts: 3,000 ms after the response (reconcile), 1,500 ms on the critical path (authority-first mode only).
7. The page seed and the k.js tag
Section titled “7. The page seed and the k.js tag”A page served to a human MUST carry, in <head>, in this order:
<script>window.Kismet=window.Kismet||{};window.Kismet._kidSid="kid_Ab3dE9xZ";</script><script async src="https://kismet.travel/k.js?c=sea-view-stays"></script>A page served to a suppressed visitor (bot, or consent denied) MUST seed the suppression flag instead of an id:
<script>window.Kismet=window.Kismet||{};window.Kismet._sidSuppressed=1;delete window.Kismet._kidSid;</script>Rules:
- The seed precedes the k.js tag. k.js adopts
window.Kismet._kidSidover its own cookie read, and re-reads the seed once the head has parsed, so a parser-inserted tag that evaluates early is still fine. A page with neither seed nor suppression flag makes k.js mint its own id, which forks the visitor. - Renderers that reorder scripts (React 19 hoists any
<script async src>element ahead of other head content) MUST NOT emit the k.js tag as a separate element. Emit one inline script that sets the seed and then appends the k.js tag itself; the WordPress plugin’s bootstrap and@kismet-tech/telemetry-nextboth do this. - The
?c=parameter is the collection slug. - The adapter MUST NOT set or read the globals
__kismetKjs,__kismetKjsEval,__kismetKjsTagor__kismetAnalyticsAdapterMounted. k.js owns them; reusing one caused a production outage on 2026-07-09. - The seed value is per visitor. See section 10 for what that means for caching.
Cache-safe variant. A site that serves full-page-cached HTML cannot inline a per-visitor seed. The WordPress plugin uses a constant bootstrap in <head> that calls a never-cached, same-origin endpoint on every visit. That endpoint evaluates current consent before adopting an existing identifier and returns the resolution of section 5. The bootstrap seeds a permitted session and then loads k.js. A suppressed response, endpoint failure or 1,500 ms timeout sets suppression and does not load k.js. This keeps page rendering independent of tracking and prevents a retained cookie from bypassing current consent. Any adapter MAY implement this variant.
8. Server-plane content events
Section titled “8. Server-plane content events”POST https://kismet.travel/api/track (default) or POST https://api.ksmt.app/v1/content-events with a service key.
Headers: Content-Type: application/json, X-Kismet-Tracking-Key: ctk_….
Sent for every page request and every agent-surface request, after the identity decision, scheduled so it never delays the response, bounded at 2,000 ms, failures swallowed. Not sent for assets, the adapter’s own API routes, robots.txt or sitemaps.
Body (field order is the reference emitter’s; keep it):
{ "trackingMode": "server", "pageUrl": "https://example.co.uk/stays/porthleven/harbour-house?in=2026-10-03&out=2026-10-06&party=4", "responseStatus": 200, "clientSessionId": "kid_Ab3dE9xZ", "resourceClass": "content_vr", "actionType": "property_view", "collectionSlug": "sea-view-stays", "vacationRentalSlug": "harbour-house", "stayCheckIn": "2026-10-03", "stayCheckOut": "2026-10-06", "guestCount": 4, "servingDomain": "example.co.uk", "isBot": false, "botName": null, "botCategory": null, "verifiedBot": false, "userAgent": "Mozilla/5.0 …", "clientIp": "203.0.113.7", "country": "GB", "referrer": "https://chatgpt.com/"}Field rules:
| Field | Rule |
|---|---|
trackingMode |
Always "server" from an adapter. |
pageUrl |
The public URL as the visitor requested it, scheme and host reconstructed from X-Forwarded-Proto and X-Forwarded-Host when behind a proxy. Keep the query string and any .md suffix. |
clientSessionId |
The resolved kid_sid for a human page view. Null for a bot, even one echoing a shared ?kid_sid= link, and null for every agent-surface fetch. |
resourceClass |
content_vr when the page is one property, else content_vrm. Allowed values: content_vr, content_vrm, content_vr_rates, content_vr_reviews, content_marketing. |
actionType |
fetch for an agent surface; cta_click for a booking-engine intent page with a resolved property; property_view for a property page; otherwise view. Other values (click, add_to_wishlist, the form_* family, meeting_booked, nl_ask) belong to the client plane. |
vacationRentalSlug |
The Kismet property slug when the adapter knows it; null otherwise. Ingest attributes .md fetches to a property by serving URL, so agent events land correctly without it. |
stayCheckIn, stayCheckOut, guestCount, promoCode |
Parsed off the URL when present (in/out/party, or checkin/checkout/guests, ISO or US dates normalized to YYYY-MM-DD). Omitted, not null, when absent. |
servingDomain |
Request host with leading www. and port stripped. |
isBot, botName, botCategory |
The shared vocabulary’s verdict as a hint. Ingest re-classifies from the raw userAgent and its verdict wins. Categories: training, search, commerce, assistant. |
userAgent |
Raw. Required for classification. |
clientIp |
Visitor IP address from the trusted request context. Send it unmodified as clientIp; ipHash is not an accepted request field. |
country, city, region |
From the platform’s geo headers when available; null otherwise. |
referrer |
The Referer header, raw. |
Agent surfaces. A request for a markdown twin (/<page>.md, Accept: text/markdown outranking text/html), /llms.txt, /.well-known/llm-index.json, or an equivalent the adapter serves, is an agent fetch: no identity work, no cookie, no seed, one event with actionType: "fetch" and a null session.
Bot vocabulary. The pattern table is published in @kismet-tech/edge-events (bot-patterns.generated.js, generated from the platform’s bot-patterns.json) and mirrored in the WordPress plugin. An adapter in a language without the package MUST carry the same table; the conformance suite checks a fixed list of user agents against it.
Response: 200 { "success": true }. 400 for an invalid trackingMode, resourceClass or actionType; 401 when server mode has no key and enforcement is on; 429 above 120 requests per minute per source address at the relay.
Conformance: the suite captures the event posted for a human page, a bot page, and an agent surface and asserts every field rule above.
9. Booking bridge
Section titled “9. Booking bridge”The deterministic join between a session and a completed reservation. Call it once, server side, from the code path that knows the booking succeeded (a checkout confirmation handler, an order-complete hook). Do not scrape it from the confirmation page in the browser; k.js already does that for engines it knows, and the point of the adapter is a reliable server-side call.
POST https://api.ksmt.app/v1/booking-bridge
Headers: Content-Type: application/json, X-Kismet-Tracking-Key: ctk_….
{ "kidSid": "kid_Ab3dE9xZ", "confirmationCode": "EX-48213", "reservationId": null, "bookingEngine": "custom", "domain": "example.co.uk"}Rules:
kidSidanddomainare required. One ofconfirmationCodeorreservationIdis required. Send the confirmation code the guest sees; sendreservationIdwhen the value is the PMS’s own id (a 24-hex Guesty id, for example). The platform matches on whichever the PMS webhook later carries.- A confirmation code MUST be 6 to 64 characters and contain at least one digit. Plain words are rejected downstream.
bookingEnginenames the engine:icnd,guesty,homerunner,kismet, or a free label such ascustom.- The bridge row lives 24 hours. A reservation whose PMS webhook arrives later than that falls back to the quote-capture match.
Response: 201 { "ok": true, "bridgeId": "…", "expiresAt": "…" }. 400 on a body that fails the rules.
The route is public today (validated in-handler in later versions); adapters MUST send the tracking key anyway so they conform when validation lands. From a browser, k.js posts the same body to https://kismet.travel/api/k/booking-bridge with a bridge token; that path is the client plane and not an adapter concern.
10. Quote capture
Section titled “10. Quote capture”Optional. Records a price the guest saw for a stay, keyed to the session, so a reservation with no bridge can still be matched on property and dates. Call it from the server code that returns a quote, after the response is sent.
POST https://api.ksmt.app/v1/quote-capture
{ "kidSid": "kid_Ab3dE9xZ", "bookingEngine": "custom", "listingId": "12345", "pageUrl": "https://example.co.uk/stays/porthleven/harbour-house", "vrSlug": null, "collectionSlug": "sea-view-stays", "checkIn": "2026-10-03", "checkOut": "2026-10-06", "externalQuoteId": null, "totalAmountCents": 57500, "currency": "GBP", "domain": "example.co.uk"}kidSid is required; everything else optional. pageUrl lets the platform resolve the property when the adapter has no slug. totalAmountCents is an integer in minor units. Rows live 30 days. Response 201.
11. Caching
Section titled “11. Caching”- A response that inlines a per-visitor seed MUST carry
Cache-Control: private, no-store. Cached HTML with a seed hands one visitor’s identity to everyone who hits the cache. - A site that must cache HTML uses the cache-safe variant in section 7 and never inlines the id.
- The
Set-Cookiefor_kid_sidMUST be on a response that is not cached by a shared cache, for the same reason. - Content events and reconcile calls are never on the critical path, so caching the page does not lose the server-plane record only if the adapter runs on the cache miss path. Where a CDN serves hits without the adapter, the operator should know those hits are invisible to the server plane; the install report (in development) shows this as a coverage gap.
12. Crossing a boundary
Section titled “12. Crossing a boundary”Same registrable domain, different surfaces (a WordPress root and a Next.js app under a path, or www and apex): the dotted cookie domain of section 4 is the whole mechanism. Both adapters adopt the same _kid_sid; whichever the visitor hit first minted it. Each surface still emits its own server-plane events and seeds its own pages.
Different domains (a marketing site and a separately hosted booking engine): the sending side appends ?kid_sid=<id> to the outbound link; the receiving side adopts it using the threaded-carrier rule and reports the supplied identifiers to the authority. k.js does this automatically for links it can see; a server-rendered link the adapter generates SHOULD append it too.
Email and campaign links: the platform’s link proxy threads ?kid_sid= on click; nothing for the adapter to do beyond rule 1.
13. What is booking-shaped, and what is not
Section titled “13. What is booking-shaped, and what is not”The contract is meant to be booking-type agnostic (booking-type agnostic by design). Sections 3 to 8 and 11 to 12 are: they describe a visitor, a page, an agent and a session, and apply unchanged to a hotel, a tour operator, a lead-gen site or a SaaS signup. Sections 9 and 10 are booking-shaped by name (confirmationCode, checkIn, checkOut), and the stay parameters in section 8 are optional. A non-lodging site conforms by leaving them out.
Planned for 1.1, not in this version: a generic conversion event (POST /api/k/conversion with kidSid, conversionType, conversionId, optional value and currency) of which the booking bridge becomes one type; and externalListingId on content events and quote capture, the site’s own PMS listing id, resolved to the Kismet property at ingest, so a site never has to know Kismet slugs. Adapters MAY send externalListingId under 1.0; ingest drops unknown fields, so it is harmless until it is read.
13a. Funnel stages and what lights them
Section titled “13a. Funnel stages and what lights them”The platform derives a journey stage per session and property from the events above. An adapter that emits only collection-level view leaves every visitor at researching. To light the rest, the adapter needs to know which URLs are which, which is why every adapter takes a route profile (property page matcher, search paths, intent path with stay parameter names, agent-surface paths) and maps automatically:
| Stage | Signal | Who emits it |
|---|---|---|
| researching | view on a results or content page |
Adapter, server plane |
| viewed | property_view on content_vr with a property identifier |
Adapter from the route profile, or the browser helper |
| shortlisting | add_to_wishlist with a property |
Browser helper only (a save is a click) |
| planning | property_view with stayCheckIn and stayCheckOut |
Adapter when the URL carries dates; browser helper when the visitor picks them |
| intent | cta_click with a property and the stay |
Adapter on the intent path; browser helper on a book button |
| conversion | Booking bridge (section 9) | Server, from the checkout handler |
Browser helpers dispatch kismet:visitor:* CustomEvents that k.js bridges to the client plane; they never call window.Kismet.track() directly, which omits resourceClass and is rejected.
How a property is identified. An adapter never needs to know Kismet’s slugs. Three resolutions, in the order the platform applies them:
vacationRentalSlugwhen the site does know it (Kismet-built sites).pageUrlagainst the collection’s registered serving URLs. For a site Kismet serves, these exist by construction; for a self-hosted site paired with a PMS, Kismet registers them once from the site’s sitemap. This is the zero-configuration path and the only one that attributes crawler and agent fetches to a property.externalListingId, the site’s own PMS listing id, resolved at ingest within the collection (contract 1.1; safe to send under 1.0).
A site with no Kismet catalog (the self-serve start) sends its own identifier as externalListingId and gets page-level reporting; per-property funnel stages need a catalog to resolve against.
14. Errors and fail-open behaviour
Section titled “14. Errors and fail-open behaviour”| Situation | Adapter behaviour |
|---|---|
| No tracking key configured | Resolve locally, set cookies, seed the page, log one warning. Post no events and no reconcile (they would be rejected). |
| Authority unreachable or slow | Keep the local id; the reconcile is retried on the next cold visit only in the sense that a new visitor gets a new reconcile. No queueing. |
| Relay returns non-2xx for an event | Drop it. Never retry on the request path. |
| Threaded id fails the read grammar | Treat as absent; fall through to the cookie. |
| Cookie value fails the read grammar | Treat as absent; fall through to bot and consent checks, then mint. |
| Authority returns an id that fails the grammar | Ignore the response entirely. |
| Adapter throws anywhere | The page MUST still be served. Wrap the whole adapter in a guard that degrades to “no tracking on this request”. |
15. Conformance requirements
Section titled “15. Conformance requirements”The shared suite runs an adapter against a local stub of the authority and the relay and asserts:
- Mint grammar (section 3).
- Cookie names, ages, attributes and domain rule; no cookie for bots or consent-denied visitors (section 4).
- The four resolution branches, with the cold branch responding before any network call (section 5).
- The resolve-anchor body carries exactly the declared field names, visitor values, click ids from the landing URL, and
proposedKidSidon a cold mint (section 6). - The seed precedes the tag, carries the resolved id or the suppression flag, and touches no reserved global (section 7).
- Content event field rules for a human page, a bot page and an agent surface, including a null session on both non-human cases (section 8).
Cache-Control: private, no-storeon any response carrying an inline seed (section 11).- No request is delayed by more than the adapter’s own compute when the stub authority and relay are made to hang (sections 5, 8, 14).
An adapter that does not ship the suite does not ship.
16. Known drift in shipped implementations (as of 2026-09-03)
Section titled “16. Known drift in shipped implementations (as of 2026-09-03)”Recorded so a reader comparing this document to the code is not surprised. Each is scheduled to close in a 1.x release.
- The Next.js reference implementation (on
@kismet-tech/edge-events0.1.0) sets host-only cookies and asks the authority before responding on a cold visit (1,500 ms cap). Contract v1 requires the dotted domain and makes local mint the default. Closed by the Next adapter 1.0. - The Kismet Elements WordPress plugin (0.1.74) already does local mint plus async reconcile and the dotted domain. Its resolve body omits
landingUrland the click ids, so first-touch capture on a WordPress landing is missing. Closed by the WordPress alignment slice. - The edge worker reads
cf-connecting-ipandrequest.cfgeo; adapters behind other proxies read forwarded headers. Both are conformant. - The relay’s server-mode key check and the API’s content-event key check run in warn mode. Adapters MUST behave as though they are enforced.
17. Versioning
Section titled “17. Versioning”The contract carries a major.minor version. A minor version adds optional fields or endpoints and never changes an existing rule; every 1.x adapter stays conformant. A major version may change a rule and ships with a migration note. Each adapter declares the contract version it conforms to in its README and its changelog, and the conformance suite is versioned with the contract.
Changelog:
- 1.0 (2026-09-03, draft). Extracted from the shipped worker, Next reference, WordPress plugin and the platform routes. Decisions taken here rather than inherited: dotted cookie domain as the rule; local mint plus async reconcile as the default; the consent hook as a MUST; the tracking key on the bridge and quote calls.
Consent suppression clarification
Section titled “Consent suppression clarification”Current consent takes precedence over a session cookie or threaded identifier. For a suppressed visitor, emit the suppression seed without loading the browser tracker. This prevents an older browser tracker from adopting a retained cookie. Consent changes within an already loaded page require integration with the consent manager; navigation-based validation does not establish in-page revocation behavior.