Conversation history in an embedded widget
You can show each visitor their own conversation history in an embedded chat without adding a host-site backend or database. History is opt-in at initialization. Older widget builds keep the existing init response and do not mint visitor credentials.
Use the public /v1/client/* API for browser history. Do not ship a management API key to a browser. That key authenticates the records API.
Enable and initialize history
Configure behavior.conversationHistory on a chat product surface:
Set enabled to false to disable history. When the configuration block is present, enabled defaults to true. visitorIdleDays defaults to 90 and clamps values to 1 to 365 days.
Send visitorHistory: true to POST /v1/client/init to request the history capability. Use the returned visitor secret for later history requests:
Replace CLIENT_TOKEN with the client token for the embedded surface.
The response includes these history fields:
Persist conversationId instead of sessionId. A session is short-lived, but the conversation is what a returning browser reopens. Pass the returned targetId to the history list and delete filters.
An agent-only client token can store conversations under the agent’s primary flow. That flow can differ from the ID that you configured. conversationRevision is an opaque change token. For details, see Detect changes from another device.
A data-only Runtype App returns app instead of flow and omits targetId. It is not chat-capable, so it has no conversation history to list.
The server returns the raw visitor secret only when it mints the visitor. It stores only a SHA-256 hash and scopes the secret to one client token. Use a client-token-specific browser storage key so two widgets on the same origin do not share a secret.
Save a newly returned visitor.token before doing other work. The server cannot recover it. If
you lose the token or it expires, a history-capable init without a usable token starts a new
anonymous visitor with empty history.
Conversation titles
The API creates a conversation record at session init, before it stores a message. The stored name starts as a timestamp label such as cs_01jq8z6k2r - Aug 7, 2026. The history list exposes that value as title.
Set behavior.conversationTitles.enabled to true to generate a title from the opening exchange. Account-level title generation must also be enabled:
The following table describes the conversation-title settings:
If you omit model, title generation uses the model that ran the surface. If that model does not resolve, it uses your account’s default model. If neither model resolves, title generation skips the conversation.
Title generation is opt-in because it sends the opening visitor and assistant messages to a model in a separate call. The service does not use a platform-selected model when your configured or account model does not resolve.
A title is generated only for a history-capable session. Request history with visitorHistory: true, or provide visitorToken, identityProof, or conversationId at init. A surface with piiRedaction: "redact" is not titled because title generation reads the stored, unredacted messages.
Title generation does not block or fail a chat turn. After it succeeds, it does not replace a title that you set through the API or dashboard. If all configured models fail, the timestamp label remains and later turns can retry while the conversation has 10 or fewer stored messages.
The setting applies to the surface that owns the client token. A token that is not associated with a surface does not have title configuration, so its conversations are not titled.
List and read conversations
Use a live sessionId in the query string and the visitor secret in X-Visitor-Token for every history request. Neither a public client token nor a session ID authorizes history by itself.
List one page of conversations with this request:
The following table lists the query parameters for the conversation list:
If you send different values for targetId and flowId, the API returns 400 with conflicting_target_filter. It accepts both parameters when their values match.
Each summary includes preview, a plain-text excerpt of the most recent visitor-visible message. The API limits it to 140 Unicode code points and returns null when that message has no text.
The list includes only conversations with stored messages. It sorts results by updatedAt, then by ID, with the newest result first. Follow nextCursor until it is null.
Call GET /v1/client/conversations/{id} to read the newest transcript page. Pass messageCursor from the previous response to load the next older page. Messages are oldest first within each page. The API rehydrates media only for the requested page and clamps messageLimit to 25.
Render what the visitor saw, not what the model read
Render the visitor-visible projection instead of assuming that content is safe to display. The content field is the model channel. It can contain structured payloads, hidden variants, or directives that the visitor did not see.
The following table describes the fields in a history message:
When you render a message, use displayContent when it is present. Otherwise, use content only when displayAvailable is true. Do not treat either field as model input.
Send displayContent alongside content in a /v1/client/chat message to record a projection. This covers visitor messages and assistant text that you know before dispatch.
Finalize the last assistant turn
After your renderer finishes an assistant stream, send the final visible text to the projection endpoint:
The API returns a conversationRevision field.
The endpoint only fills or replaces displayContent. It does not create a message or change its model content, role, ordering, timestamps, message count, ownership, or title. The batch applies atomically, and a revision guard prevents it from overwriting a chat turn that the API stores at the same time.
You can replay the same projection. The API performs no write and returns the same revision when the value is unchanged.
If the endpoint returns 409, the conversation changed during the write and nothing was stored. Retry the request, or let the next chat turn carry the projection.
A turn that went through /v1/client/resume is the one case where the assistant
message is written for you: the resumed leg’s completion is persisted server-side.
Send the same id as assistantMessageId on the resume request that you later name
in this call. Without it the completion is stored under a server-minted id, which
your projection cannot address, and the batch answers 404.
A changed projection refreshes the list preview but does not change updatedAt.
Finalizing a projection therefore does not reorder history.
The endpoint returns 404 when the live session is not bound to the conversation
or the visitor does not own it. It also returns 404 when the batch names a
message that the conversation does not store. One unknown message ID fails the
whole batch.
The endpoint accepts up to 50 messages per batch, limits each displayContent
value to 32768 characters, and limits the batch to 49152 characters. These limits
keep a page-exit request within the browser keepalive budget.
If a finalization is lost, the next chat turn repairs it: re-send the message with
its displayContent. The API uses only the projection for an existing message and
keeps its stored model content.
Detecting changes from another device
Compare conversationRevision for equality only. The token is opaque, unordered, and not parseable.
The token changes on every transcript change, including a projection finalization that leaves updatedAt unchanged. Use it to detect whether a cached transcript is current without reordering the history list.
When you send /v1/client/chat, the API executes the stored transcript and merges in only message IDs that are new in your request. Turns from another device remain in the execution context, and stored content wins for an ID that already exists.
Reopen a conversation
Use a listed record ID as conversationId on a new init request. Do not maintain a browser-local conversation-to-session map.
Reopen a listed conversation with this request:
conversationId and legacy sessionId are mutually exclusive. The stored conversation target is authoritative, so a resume request cannot switch the conversation to another flow or agent. Missing, deleted, disallowed, and out-of-scope conversations return 404.
Treat sessionId as a value that the API issues. Do not accept it from a URL, query string, or
other third-party input. An attacker can use an unowned session ID to make this visitor resume the
attacker’s conversation. Use conversationId for cross-session or cross-device resume because the
API authorizes it with the visitor secret.
Delete or reset
Delete one conversation with DELETE /v1/client/conversations/{id}, or delete all conversations with DELETE /v1/client/conversations. Pass sessionId in the query string and X-Visitor-Token in the header for both requests.
For a target-scoped history UI, pass targetId to bulk delete. Omit targetId to delete every conversation in the authorized visitor scope across all targets. Deletion removes stored transcript assets and the conversation record. If the deleted conversation backs the active chat session, initialize a new session before sending another message. The chat endpoint returns 410 conversation_deleted instead of running a turn that it cannot persist.
To revoke the visitor during widget shutdown, logout, or a “forget this browser” action, call the reset endpoint:
Reset is idempotent and returns { "reset": true }, even when the secret is expired or already revoked. A later history-capable init mints a clean visitor.
Verified cross-device history
Pass a registered identity proof as identityProof at init to bind the browser visitor to an admitted eu_* end user. If another person signs in on an already-bound browser, the API mints a new visitor instead of giving that person the existing history.
The binding is durable, but it does not authorize sibling-device history by itself. To widen list, read, or delete scope beyond the exact browser, send a fresh proof on each request:
Use identityProof in the init body for conversationId resume. The admitted identity must match the visitor’s stored binding. Without a fresh proof, a bound visitor can read only conversations owned by that browser.
Identity Exchange admission must be enabled for the account. With admission enabled, invalid,
expired, cross-tenant, or mismatched proofs fail closed with 401. The API does not rebind the
visitor or return sibling history.
Know which scope you received
The API evaluates a proof only when end-user admission is enabled for the account. Without admission, init ignores a supplied proof and uses browser scope. A history request that includes that proof returns 503 with identity_proof_not_admitted before it reads or deletes data.
Init reports the result in the visitor grant. Every history route reports it in the CORS-exposed X-History-Identity-Status response header:
Init succeeds with ignored so ordinary chat continues. Do not offer verified cross-device history or verified erase in that state. A history route with the same proof returns 503 with identity_proof_not_admitted. Retry without the proof to act at browser scope.
These statuses report the request outcome. They do not grant authority or carry identity information.
Mobile, native, and custom clients
Use the same public HTTP and SSE API from a mobile app, desktop app, or custom web client. The chat widget is the reference client, not a requirement.
For a custom client, call POST /v1/client/init with the client token and visitorHistory: true. Store the returned cvt_* secret in Keychain or Keystore, call POST /v1/client/chat with the session ID, and read history with sessionId and X-Visitor-Token. Read title from each list response to display conversation titles.
Native fetch requests omit the Origin header. Add the literal native sentinel to allowedOrigins to admit originless requests. The sentinel does not match a request that sends Origin: native.
Do not put native on a token that you also ship on the web. A client token is public, so a page
can remove the Origin header and use that token without origin protection. Issue separate tokens
for web and native clients. Use real origins for web tokens and native for native tokens. Rely
on the native token’s capability scope, namespaces, rate limits, and end-user identity as its
security boundary.
Bind the native visitor to an eu_* end user at init when you need cross-device history. For more information, see Verified cross-device history.
Errors and recovery
Use the following status table to handle common history responses:
When a history request returns 401, run one history-capable init with the stored token. If init returns a new token, replace the stored value and show empty history. Retry no more than one time to avoid a recovery loop.
Next steps
Continue with one of these guides:
- End-user identity and credentials: bind history to a verified end user.
- Client tokens and domain restrictions: configure browser and native token origins.
- Embedding the chat widget: add the embedded client.