> ## Documentation Index
> Fetch the complete documentation index at: https://docs.puntego.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Headless SDK

> Render Puntego Guide and Experiences in your own interface without weakening action policy, consent, or auditability.

Headless mode keeps Puntego's authenticated matching, grounding, confirmation,
postconditions, and audit trail, while your product renders the Guide transcript
and Experience content. It is intended for teams that need complete visual
control—not for bypassing Puntego's execution policy.

> **Release preview:** The headless APIs below are implemented and tested in the
> repository, but the referenced `@puntego/embed` and `@puntego/react` packages
> are not yet published to npm. Treat these examples as the upcoming package
> contract until the package release is announced. This does not affect the
> production hosted-script path for Puntego's built-in Guide and Experiences.

## Framework-neutral client

Load the page-level runtime once, explicitly in headless mode, then create the
state adapter:

```ts theme={null}
import { createPuntegoHeadlessClient } from '@puntego/embed/headless';
import { loadPuntego, whenPuntegoReady } from '@puntego/embed/loader';

loadPuntego({ appId: 'gp_your_app_id', headless: true });
await whenPuntegoReady();

const puntego = createPuntegoHeadlessClient();
const unsubscribe = puntego.subscribeGuide(() => {
  renderGuide(puntego.getGuideSnapshot());
});

await puntego.sendMessage('How do I invite my team?', {
  initiatedBy: 'visitor',
});

unsubscribe();
puntego.destroy();
```

The adapter is safe to import and instantiate during server rendering. Commands
fail clearly until a browser runtime is ready. `destroy()` detaches adapter
listeners; it does not destroy the shared page-level Puntego runtime.

To tell the guide about the signed-in account, hand the adapter plain data
whenever it changes. It keeps a JSON copy and serves it on every message:

```ts theme={null}
await puntego.setContext({
  accountFacts: { accountId: account.id, plan: account.plan, seats: { used: 3, limit: 5 } },
  signedAccountFacts, // optional: a token your server signed
});
```

Page-reported facts ground answers and never authorize anything. When
`accountId` (or the signed token's subject) names another account, the guide
starts a new conversation: once `setContext` resolves, the Guide and turn
snapshots no longer hold the previous account's messages, and their subscribers
are notified. `setContext(null)` stops sending account facts. See
[Account facts](/account-facts).

The adapter also carries `assist`, the guide as a tool for other AI agents
(`query`, `handleToolCall`, `tools`), when the owner turned it on. A headless
install renders no guide card, so only `ask` is available there: showing the
way and actions the visitor approves need the guide's own surfaces. See
[Other AI agents](/agent-tools#assist).

## Typed turns

`getGuideSnapshot()` keeps its four fields for existing integrations. For a
complete custom answer surface, read the typed turn model instead: each
`PuntegoTurn` carries the visitor's question, the answer text, the disclosed
sources, the follow-up chips, a tour proposal, the footer actions the built-in
guide would show, and the turn's status and timings.

```ts theme={null}
const stop = puntego.subscribeTurns(() => {
  const { latestTurnId, turns } = puntego.getTurnSnapshot();
  const turn = turns.find((entry) => entry.id === latestTurnId);
  if (turn) renderCard(turn);
});

function renderCard(turn: PuntegoTurn) {
  // turn.phase: 'submitting' | 'thinking' | 'streaming' | 'complete' | 'stopped' | 'error'
  // turn.status.stage: 'reading_page' | 'searching_docs' | 'checking_account' | 'preparing_guide'
  // turn.answer.text, turn.sources, turn.followUps, turn.proposal, turn.footer.actions
}

// Commands act through the same paths as the built-in guide.
await puntego.selectFollowUp(turn.id, turn.followUps[0].id); // sends the chip's label
await puntego.respondToProposal(turn.id, 'accept'); // "Start tour"
await puntego.replayTour(turn.id); // "Replay tour", once that tour ran
await puntego.runTurnAction(turn.id, 'show_me_how'); // any action in turn.footer.actions
await puntego.rateTurn(turn.id, 'unhelpful', 'Missing the SSO step');
await puntego.resolveTurn(turn.id); // "Got it": records the resolution once
await puntego.retryTurn(turn.id);
await puntego.stopTurn(); // the composer's Stop
```

`getTurnSnapshot()` returns the last 20 turns, oldest first, deep-frozen. The
client re-reads it once per burst of `turn:*` events and returns the same
reference until the next change, so it can back `useSyncExternalStore`
directly. A turn never carries credentials, proofs, raw source identities,
selectors, target ids, or executable actions: web sources expose a navigable
`href`, published notes expose only their title and revision, and generative UI
blocks are reduced to their id, kind, title, and visible text. A chart block
also carries `chart` (its figures with their layout as fractions of the plot)
and a metric grid `metrics` (each tile's limit, meter, change and trend), so you
can draw them yourself; see [React components](/react-components#charts).

Footer actions are ordered tertiary → neutral → primary with at most one
primary. Their labels are localized in the conversation's language (the
page's language until the first answer says otherwise). `footer.meta`
includes `aiLabel`, the visible AI identification your answer surface should
show. It is empty on an `error` turn that shows no generated text and on a
notice the Worker writes itself (the site's pause or a limit), because neither
is an AI answer.

`runTurnAction(turnId, actionId)` runs one of the latest turn's footer actions
(`show_me_how`, `do_it`, `point_it_out`, `take_me_there`, …) from your own
button, through the built-in button's path: it resolves `false` when the turn
does not offer that action now, and `do_it`/`undo` need a real visitor gesture,
so call it from your click handler. React apps can render the whole card with
their own components through [`@puntego/react/ui`](/react-components).

Commands are inert in privacy-only mode, while consent blocks the assistant,
and after `destroy()`. A runtime older than the turn model has no turn
commands: the client then returns an empty snapshot, `rateTurn`,
`respondToProposal` and `replayTour` resolve `false`, and the other commands do
nothing. `replayTour` also resolves `false` on a hosted runtime that predates
it.

## React hooks

Pass `headless` to the page-level provider and render snapshots with normal
React components:

```tsx theme={null}
'use client';

import {
  PuntegoProvider,
  usePuntegoExperiences,
  usePuntegoGuide,
} from '@puntego/react';

function CustomGuide() {
  const { sendMessage, snapshot, turn } = usePuntegoGuide();
  const { engage, snapshot: experiences } = usePuntegoExperiences();
  const announcement = experiences.matches.find(
    (match) => match.definition.content.kind === 'announcement',
  );

  return (
    <aside aria-label="Product guide">
      <p aria-live="polite">{snapshot.assistantMessage}</p>
      <button onClick={() => void sendMessage('What should I do next?')} type="button">
        Guide me
      </button>
      {announcement ? (
        <section>
          <h2>{announcement.definition.content.title}</h2>
          <p>{announcement.definition.content.body}</p>
          <button
            onClick={() => void engage(announcement.experienceId, { kind: 'impression' })}
            type="button"
          >
            Acknowledge
          </button>
        </section>
      ) : null}
    </aside>
  );
}

export function ProductShell({ children }: { children: React.ReactNode }) {
  return (
    <PuntegoProvider appId="gp_your_app_id" headless>
      {children}
      <CustomGuide />
    </PuntegoProvider>
  );
}
```

Both hooks use external-store subscriptions and provide stable server
snapshots, so they can render in React and Next.js hydration paths. The provider
still boots only in a client effect.

`usePuntegoGuide()` also returns the typed turn model — `turn` (the latest
turn, or `null`), `turns`, and the commands `selectFollowUp`,
`respondToProposal`, `replayTour`, `rate`, `resolve`, `retry`, and `stop`:

```tsx theme={null}
function AnswerCard() {
  const { replayTour, respondToProposal, resolve, selectFollowUp, turn } = usePuntegoGuide();
  if (!turn) return null;
  return (
    <article aria-busy={turn.phase === 'thinking' || turn.phase === 'streaming'}>
      <p>{turn.answer.text}</p>
      <ul>
        {turn.sources.map((source) => (
          <li key={source.id}>
            {source.href ? <a href={source.href}>{source.title}</a> : source.title}
          </li>
        ))}
      </ul>
      {turn.followUps
        .filter((followUp) => followUp.kind === 'ask')
        .map((followUp) => (
          <button key={followUp.id} onClick={() => void selectFollowUp(turn.id, followUp.id)} type="button">
            {followUp.label}
          </button>
        ))}
      <footer>
        {turn.footer.meta.aiLabel ? <span>{turn.footer.meta.aiLabel}</span> : null}
        {turn.footer.actions.map((action) => (
          <button
            disabled={action.disabled}
            key={action.id}
            onClick={() => {
              if (action.id === 'start_tour') void respondToProposal(turn.id, 'accept');
              if (action.id === 'not_now') void respondToProposal(turn.id, 'decline');
              if (action.id === 'replay_tour') void replayTour(turn.id);
              if (action.id === 'got_it') resolve(turn.id);
            }}
            type="button"
          >
            {action.label}
          </button>
        ))}
      </footer>
    </article>
  );
}
```

## Experience engagements

`getExperienceSnapshot()` and `usePuntegoExperiences()` expose deployed content,
revision identity, matched route, and checklist state. They accept only typed
engagements:

* impression or dismissal;
* an authored action index;
* checklist completion;
* validated Survey or Form answers.

The browser SDK never accepts a deployment id, revision id, environment,
visitor identity, callback, URL, credential, or signed match receipt from custom
UI code. It reconstructs the authorized engagement from the current matched
snapshot and sends the private proof internally. A stale route, ambiguous id,
invalid payload, expired receipt, or non-headless runtime fails closed.

An accepted authored `start_playbook` action can start only the playbook bound to
the server receipt. A tour with no playbook carries a `start_tour` action: it
starts the tour's authored steps and nothing else. An authored Guide prompt can start a text stream without
reopening the built-in panel. Arbitrary code and arbitrary network execution are
not part of the API.

Snapshots preserve the presentation discriminants for `hint`, `release_feed`,
and `progress`. Custom UI owns the local open/collapse state, while the same
typed engagement methods remain authoritative. See [Hints, release feeds, and
progress badges](/experience-surfaces) for the content shapes and event model.

## Reusable placement Collections

Resolve a stable inline placement without exposing the private match proof:

```ts theme={null}
const placement = await puntego.resolveExperienceCollection('onboarding_card');
const match = placement?.matches[0];

if (match) {
  renderTypedExperience(match.definition, placement.collection.surface);
  await puntego.engageExperienceCollection(
    'onboarding_card',
    match.experienceId,
    { kind: 'impression' },
  );
}
```

The server returns at most the first eligible production Experience, never the
IDs of failed fallback candidates. The public snapshot includes the stable key,
typed surface, Collection version, immutable revision identity, and allowed
checklist state. App identity, audience evaluation, environment selection, and
the signed receipt remain internal. See **Experience Collections** for native
card/banner mounting and lifecycle behavior.

## Consent and security boundaries

Headless mode disables the built-in launcher, chat panel, automatic opening,
automatic questions, proactive nudges, voice hotkey, and built-in Experience
renderer. It does not disable required consent/privacy controls, PII scrubbing,
target policy, risk confirmation, action postconditions, or audit events.

Keep these boundaries in your custom UI:

* Mint authenticated customer identity tokens on your server and pass each
  one-time token to the first loader/provider mount. Never persist it.
* Render status and errors with `aria-live`; keep controls keyboard accessible
  and restore focus when custom dialogs close.
* Show a clear confirmation for consequential action requests and a durable
  success/failure state. Runtime policy remains authoritative.
* Call `trackEvent(name)` only for bounded, authored milestone names. Arbitrary
  event payloads are intentionally unsupported. CRM user/group traits must be
  supplied by your authenticated server through the one-time identity handoff.
  Account facts (plan, seats, usage, features) go through `setContext`: facts
  the page reports only ground answers, and anything that needs proof must be
  signed by your server (`signedAccountFacts`).

## Auditability

Guide streams continue to emit typed runtime events. Experience engagement
responses include only public acceptance, duplicate, action receipt, and
submission receipt identifiers. The runtime's signed matching proof never
appears in a snapshot or event.

Each turn emits `turn:start` once, `turn:status` per stage change, `turn:text`
per released text batch, coalesced `turn:update` for anything else, and exactly
one of `turn:complete`, `turn:stopped`, or `turn:error` (a turn replaced or
cleared before it ended gets `turn:stopped`). `turn:removed` fires once when a
turn leaves the conversation (a new conversation, an account switch), so a UI
that caches turns drops it. `turn:resolved`, `feedback:submitted`,
`panel:open`, `panel:close`, and `navigate` (fired just before an in-product
navigation) complete the set. The `chat:*` events keep
firing unchanged.

Use `experience:engaged`, `guide-action:proposed`, `guide-action:started`,
`guide-action:completed`, and `guide-action:failed` for host-side observability.
The authoritative action and Experience audit records remain on Puntego's
authenticated backend.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.