# Install on Node

> Install Kismet Telemetry in an Express, Connect, Fastify, Koa or plain node:http server with @kismet-tech/telemetry-node.


`@kismet-tech/telemetry-node` wraps the [core](https://developers.kismet.travel/telemetry.md) for Node servers: a middleware on the raw `node:http` request and response that does the whole [contract](https://developers.kismet.travel/telemetry/contract.md) on every page request, the seed for your template, and the checkout call. Node 20 or later, ESM and CommonJS.


**Contract 1.0 limit:** `externalListingId` can be emitted, but ingest-side resolution and generic conversions are Contract 1.1 additions. For property-level attribution today, arrange a registered serving-URL mapping or use a known Kismet property slug. Confirm backend support before relying on the 1.1 fields.

## Before you start

Request from Kismet during onboarding: the collection slug and the collection's tracking key. Put them in the environment, never in a page:

```dotenv
KISMET_COLLECTION_SLUG=your-collection
KISMET_TRACKING_KEY=ctk_...
```

Confirm your production and staging hostnames are on the collection's authorized domains.

```sh
npm install @kismet-tech/telemetry-node
```

## 1. The middleware

Mount it before your routes.

```js
import express from 'express';
import { kismetTelemetry, consentFromCookie } from '@kismet-tech/telemetry-node';

const app = express();

app.use(
  kismetTelemetry({
    collectionSlug: process.env.KISMET_COLLECTION_SLUG,
    trackingKey: process.env.KISMET_TRACKING_KEY,
    // Your consent manager's cookie. Consent gates the session, not the recording.
    consent: consentFromCookie('CookieConsent', /statistics:true/),
    // Which URLs are what, once. Your own listing id is fine: send it as
    // externalListingId; ingest resolution requires Contract 1.1.
    profile: {
      property: { pattern: /^\/stays\/[^/]+\/([^/]+)\/?$/, as: 'externalListingId' },
      searchPaths: ['/stays', /^\/stays\/in\//],
      intent: { path: '/stays/checkout', checkinParam: 'checkin', checkoutParam: 'checkout', guestsParam: 'guests' },
    },
  })
);
```

Behind a reverse proxy (nginx, Apache, a load balancer), make sure it forwards `X-Forwarded-Host`, `X-Forwarded-Proto` and `X-Forwarded-For`. The adapter reads them to rebuild the public URL, decide `Secure` on the cookie, and take the visitor's IP. A Node server has no Cloudflare or Vercel country header, so pass the consent hook, or `country: (headers) => ...` if your proxy sets a country header of its own.

## 2. The seed

The middleware leaves `{ kidSid, suppressed, tier, classification, seed }` on `req.kismet` and `res.locals.kismet`. Print `seed` first thing in `<head>`, unescaped. It is one inline script that sets the id and then appends the browser tracker tag itself, or empty when there is nothing to seed.

```js
app.get('/stays/:town/:house', (req, res) => {
  res.render('property', { kismetSeed: res.locals.kismet?.seed ?? '' });
});
```

```html
<head>
  <%- kismetSeed %>
  <title>...</title>
</head>
```

A page that prints the seed is per visitor, and the middleware marks it `private, no-store` for that reason. If you serve HTML from a shared cache, do not print the seed into it; use the cache-safe bootstrap in the [contract](https://developers.kismet.travel/telemetry/contract.md#7-the-page-seed-and-the-kjs-tag) instead, the pattern the WordPress plugin uses.

## 3. The checkout call

From the server-side handler that knows the reservation succeeded, once:

```js
import { bookingBridge } from '@kismet-tech/telemetry-node';

await bookingBridge(config, { kidSid, confirmationCode, bookingEngine: 'custom', domain: 'example.co.uk' });
```

`kidSid` is the `_kid_sid` cookie on the checkout request (`req.kismet.kidSid` on any page request); carry it through your booking pipeline to wherever the confirmation code is known. Bounded, never throws, must not affect the booking.

## Fastify, Koa, plain node:http

The middleware only needs the raw request and response.

```js
// Fastify
fastify.addHook('onRequest', (request, reply, done) => middleware(request.raw, reply.raw, done));
// then request.raw.kismet.seed in the handler

// Koa
app.use((ctx, next) => new Promise((r) => middleware(ctx.req, ctx.res, r)).then(next));
// then ctx.req.kismet.seed

// node:http
createServer((req, res) => middleware(req, res, () => render(req, res)));
```

Frameworks that speak `Request` and `Response` (Hono, Cloudflare Workers, Deno): `createKismetResolver(config).resolve(request)` returns the decision (state, headers, `Set-Cookie` lines) without applying it.

## Browser signals the server cannot see

```js
import { createTracker } from '@kismet-tech/telemetry-node/client';
const track = createTracker({ collectionSlug: 'your-collection' });

track.propertyView({ externalListingId: listing.id, checkIn, checkOut, guests, stayTotalCents });
track.save({ externalListingId: listing.id });
track.bookIntent({ externalListingId: listing.id, checkIn, checkOut, guests, stayTotalCents });
```

## What the middleware does on every page request

Resolves the visitor (threaded id, cookie, suppressed for bots and visitors without consent, else a locally minted id reconciled with Kismet after the response), emits one server-plane event (agent surfaces as `fetch` with no identity), sets `_kid_sid` and `_kid_vid` on the dotted serving domain, marks the response `private, no-store`, and leaves the seed on the request for the template. The page never waits on Kismet, and a middleware error serves the page without telemetry rather than failing it. The [overview](https://developers.kismet.travel/telemetry.md#how-it-works) walks through the six steps; the [contract](https://developers.kismet.travel/telemetry/contract.md) specifies them.

## Verify

1. Load a page in a fresh browser. Expect a `_kid_sid` cookie on the dotted domain and `window.Kismet._kidSid` equal to it, with the response header `x-kismet-anchor-tier: minted`.
2. Reload. Same cookie, tier `cookie`, no new `Set-Cookie`.
3. `curl -A GPTBot https://your-host/some-page`: no `Set-Cookie`, and the page seeds `_sidSuppressed`.
4. `curl https://your-host/llms.txt`: recorded as a fetch with no session.
5. From a consent jurisdiction without consent: no cookie, page still recorded.
6. Complete a test booking: the bridge call returns 201.

The package's own tests run it through the contract's conformance suite as a real Express app on a real HTTP server behind forwarded headers. `middleware.flush()` awaits the background work, for your own tests and for graceful shutdown.
