# Build an app for Kismet

> Build and host your own integration, add your branding, and give managers settings and performance in Kismet Apps.


Build a welcome offer, search introduction, or another configurable experience in
your own repository. Use the SDK to store settings and engagement in Kismet, then
show your controls and reports in the collection's **Apps** directory. An
outside developer can own the code and hosting while the manager keeps access,
configuration, and performance in their Kismet account.

Your guest experience and manager panel are separate entry points. The guest
experience runs on the website; managers open the panel from
`https://kismet.travel/account/collections/{collection}/software`.

> **Early access**
>
> Request access before integrating. These operations require the Software Modules
> API, a compatible SDK, and the Kismet account host. Installing the SDK alone does
> not activate or mount a panel.

## Give your app a recognizable identity

Include `presentation` when you register a new app: its registration and branding
are saved together. Use the operation below to update branding on an existing app.

The Apps directory separates the app's identity from its builder and connected
services. Add a short description, an app icon, builder attribution, and integration
logos. Use square logo assets with transparent or white backgrounds. Missing or
unavailable logos render as initials; names remain visible.

```ts
import { createKismetClient as createAppClient } from '@kismet-tech/sdk/server';

const client = createAppClient({
  baseUrl: 'https://api.ksmt.app/v1',
  apiKey: process.env.KISMET_SERVER_KEY!,
  collection: 'your-collection',
});

await client.software.updatePresentation('welcome-offer', {
  description: 'Help guests discover and save a welcome offer.',
  iconUrl: 'https://assets.example.com/welcome-offer.png',
  builder: {
    name: 'Example Studio',
    logoUrl: 'https://assets.example.com/studio.png',
    websiteUrl: 'https://studio.example.com',
  },
  integrations: [{
    name: 'Offer provider',
    logoUrl: 'https://assets.example.com/provider.png',
    websiteUrl: 'https://provider.example.com',
  }],
});
```

Use `client.software.presentation(moduleId)` to read the current values. REST uses
`GET` and `PUT /v1/developer/collections/{collection}/software/{module}/presentation`.
The write replaces the complete presentation object: include every field you want
to keep. `{ integrations: [] }` clears optional branding. It does not change configuration,
experiment revisions, permissions, or TEST/LIVE status.

Both operations require a server credential: `telemetry.read` for reads and
`telemetry.configure` for writes. Descriptions support up to 320 characters, names
160 characters, and up to 12 integrations. All image and website URLs must use HTTPS
without embedded credentials. Invalid values return `400`; missing grants return
`403`, and a module outside the installation's scope returns `404`.

In an OAuth-connected coding client, use `update_software_presentation` with the
collection, installation ID, module ID, and presentation object. Preview without
`confirm`, review the current and proposed values, then apply with `confirm: true`.
This requires ADMIN or DEVELOPER access and an active authorized panel installation.
Attribution and integration names are developer-provided display information, not
verified endorsements or connection-health indicators.

## Register from your coding agent

Connect the [Developer MCP](https://developers.kismet.travel/mcp.md) in your coding client and sign in with Kismet.
Ask it to set up a dedicated TEST installation for your collection with
`telemetry.read` and `telemetry.configure`; add `telemetry.write` only if the app
will record guest engagement. Approve the exact site and panel origins.

Use `register_software_app` with `collection`, `installation_id`, `module_id`,
and `definition` (the same object shown below). The tool previews the destination,
environment, and app definition without writing. Review that result, then call it
with `confirm: true`. It creates a disabled app and returns its dashboard URL.
No server key is needed in the prompt. Registration does not deploy the app,
connect to an integration vendor, or enable guest traffic.

For later branding changes use `update_software_presentation`. For app runtime
calls, store a restricted server credential in the hosting secret store; OAuth
management access and your deployed app's machine access are separate.

## Set up access

Use [Kismet OAuth in your coding client](https://developers.kismet.travel/guides/developer-access.md) to configure an
installation. Register your site's exact HTTPS origin, including the panel host.
Use a restricted server credential in your server environment; never give it to
the guest component or embedded panel.

| Capability | Operations |
| --- | --- |
| `telemetry.configure` | Register a module; update its configuration |
| `telemetry.read` | Read configuration and aggregate performance; redeem a manager panel launch on your server |
| `telemetry.write` | Enroll an eligible visit; record a display or action |

TEST and LIVE use separate installations and reports. Use TEST for QA. LIVE
enrollment rejects non-production pages and classified bot traffic. Request scope
comes from the credential; a URL or module ID cannot select another installation.

The collection administrator grants an outside developer access to that collection.
The developer uses their own Kismet sign-in; managers do not share their login or
password. Keep credentials in the site's deployment secret store, not its source
code or panel URL.

## Connect your repository

1. Create a TEST installation for the collection, with the three capabilities
   above and your exact staging and panel origins.
2. Build the guest component and a separate, server-protected manager panel in your repository.
   For example, use `/our/software/welcome-offer/panel` for the manager panel;
   the path itself does not grant or restrict access.
3. Register the module with its hosted panel URL and `enabled: false`. Registration
   with an approved panel URL makes it discoverable in the collection's Software
   dashboard; you do not need to modify Kismet's frontend or upload a second report.
4. Open it from Kismet, verify manager access and configuration, then enable the
   TEST module when you are ready to exercise guest enrollment and events.
5. Record actual displays and declared actions through the guest handlers below.
   Kismet stores and aggregates those receipts; the panel reads the same report.

The developer owns the website and panel presentation. Kismet owns the collection
scope, current manager permissions, installation environment, revisioned
configuration, visit assignment, and performance aggregation. This API reports
software engagement; it is not an arbitrary metric-upload or general BI API.

## Register a module

```ts
import { createKismetClient } from '@kismet-tech/sdk/server';

const kismet = createKismetClient({
  apiKey: process.env.KISMET_SERVER_KEY!,
  baseUrl: 'https://api.ksmt.app/v1',
});
const configuration = await kismet.software.register('welcome-offer', {
  displayName: 'Welcome offer',
  panelUrl: 'https://staging.example.com/our/software/welcome-offer/panel',
  presentation: {
    description: 'Help guests discover and save a welcome offer.',
    builder: { name: 'Example Studio', websiteUrl: 'https://studio.example.com' },
    integrations: [{ name: 'Offer provider', logoUrl: 'https://assets.example.com/provider.png' }],
  },
  enabled: false,
  signedOutOnly: true,
  settingsSchema: {
    eligiblePage: { type: 'integer', label: 'Eligible page', minimum: 1, maximum: 2 },
  },
  variants: [
    { id: 'first-page', label: 'First page', weight: 5000, settings: { eligiblePage: 1 } },
    { id: 'second-page', label: 'Second page', weight: 5000, settings: { eligiblePage: 2 } },
  ],
  actions: [{ id: 'claim', label: 'Claim selected' }],
});
```

REST equivalent: `PUT /v1/developer/collections/{collection}/software/welcome-offer`
with the same JSON and `Authorization: Bearer <server-key>`.

Registration declares immutable fields, actions, labels, and panel URL. An existing
module ID returns `409 MODULE_EXISTS`; registration never overwrites an app.
After an interrupted response, read its configuration and presentation before
deciding whether to retry. Use the presentation operation for branding changes and
a new module for a changed definition. Managers can change declared setting values,
variant weights, enabled state, and the signed-out restriction—not executable
code, consent rules, prices, or guest entitlements. Weights total 10,000.

Registration does not deploy your panel or enable guest traffic. To appear in
**Apps**, its application and installation must be active, the installation
must grant collection-wide `telemetry.read`, and the panel must use an approved,
separate HTTPS origin. Use a panel URL without credentials, query parameters, or
fragments. A disabled module can still be reviewed in the dashboard.

## Give managers one destination

### Acceptance before enabling

1. Open the returned Kismet dashboard URL and confirm the app name, description,
   builder, integration names, and TEST badge. Check narrow screens and broken-logo fallbacks.
2. Open the panel from Kismet and verify both settings and performance. Direct visits
   must redirect to Kismet sign-in before rendering manager HTML.
3. Test a read-only manager and a user without collection access. Neither can save;
   the unauthorized user must not see panel data.
4. Enable the TEST configuration explicitly, record an actual display and declared
   action, and verify the report for that installation and revision. Unavailable
   conversion evidence is not zero conversions.
5. Repeat registration separately for LIVE only after reviewing its origins,
   permissions, hosting, and guest flow. TEST registration does not promote to LIVE.

If the row is missing, check active application/installation state, collection-wide
`telemetry.read`, approved separate HTTPS panel origin, and the manager's collection
role. Branding is optional and does not determine whether an app is authorized.
An integration logo is attribution—not proof that vendor credentials or connectivity work.

Use the SDK to link to Kismet rather than asking managers to find a private route
on the website:

```ts
import { getSoftwareDashboardUrl } from '@kismet-tech/sdk/software';

const dashboardUrl = getSoftwareDashboardUrl({
  collection: 'example-stays',
  moduleId: 'welcome-offer',
  installationId: '10000000-0000-4000-8000-000000000003',
});
// Use dashboardUrl for an "Open in Kismet" link on your site's manager entry page.
// Omit moduleId and installationId to open the collection's Software index.
```

Use the collection slug and installation ID from setup. The link contains no
credential and grants no access: Kismet requires sign-in and checks the manager's
current collection role. An installation-specific link keeps TEST and LIVE
panels unambiguous.

## Connect the guest component

Use the SDK's browser client and Next handlers. The guest component sends only a
page path and engagement receipts; the handler reads the existing Kismet session,
forwards a server-held guest token when signed in, and enforces the site's origin
and CSRF protection. You do not need a second cookie or authentication flow.

Add `software` to your existing [guest-account handler](https://developers.kismet.travel/sdk.md#branded-guest-experiences)
configuration. Keep the same auth client, session secret, and allowed origins:

```ts
import { createKismetGuestAuthNextHandlers } from '@kismet-tech/sdk/next';

export const handlers = createKismetGuestAuthNextHandlers({
  client: guestAuthClient,
  sessionSecret: process.env.KISMET_GUEST_SESSION_SECRET!,
  allowedOrigins: ['https://staging.example.com'],
  software: {
    client: kismet.software,
    modules: ['welcome-offer'],
    pageEnvironment: 'staging',
  },
});
```

`guestAuthClient` is the server client from your existing guest-account setup;
`kismet` is the server client above. `modules` explicitly enables the modules this
site serves. Set `pageEnvironment` in server configuration, not from browser
input. It describes the page; TEST or LIVE still comes from the installation.

Mount these three routes alongside the existing `/api/kismet/auth/csrf` route:

| Same-origin route | SDK handler |
| --- | --- |
| `GET /api/kismet/guest/software/[moduleId]/configuration` | `handlers.softwareConfiguration(request, moduleId)` |
| `POST /api/kismet/guest/software/[moduleId]/enrollments` | `handlers.softwareEnroll(request, moduleId)` |
| `POST /api/kismet/guest/software/[moduleId]/events` | `handlers.softwareRecordEvent(request, moduleId)` |

For example, in `app/api/kismet/guest/software/[moduleId]/enrollments/route.ts`:

```ts
import { handlers } from '@/lib/kismet-auth';

export async function POST(
  request: Request,
  context: { params: Promise<{ moduleId: string }> },
) {
  const { moduleId } = await context.params;
  return handlers.softwareEnroll(request, moduleId);
}
```

Use the corresponding method and handler for configuration and events. Sites
using Kismet's managed guest relay need these routes enabled in that relay too;
keep its existing verified-host request adapter. Registration, configuration
writes, and performance reports are not guest relay operations.

In your guest component:

```ts
import { createKismetGuestBrowserClient } from '@kismet-tech/sdk/react';

const guest = createKismetGuestBrowserClient();
const guestConfiguration = await guest.software.configuration('welcome-offer');
// After auth resolves and the visit is eligible:
const enrollment = await guest.software.enroll('welcome-offer', {
  pagePath: window.location.pathname,
});
// Only when the component has actually been displayed:
await guest.software.recordEvent('welcome-offer', {
  pagePath: window.location.pathname,
  enrollmentId: enrollment.id,
  eventId: crypto.randomUUID(),
  kind: 'display',
});
```

The browser client obtains and submits CSRF proof automatically. The handler
uses the existing `_kid_sid` session, returns `CANONICAL_SESSION_REQUIRED` while
it is missing, and never creates an alternative visitor ID. Configure canonical
site identity before enrollment. Query strings and fragments are not forwarded
as page evidence. Malformed or expired guest cookies fail explicitly; they are
not interpreted as signed-out visits.

### Keep staging visits separate from production

Use a TEST installation and register your exact HTTPS staging hostname as a
verified staging domain for the same collection. The installation must also
allow that origin. Kismet's authenticated site tracking uses the verified domain
registration to record staging page views as sandbox activity; a hostname prefix
or a browser-supplied `pageEnvironment` value is not sufficient.

TEST enrollment refuses a canonical session that already contains production
activity, even from another collection, with `ENVIRONMENT_MISMATCH`. Do not
replace `_kid_sid`, invent a visitor ID, or disable this check. Keep staging and
production browsing separate and use the supported site-identity integration.
Previously recorded activity is not relabeled when a domain is verified; an
existing mixed session remains ineligible for TEST enrollment.

Staging page views belong to the collection's site tracking. They are not
attributed to an arbitrary app installation. App enrollment and engagement
receipts retain their own installation scope. No SDK upgrade or new enrollment
parameter is required for this distinction.

## Enroll before the display trigger

Wait until the canonical guest-auth helper resolves. Suppress new enrollment and
automatic display while auth is loading or unknown, or when `signedOutOnly` is
enabled and the guest is signed in. A claim flow already opened by the guest can
continue through its own sign-in; do not restart it as a new experiment.

Keep the current enrollment and an already-open claim mounted after its own
successful login. The signed-out rule gates **new enrollment**, not completion
of that claim. Continue engagement against the same enrollment; do not re-enroll
or discard it merely because the auth helper now reports signed in.

Enroll on the first eligible page **before** checking the variant's display
condition. Count the first or second eligible page within that same visit. A
visit that leaves before page two stays in the denominator. Enrollment is stable
for the module, revision and session, and expires after 24 hours.

When the component is actually displayed, call `software.recordEvent` with the
same enrollment/session/page context, `kind: 'display'`, and a UUID `eventId`.
For a declared action, use `kind: 'action'` and its `actionId`. Reuse the same event
ID when retrying a logical event; a second click gets a new event ID. The supported
handlers bind these requests to the canonical session and derive host, environment,
and user-agent evidence on the server.

Setting names are declared by the module, not predefined SDK fields. For example,
an existing module may use `triggerPage` instead of `eligiblePage`; consume that
declared name from `enrollment.variant.settings` without renaming or dropping it.

## Connect the account panel

The panel lives at the registered HTTPS URL. Keep it separate from guest-site
navigation and guest telemetry. Its parent is the Kismet account host:

```ts
import { connectSoftwarePanel } from '@kismet-tech/sdk/software';

const panel = connectSoftwarePanel({ parentOrigin: 'https://kismet.travel' });
const context = await panel.getContext();
const current = await panel.getConfiguration();
const performance = await panel.getPerformance({
  from: '2026-09-01T00:00:00.000Z', to: '2026-09-02T00:00:00.000Z',
  revision: current.revision,
});
// On unmount: panel.dispose();
```

The host selects the collection and installation and rechecks access for each
request. ADMIN and DEVELOPER can save when the installation grants configuration
access; MEMBER and VIEWER have read-only access. No account session, API key, or
raw guest record crosses into the iframe. Open the panel through **Software** in
the collection account; a standalone page reports `HOST_UNAVAILABLE`.

### Protect the manager experience

Use **Kismet account sign-in** for managers, not the guest login on your website.
Render settings and reports only after the host has returned an authorized
context and the corresponding data. Before connection, show a loading state.
For `HOST_UNAVAILABLE`, show an **Open in Kismet** link using `dashboardUrl`; do not
render controls, cached reports, or sample performance. On access failure, clear
previously displayed data and offer a retry from Kismet. Recheck permissions when
loading or saving; an earlier successful connection is not a permanent grant.

Protect delivery of the page with `authorizeSoftwarePanelRequest` from the
server SDK. This early-access helper requires the panel-launch API and a matching
Kismet account host. It is separate from the browser bridge; installing a browser
component does not protect a server-rendered page.

Kismet signs the manager in and checks their current collection role before
opening the registered panel with a **single-use, 30-second render code** in a
form POST body, never a URL. Your
server redeems that code with its existing installation credential. Kismet checks
the manager's current role again, along with the active application, installation,
grants and exact registered URL. No manager password, account session, user ID or
role claim is passed to your site. No separate website password is required.

```ts
import { authorizeSoftwarePanelRequest } from '@kismet-tech/sdk/server';

// Call before rendering the manager page, including nested or preview routes.
export async function authorizeManagerPage(request: Request) {
  return authorizeSoftwarePanelRequest({
    software: kismet.software,
    request,
    panelUrl: 'https://staging.example.com/our/software/welcome-offer/panel',
    collection: 'example-stays',
    moduleId: 'welcome-offer',
    installationId: '10000000-0000-4000-8000-000000000003',
    environment: 'TEST',
  });
}
```

When `authorized` is false, use a **303 redirect** to the returned `redirectTo` without
rendering the panel. The destination is your module's Kismet Software dashboard,
where a signed-out manager can sign in. Render the panel only when `authorized`
is true, then load settings and reports through the browser bridge. Surface
service failures and server-credential configuration errors without rendering
the panel; do not turn them into a repeated sign-in redirect.

The registered URL, collection, installation and environment come from your
server configuration, never from the query or an untrusted forwarded host. Pass
the complete `Request` (or a clone if your framework needs to read it again).
The helper accepts only a POST from `https://kismet.travel` to the exact,
query-free panel URL, with `application/x-www-form-urlencoded` content and a
single `kismet_panel_launch` field. Duplicate fields, additional fields and bodies
larger than 256 bytes are refused before redemption. Your reverse proxy must
preserve this bounded POST body, content type and Origin; it must not turn the
body into a query string. A 303 denial prevents forwarding the POST body to the
sign-in destination. Direct GET/HEAD visits may use a fixed 307 redirect.

For Next.js, guard the route in Proxy/Middleware before rendering a
`force-dynamic` page. Reject Server Action, RSC and prefetch requests at this
entry point. After successful redemption, continue the ordinary form POST to the
page; do not redirect to an unprotected GET or create a website session. Keep
the browser bridge attached before submitting the form into its named iframe.
The legacy `requestUrl` query-code adapter remains for installed clients;
migrate to `request` to avoid exposing render codes to URL logging.
REST adapters can call `POST /v1/developer/collections/{collection}/software/{module}/panel-launches/redeem`
with `{ code, panelUrl }` and a server credential granting `telemetry.read`.

The code authorizes one page render, **not a website session or data access**.
It cannot be reused after reload; reopen the panel from Kismet for a new code.
The flow needs no third-party cookies. Do not prefetch, cache, log, or send the
launch body to analytics. Do not record request bodies on the launch route.
The clean panel URL contains no credential and requires no history scrubbing.
A previously issued code expires within 30 seconds;
signing out does not create or retain a session on your website. Collection-role
revocation is checked at redemption and on every subsequent data operation.

Do not include manager data in HTML, page props, static files, or unauthenticated
API responses. Do not add a server-key reporting endpoint to make a standalone
page work. Static JavaScript assets are not confidential; protect data and the
page-render entry, including alternate paths. Never use a path, `Referer`, or
iframe detection as authentication.

Keep manager pages out of guest navigation and guest telemetry. Set
`Cache-Control: private, no-store`, `Referrer-Policy: no-referrer`, and
`X-Robots-Tag: noindex, nofollow` on manager responses, including redirects and
errors. Set `Content-Security-Policy: frame-ancestors https://kismet.travel`
(or the explicitly configured Kismet account origin). Disable framework static
generation and shared caching for these routes. These settings complement the
server authorization check.

To save, call `panel.updateConfiguration` with `expectedRevision`, `enabled`,
`signedOutOnly`, and the existing variant IDs, weights and settings. Read back the
result. On `REVISION_CONFLICT`, reload and ask the manager to review their changes
instead of overwriting another editor.

## Display performance accurately

Use `software.performance` on your server, or `panel.getPerformance` in the account
panel. Supply one revision and an elapsed UTC interval of at most 31 days.
The API returns all retained receipts for that cohort, not a recent-event sample.

- `assignedVisits` is the denominator; it counts canonical sessions, not people.
- `displayedVisits` and action `visits` count distinct assigned visits. Action
  `events` counts deduplicated occurrences.
- Show period, revision, `generatedAt`, `lastEventAt`, and coverage. Complete
  coverage means all retained received events, not proof of every browser event.
- Marketing signup and confirmed-booking attribution are currently unavailable:
  their counts and rates are `null`. Do not report a claim or submit as conversion.

Expired sessions, missing modules, stale revisions, revoked credentials or grants,
and invalid origin/environment evidence are refused explicitly. A failed or
unavailable report is not zero performance. None of these operations sends email,
changes consent, creates a booking, or authorizes payment.

See the method-labelled endpoints under **API reference → Software** for
request schemas, response contracts, limits, and problem codes.

## Verify the handoff

- An authorized manager opens the registered TEST panel from the collection's
  Software dashboard. An unrelated collection cannot read or change it.
- Opening the website's panel URL directly redirects to Kismet before the panel
  HTML is served. Missing, expired, replayed and wrong-installation render codes
  are refused. A signed-in guest cannot substitute for a Kismet manager.
- The panel opens with third-party cookies blocked. A service outage does not
  expose the page or cause a sign-in loop; leaving the account view removes the
  bridge and frame. Keep server credentials and launch URLs out of logs.
- Read-only managers can see permitted reports but cannot save. Revoking access
  prevents subsequent reads and writes, including in a panel that is already open.
- A saved configuration is read back with its new revision. Concurrent changes
  produce `REVISION_CONFLICT`, not a silent overwrite.
- After enabling TEST and completing a test visit, the dashboard reads the same
  module, installation, revision, and period as `software.performance` on the
  server. No report import or client-calculated total is required.
- A failed request is an error, not a zero report. Missing verified conversion
  counts stay unavailable; TEST receipts never become LIVE performance.
