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. This guide covers the public /v1/client/* API that the chat widget and custom clients use. History is off unless your client requests it at initialization.
A management API key authenticates the management API, including the records API. Never ship it to a browser. Use the public /v1/client/* routes with a client token instead.
Before you begin
Make sure that you have the following:
- A chat product surface. For more information, see Setting up a chat surface.
- A client token for that surface, with
allowedOriginsset to the sites that host your chat.
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 sets how long a browser can go unused before it loses access to its history. It defaults to 90 and clamps values to 1 to 365 days. Each successful history-capable request renews the window, and visitor.expiresAt in the init response shows the current expiry. After the window expires, the browser starts over with empty history.
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 stores conversations under the agent ID, which is the targetId the init response returns. 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
Runtype creates a conversation record at session init, before it stores a message. The stored name starts as a label built from the session ID and creation time, such as cs_01jq8z6k2re9v0w1m3n5p7s9t2 - Aug 7, 2026, 6:30 PM. The history list returns that value as title.
Set behavior.conversationTitles.enabled to true to generate a title from the opening exchange. Conversation titles must also be available for your organization. If titles do not appear, contact Runtype.
The following surface configuration turns on title generation:
The following table describes the conversation-title settings:
If no model resolves, the conversation is not titled. Runtype does not substitute a platform-selected model.
Title generation is opt-in because it sends the opening visitor and assistant messages to a model in a separate call.
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.
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.
The following table describes the fields in each conversation summary:
Call GET /v1/client/conversations/{id} to read the newest page of a transcript. The following table lists its query parameters:
The response contains the summary fields, plus messages, nextMessageCursor, and conversationRevision. Messages are oldest first within each page. To load the next older page, pass the previous response’s nextMessageCursor as messageCursor. The cursor is null when no older messages exist.
Render what the visitor saw, not what the model read
Each history message has two text fields. content is what the model received. displayContent, the display projection, is the text the visitor saw after your client rendered it. content can contain structured payloads, hidden variants, or directives that the visitor did not see, so do not assume that it is safe to display.
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.
To record a display projection, send displayContent alongside content in a /v1/client/chat message. This covers visitor messages and assistant text that you know before you send the request.
Finalize the last assistant turn
Every /v1/client/chat and /v1/client/resume turn stores the assistant reply for you. To address that reply later, generate an ID and send it as assistantMessageId on the chat or resume request. If you omit it, Runtype assigns its own ID, and a display-projections request that names your client’s ID returns 404.
The following chat request names the assistant reply:
Replace userMessageId with your ID for the visitor message and userText with the visitor’s text.
After your renderer finishes the assistant stream, send the final visible text to the display-projections endpoint with the same ID:
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 display 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 display projection.
A changed display projection refreshes the list preview but does not change updatedAt. Finalizing a display 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 display projection for an existing message and keeps its stored model content.
Detect 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 display-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 runs the stored transcript and merges in only message IDs that are new in your request. Turns from another device remain in the conversation context, and stored content wins for an ID that already exists.
Reopen a conversation
Use the id of a listed conversation 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. If you send conversationId without a usable visitorToken or identityProof, init returns 401 with visitor_required. 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. Both return { "deleted": N } with the number of deleted conversations. Delete-all returns 0, not an error, when the visitor has no history.
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.
identityProof takes either form:
- A string that holds the proof token. Runtype selects the identity integration by the token’s verified issuer.
- An object with
providerandtoken, the same form that chat and dispatch requests take. Setproviderto the integration’s provider value, for exampleclerk. If the token verifies against an integration with a different provider value, init returns aninvalid_identity_prooferror whoseerrorDescriptionisprovider_hint_mismatch.
Replace YOUR_CLIENT_TOKEN with the surface’s client token and YOUR_IDENTITY_PROOF with a fresh token from your identity provider. Check visitor.identityStatus in the response: admitted means Runtype verified the proof, and ignored means Identity Exchange admission is not enabled for your organization.
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 your organization. For more information, see
Pass a proof on a request.
With Identity Exchange 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 Identity Exchange admission is enabled for your organization. Without Identity Exchange 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
An error response body contains an error field. Branch on the stable codes in the following table. Other error values are readable messages that can change. Some responses also carry a hint or errorDescription field that explains the cause.
The following table lists the common history responses:
When a history request returns 401 for an expired session or an unusable visitor secret, 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.
When a request returns 401 with invalid_identity_proof or visitor_identity_mismatch, get a fresh proof from your identity provider. Then run a new init with that proof in identityProof.
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.