Chapter 12Beta

Build your own chat interface.

Put a conversation wherever it helps: inside a setup step, beside a line of code, or in a character that brings your website to life. You or your AI coding agent build the experience. Stand handles the AI replies, human conversations, routing, and history.

Live custom UI · Beta

A real conversation, your own UI.

Talk with the Stand team or their AI Stand-in. Sending your first message starts a real chat.

View the code

Stand

Stand chat

Checking availability…

Ask us about Stand or building your own chat interface.

Replies appear when complete.

Stand Guidebook

Chapter 12 of 12

Field guide

What to learn in this chapter

Looking for an idea? Start with the reel and the design brief. Ready to build? Copy a working client, configure your site, and follow the complete visitor protocol below. This is a supported beta integration: your developer or coding agent needs browser JavaScript, HTTP, and WebSockets, and should retest when adopting contract changes.

Need an exact capability definition, plan requirement, or limitation? Browse the Feature Reference.

From idea to implementation

01

Design the invitation, then the conversation.

Start with a moment your visitor cares about: choosing a plan, understanding a code example, or exploring a story. Decide what invites the question, what context the responder needs, and where the answer belongs. An unusual interface earns its place when it helps someone take that next step.

Borrow a complete example from the reel or use this page’s smaller React client. Keep the network adapter and transcript state separate from the presentation so you can change the visual design without rebuilding delivery and recovery.

Page context makes the experience useful. Supply relevant starting context in the session prompt; it is fixed at creation. When the visitor changes a selection or step, include that current context with their next question. The wizard and documentation examples show how to keep contextual questions readable in Stand’s transcript. Page navigation and annotations are implemented by your UI; they are not new server-side message types.

Code
DecideThe invitation
Example design briefA visitor comparing plans can ask beside the feature they are unsure about.
DecideThe context
Example design briefInclude the selected plans, the feature name, and the visitor’s question. Send only relevant, non-secret information.
DecideThe answer
Example design briefKeep the answer beside the comparison. Preserve the same conversation when the visitor changes plans.
DecideThe next step
Example design briefOffer links to known page sections. Let the visitor choose whether to follow them; keep page actions under your code’s control.

Scope and requirements

02

Replace the visitor UI, keep the Stand conversation service.

You do not need to load stand.js or ui.js for a fully custom client. Those bundles are the supplied launcher and chat implementation, not a separately supported headless JavaScript SDK. Use the documented network contract below. For a styled inline conversation, stand-chatbox keeps the supplied widget’s full message rendering, cards, recovery, and controls while exposing CSS variables, shadow parts, and slots. Use stand-button or stand-card for a page-native invitation, or the public JavaScript API for an existing control.

The visitor API is available on all plans. Existing plan entitlements, configured skills, routing, concurrent capacity, and chat quotas still apply. Creating a session consumes chat capacity even if no visitor text has been sent; opening a local panel or running discovery does not itself create a conversation. Your team owns the custom code, hosting, accessibility, and browser testing.

In Stand, add and enable the real website in Sites and copy its site ID from the generated installation snippet. Configure a human with I chat here and availability, or enable an eligible Stand-in. Run discovery from that page: a valid site/domain discovery records installation observation, so loading the default widget is not a prerequisite. Use a separately configured site and eligible responder for a different test hostname.

To prototype before registering, use demo as the siteId for discovery and session creation. Stand Chat’s demo Stand-in then answers from any page domain. Only that Stand-in responds, and demo conversations do not appear in your account. Switch to your own site ID to test your responders, instructions, knowledge, skills, and History.

Use https://api.stand.chat for production HTTP and wss://api.stand.chat for WebSockets. Browser requests use credentials: omit; the visitor endpoints allow cross-origin requests without cookies. Allow both API origins in your Content Security Policy connect-src, and allow the image origins you actually render. Use the current absolute page URL for page, including its path; domain matching normalizes case and a leading www., but arbitrary subdomains do not match.

siteId and the responder identifiers returned by discovery are public identifiers. They are not credentials. Never put a rep/admin JWT, account password, or backend integration secret in a visitor client. A visitor token authorizes one conversation only; it does not authenticate the visitor to your own application.

Working example · Beta

03

Use this page as a working example.

The chat at the top of this page is a React client connected to Stand’s real website coverage. It discovers a responder on mount and starts a conversation on the first send. The two files below are its actual implementation, published at build time.

The framework-independent client handles HTTP sends, WebSocket replies, storage, and recovery. The React component renders completed text, links, identity changes, email follow-up offers, notices, and attribution. Copy both, keep the protocol handling, and change the presentation.

Adapt the example to your site

  1. Replace the example’s Site ID with demo to prototype, or your registered Site ID to use your own responders. Pass the current page URL; a production Site ID does not cover an unregistered localhost or preview domain.
  2. Remove the getEmbedConfig import and replace its call with { apiBase: 'https://api.stand.chat', wsBase: 'wss://api.stand.chat' }. The client appends endpoint paths.
  3. Update the component’s local client import. Replace this site’s Tailwind classes with your design; the client itself has no React or widget dependency.
  4. Implement any optional presentation you need, then run the launch checklist against your own configured site.
Example behavior and deliberate omissions

This component uses English controls and omits streamed previews, typing indicators, rich Markdown, behavior-rule triggers, and optional launcher/greeting analytics. It implements its own inline presentation without the widget JavaScript API.

One browser-side client retains the transcript, pending send, and draft across Next.js navigation. Leaving this chapter releases its socket, retries, and listeners; returning restores and reconciles the conversation. Environment-and-site-scoped sessionStorage supports same-tab reload recovery. With storage unavailable, continuity lasts only for the JavaScript lifetime.

The floating widget is hidden on this chapter and returns when you leave, with its independent conversation state preserved. Mounting this example in another app requires equivalent lifecycle handling.

If closure interrupts a send, unconfirmed text stays visible and, when storage works, survives reload. A late successful response can still confirm it. Otherwise, New chat restores it as an editable draft for the visitor to review and send. This sample clears credentials at closure without a final transcript fetch; without a canonical confirmation, delivery remains uncertain. The full contract below also describes final recovery before clearing credentials.

Uncertain first-start requests need an explicit visitor decision before another start. Retries within an active conversation reuse the original client message ID; nothing is automatically replayed into a new conversation.

TypeScript
TypeScript

1. Discovery

04

Ask Stand which responder is available.

The protocol calls a persisted message canonical, a local pending bubble optimistic, and a short-lived update such as typing transient. Reconcile means confirming or replacing a pending bubble with its server message. Read all six lifecycle sections together before implementing.

GET /v1/reps/find is public. Send siteId and page, plus greetingsEnabled=true if you render the supplied greeting (otherwise false). Pass a previously rendered greetingVariantId only when reusing that greeting. URL-encode query values with URLSearchParams.

A successful response is either { available: false } or an available responder with the fields below. Unavailable can mean no coverage, exhausted capacity, a disabled site, an ineligible page, or temporarily unavailable routing data. Treat it as a normal UI state. Do not create a session or invent a responder ID when unavailable.

Discovery is a point-in-time offer, not a reservation. Create can still return 409, and a requested human can be replaced by an eligible teammate or Stand-in. The session response, its initial system cards, and later handoff events are authoritative for the assigned identity.

Code
Response fieldavailable
Contract and client responsibilityBoolean. Only proceed with a true result and a usable responder identifier.
Response fieldresponderType; repId; standinProfileId
Contract and client responsibilityresponderType is rep or standin. For a human, use repId. For AI, use standinProfileId; repId is null and the owner rep ID is not exposed. Send exactly the selected identifier when creating the session.
Response fieldrepName; repTitle; brandName; avatar
Contract and client responsibilityVisitor-facing identity and avatar URL, including when the responder is AI. Optional values can be absent or null. Render identity text safely.
Response fieldgreeting; greetingVariantId; showId
Contract and client responsibilityGreeting text, its optional experiment attribution, and a show correlation ID. Preserve the variant only for a greeting actually shown; carry showId into activation and create.
Response fieldsensitiveNoticeText; poweredByUrl
Contract and client responsibilityConfigured sensitive-data notice and Stand attribution URL. Preserve required notice/attribution behavior; validate outbound URLs.
Response fieldlapelPin
Contract and client responsibilityOptional object: pinId, orgId, domain, pinType, color, borderColor, fontFamily, fontColor, pillText, circleIconUrl, pillLogoUrl, usePinAsBotAvatar. pinType is none, circle, pill, or free; the supplied UI renders circle/pill, while free is a policy value delegating pin choice to reps/agents. Treat text and style values as data. Discovery avatar already includes server-side pin-as-avatar selection; do not replace it a second time.
Response fieldbehavior
Contract and client responsibilityOptional matched declarative rule: ruleId, name, enabled, pathPrefix, hideButtonUntilTriggered, presentation {noGreetings, halfSize, stepAside}, trigger, action. Your custom client implements the relevant behavior or explicitly owns its own presentation rules. Never execute customJavascript; that compatibility field is empty or omitted.
Response fieldbehavior.trigger
Contract and client responsibilityObject {type, delayMs, scrollPercent}. type is immediate, delay, firstScroll, scrollDepth, or none. delayMs is 0–60,000; scrollPercent is 1–100 (default 70). The supplied launcher applies delayMs after the trigger condition, including scroll triggers; none disables automatic activation.
Response fieldbehavior.action
Contract and client responsibilityObject {type, initialMessage}. type is showButton, openChat, or openChatAfterGreeting; initialMessage is at most 500 characters. showButton reveals the launcher; openChat opens the supplied widget with initialMessage as the responder greeting, not as visitor speech. openChatAfterGreeting uses the supplied greeting presentation to open desktop chat; these presentation details are client behavior, not a server timer.

2. Start

05

Create once, then use the returned visitor token.

POST /v1/sessions with Content-Type: application/json and no Authorization header. Required context is page and siteId, plus exactly one responder identifier from discovery: repId or standinProfileId. The server revalidates the site, path, responder, and capacity. Do not send a fabricated visitor ID.

Serialize creation in the client so a double click cannot start two conversations. POST /v1/sessions has no client idempotency key. Do not automatically replay a creation request after an ambiguous network failure: it may already have created a conversation that counts against the chat quota. Present a retry decision to the visitor instead of a background retry loop.

The response contains sessionId, status, createdAt, lastActivityAt, closedAt, closedBy, participants, messages, page, siteId, websocketUrl, visitorToken, and conversationLanguage. Timestamps are ISO 8601 strings; absent optional values may be null. Require a non-empty sessionId and visitorToken before considering creation successful.

Save sessionId and visitorToken together in storage scoped to this API environment and site. Preserve the discovery notice and attribution URL with that conversation too: session reads do not return sensitiveNoticeText or poweredByUrl. Handle denied/full storage by continuing in memory.

The visitor participant is participants.find(p => !p.isRep); its userId comes from the server. Host participants have isRep: true and can represent a human or Stand-in. The responderType from discovery is only a preliminary identity. The creation response has no responderType field: apply its session-start card and later takeover/handoff cards to update AI/human disclosure. isRep alone does not mean human. Host presentation includes name, brand, title, and avatar when available.

Render the returned messages as the canonical initial transcript. If initialMessage was included in create, do not send it again. Remove or reconcile your optimistic initial bubble against that snapshot; create does not accept clientMessageId. A Stand-in session includes a persisted session-start card, and the first AI turn can begin before the socket opens, so recover the transcript again after connecting.

Code
Optional creation fieldinitialMessage
Meaning and limitsFirst visitor-authored text, or null/omitted when opening an empty conversation. Do not substitute page instructions for visitor speech.
Optional creation fieldincludeOpeningGreeting; openingMessage
Meaning and limitsSet true only when persisting the opening greeting your UI shows. openingMessage supplies that text; if omitted, Stand uses the selected responder greeting. Render the canonical result once.
Optional creation fieldprompt
Meaning and limitsPer-session internal context, up to 2,000 characters. Stored as system-prompt; hidden from visitors, visible in dashboard/history and AI context. It is not a secret channel or an authorization rule.
Optional creation fieldvisitorTimezone
Meaning and limitsOptional IANA timezone, for example Europe/Helsinki. Obtain from Intl.DateTimeFormat().resolvedOptions().timeZone when available.
Optional creation fieldvisitorLanguage; visitorLanguages; pageLanguage
Meaning and limitsBCP 47 browser/page language hints; invalid tags are ignored. conversationLanguage is the authoritative current language and may later change based on visitor text.
Optional creation fieldgreetingVariantId; showId
Meaning and limitsDiscovery attribution. Only attribute the greeting variant actually rendered.
Optional creation fieldactivationId; activationSource; activationAnalyticsId
Meaning and limitsCorrelate the UI activation with this chat; use the same activation ID as the optional activate event.
Optional creation fieldvisitorExternalId; visitorIdentityName
Meaning and limitsOptional host-asserted identity; each trimmed and limited to 255 characters. Name requires an external ID. These are unverified correlation metadata, never authentication, authorization, or proof of account ownership. Raw identity fields are not returned to visitor clients.

3. HTTP contract

06

Use session-scoped authorization for every later request.

Send Authorization: Bearer <visitorToken> on all requests in this table. Use JSON request bodies where specified and credentials: omit. Treat the token as opaque; do not parse it or log it. Production visitor tokens currently have a fixed 24-hour lifetime from issuance; activity does not refresh them and there is no visitor refresh endpoint. A later session read does not reissue the token. Closing a session does not itself revoke its token: an authenticated read can return its closed archive until the token expires.

On closure, stop sends and reconnects. If a send remains unconfirmed, a final authenticated read may recover its accepted message; perform that read before clearing credentials. Then discard local credentials while retaining unresolved text. A failed final read does not prove rejection.

Closing a UI panel or disconnecting its socket is not ending the chat. Only send DELETE when the visitor explicitly ends the conversation. With usable credentials, end or confirm the closure of the current chat before offering a new one; merely discarding credentials would leave it running. If authorization is unusable, explain that you cannot confirm closure and let the visitor explicitly start again through discovery/create. The server remains authoritative for token validity and session state.

Code
Method and pathGET /v1/sessions/{sessionId}
RequestOptional messageLimit (integer; default 200, maximum 500; 0 for metadata; negative is invalid).
Successful response / behaviorSession details and the most recent N messages, plus conversationLanguage and optional closedByType for a closed session. No visitorToken field is reissued. Visitor responses exclude rep-private labels and AI usage. No cursor for older visitor transcript pages.
Method and pathPOST /v1/sessions/{sessionId}/messages
Request{ body: string, type: "text", clientMessageId: string }
Successful response / behaviorA canonical Message. Use a unique clientMessageId per logical send and reuse it on retry; while the session is active, the original message is returned for a duplicate in that session. After closure, recover the transcript instead of expecting a retry acknowledgement. Sender identity and text type are server-derived.
Method and pathDELETE /v1/sessions/{sessionId}
RequestNo body.
Successful response / behavior{ sessionId, status: "closed", closedAt, closedBy, closedByType? }. This is terminal metadata, not a full session snapshot. Stop sends/reconnects; finish any final transcript recovery before clearing local credentials. Also handle session.closed.
Method and pathPOST /v1/sessions/{sessionId}/followup-request
Request{ email: string }
Successful response / behavior{ sessionId, status: "closed", followupRequested: true }. Only valid while a rep-followup-offer is active. Email is trimmed/lowercased, validated, and limited to 320 characters.
Method and pathPOST /v1/sessions/{sessionId}/link-clicks
Request{ messageId: string, url: string }
Successful response / behaviorCanonical system-card recording a real click. messageId must identify a server-issued link-card and url must match that card. Best-effort tracking; do not block navigation or retry indefinitely.

4. Real-time protocol

07

Distinguish persisted messages from transient events.

Connect to the session response websocketUrl with ?token=<URL-encoded visitorToken>. Validate its scheme and expected API host before attaching credentials. In production use wss:. The browser WebSocket API cannot set an Authorization header. Keep token-bearing socket URLs out of analytics, error reporting, and access logs under your control.

All frames are JSON. Canonical messages and terminal state changes must be handled even if you omit typing indicators or streamed previews. Unused transient events can be ignored safely. The server acknowledges an established subscription with { type: "connected", sessionId }, but live messages or a terminal close can arrive before that acknowledgement. Process frames immediately. After connected, fetch a session snapshot while also receiving events and merge by messageId and seq. This covers the interval between create or a previous snapshot and the socket subscription. There is no WebSocket replay cursor or exactly-once delivery guarantee.

Client-to-server sends use type: "message" and messageType: "text". Server-to-client persisted messages use event: "message" and type as the content type, for example text or system-card. Do not dispatch incoming chat solely on type === "message". REST Message objects have the same canonical fields but do not need the event wrapper.

Code
Code
Incoming type / discriminatorevent: "message"; type: text, link-card, system-card, or standin-idle-prompt
Additional fieldsMessage fields shown above. senderType can be visitor, rep, standin, system-card, or internal system-prompt. Canonical AI events may also carry turnId.
Client actionMerge canonical transcript by messageId, reconcile pending sends by clientMessageId, sort by numeric seq, then render according to the next section.
Incoming type / discriminatortyping
Additional fieldssenderId, senderType, active, sentAt
Client actionOptional typing UI: expire it after a short silence and clear on active:false, a message, or disconnect. Ignore your own typing echo.
Incoming type / discriminatorstandin.status
Additional fieldsturnId, phase: typing | reconnecting | fallback | clear
Client actionOptional progress UI: clear matching turn state; discard a partial preview on fallback.
Incoming type / discriminatorstandin.delta
Additional fieldsturnId, seq, text
Client actionOptional streaming UI: text is the FULL accumulated preview, not a token to append. Replace only for a newer per-turn seq. This seq is separate from transcript seq. Replace the preview with the final canonical AI message; discard it on restore/closure.
Incoming type / discriminatorconversation.language
Additional fieldslanguageTag, source: "visitor"
Client actionFor a localized client, update UI copy. A fixed-language client may ignore this event. The REST conversationLanguage remains authoritative on recovery.
Incoming type / discriminatormessage.rejected
Additional fieldsreason: "session_reassigned", message
Client actionA send raced with reassignment. Keep it pending, recover session state, and only retry with its original clientMessageId when active.
Incoming type / discriminatorsession.closed
Additional fieldssessionId; closedAt, closedBy, closedByType when available
Client actionTerminal state: stop sends/reconnects and show an ended conversation. Finish any final transcript recovery, then clear cached credentials and retain unresolved text. closedByType is rep | visitor | standin | system | other; missing means other. Do not infer identity from raw closedBy.
Incoming type / discriminatorUnknown event or extra field
Additional fieldsMay appear during beta evolution or rolling deployments.
Client actionIgnore safely without throwing or displaying raw JSON. Rep-private events, including conversation labels, are not part of this visitor contract.

5. Rendering and skills

08

Keep the meaning of each message while changing its appearance.

body is a string. Text and standin-idle-prompt messages contain visitor-facing text. link-card and system-card bodies contain serialized JSON: parse defensively, dispatch by message type, then check cardType for system cards. Link-card bodies have no cardType. Never render arbitrary message HTML. Plain text is acceptable; if supporting Markdown, escape HTML and validate links. Hide any message whose type or senderType is system-prompt on both live and restored paths.

Preserve truthful AI/human identity, the configured sensitive-data notice when returned, and Powered by Stand attribution when poweredByUrl is returned. Apply the account’s branding entitlement instead of assuming a custom layout removes it. Unknown or malformed cards should not break the transcript or expose raw metadata.

AI skills still run on Stand. Link sharing and human handoff need the client behavior below. OpenAPI write confirmation is a later visitor-authored text message consisting of confirm or confirmed (case-insensitive; surrounding whitespace and trailing periods/exclamation marks are ignored). A custom confirmation button may send that text only after an explicit visitor click on the displayed action; never auto-confirm. There is no browser integration-secret or confirmation-token endpoint.

The current visitor send API is text-only. File uploads, image attachments, arbitrary HTML messages, custom server-side message types, rep administration, and private conversation labels are not capabilities of this interface. A decorative avatar in your UI does not add a media-message API.

Content / cardTypelink-card
JSON body fieldsurl, title?, description?
Required behavior when presentRender a useful link, permitting only http: or https:. For a new tab, use noopener/noreferrer. Track an actual click with messageId and url through link-clicks.
Content / cardTypesession-start
JSON body fieldscardType, standinName, greeting
Required behavior when presentShow a start notice; greeting is metadata, not an instruction to duplicate an already persisted opening message.
Content / cardTypehandoff; human-transfer
JSON body fieldscardType, repName, repTitle, repBrandName, repAvatar, message
Required behavior when presentAnnounce the human and update header identity; remove the AI badge.
Content / cardTypestandin-takeover
JSON body fieldscardType, standinName, standinTitle, standinAvatar, message
Required behavior when presentAnnounce AI coverage and update the header and AI disclosure.
Content / cardTypesession-end
JSON body fieldscardType, reason
Required behavior when presentRender an end notice; use session status/session.closed for the terminal transition.
Content / cardTyperep-followup-offer
JSON body fieldscardType, repName, message
Required behavior when presentOffer an email form for an unanswered human chat. Submit through followup-request; dismiss when a human reply supersedes it.
Content / cardTyperep-followup-confirmation
JSON body fieldscardType, message
Required behavior when presentShow successful follow-up submission, then the closed state.
Content / cardTypelink-clicked
JSON body fieldscardType, messageId, url
Required behavior when presentTracking metadata from link-clicks. Do not show as a raw visitor card.
Content / cardTypefollowup-requested
JSON body fieldscardType, reason, preferredContact?
Required behavior when presentFollow-up metadata. Do not show as a raw visitor card; this is distinct from the actionable rep-followup-offer email form.

6. Recovery and lifecycle

09

Treat the server transcript as the source of truth.

On page reload, try a saved session before fresh discovery. GET its details with its token. For active sessions, restore the authoritative participants, conversationLanguage, and canonical transcript, then connect. After connected, fetch again and merge any concurrent socket messages. Restore AI/human identity by applying the transcript’s start/takeover/handoff cards as well as participant data. Restore the saved discovery notice and attribution with that session; a fresh availability offer describes a potential new conversation and must not overwrite the current participants.

On unexpected disconnect, retain pending sends and show a reconnecting state. Use bounded exponential backoff with jitter; recover through HTTP before reconnecting and after connected. Stop when the server says the session is closed, credentials are rejected, or the visitor ends it. Do not create a new conversation merely because a socket closed.

Use messageId for persisted-message deduplication and clientMessageId for optimistic reconciliation. A duplicate WebSocket send can be suppressed without another echo; while active, use REST retry with the same ID or read the transcript to resolve uncertain delivery. If the session has closed, you can read its transcript before discarding a still-valid token. Closure alone does not prove an unresolved send was accepted; keep its text available to copy without automatically sending it into a new chat.

A capped snapshot is the newest 200 messages by default (up to 500), not a guarantee of the full history; retain already-known canonical messages during in-page recovery rather than deleting them when absent from a capped snapshot.

HTTP failures or socket closure alone do not prove a message was rejected. Preserve the visitor’s draft and explicit pending/failed state. Never silently retry with a new message ID. Discard transient AI deltas on recovery and replace them with canonical messages; do not persist partial streamed previews as transcript entries.

Inactivity is server-controlled. Current defaults close idle human chats after 30 minutes; AI chats receive an idle check-in after 5 minutes and close after another idle interval. Do not implement a client timer that claims the conversation has ended before the server does. Session status is active or closed; a closedByType of other may cover older or unattributed closures.

Client states to implement

  • Discovering → available / unavailable / recoverable discovery error.
  • Creating → active / explicit start error; suppress duplicate create requests.
  • Active → connecting / connected / reconnecting, with pending and confirmed messages.
  • Active → handoff / AI takeover / follow-up offer without opening a second session.
  • Closed or unusable credentials → ended state and an explicit new-chat action.
  • Storage unavailable → in-memory operation; no recovery promise after a document reload.

7. Errors

10

Handle HTTP status before relying on error wording.

Prefer error.message in the documented { error: { code, message, timestamp } } response. Some rejection paths return message or detail instead; intermediaries can return no JSON at all. Parse defensively, provide a safe fallback, and do not match human-readable text to drive session state.

The API does not promise a stable per-visitor session-creation rate allowance. Deployment throttles and service failures can occur. Treat 429 and temporary 5xx as recoverable for reads, use bounded backoff and Retry-After when supplied, and preserve creation/message idempotency rules when considering retries.

Status / event400
Meaning and responseInvalid context or payload: verify siteId, absolute page URL/domain, required fields, email, or link-card target. Correct input before retrying.
Status / event401 / 403
Meaning and responseMissing, expired, or unusable session authorization. Clear unusable saved credentials; never switch to rep credentials. Preserve unresolved text, explain that closure/delivery cannot be confirmed, and offer an explicit new conversation.
Status / event404
Meaning and responseThe requested resource is unavailable. A message send can return 404 after closure: read the session to recover its final status and transcript before resolving pending text. If the session read is also 404, abandon that saved session. For start, refresh discovery. For link-clicks, a missing card does not invalidate the conversation.
Status / event409
Meaning and responseCreation: routing/capacity/site eligibility changed. Message: reassignment/recovery conflict. Follow-up: closed session or expired offer. Recover current state; do not repeatedly submit the same invalid transition.
Status / event429 / 5xx / network failure
Meaning and responseShow a retryable service state and preserve draft/pending text. Back off; do not automatically replay an ambiguous create.
Status / eventWebSocket error or close
Meaning and responseRecover over authenticated HTTP; a transport close is not itself a session.closed event. No separate delivery acknowledgement or replay cursor is available.

8. Optional attribution

11

Report interactions that actually happened.

The following public POST endpoints accept JSON with Content-Type: application/json, or text/plain containing JSON for sendBeacon. They need no visitor token and return an empty successful response. Treat them as best-effort telemetry: do not wait for them before opening chat or sending a message. Only emit events for UI elements you actually rendered and interactions that occurred. Disabling telemetry means those custom UI interactions are absent from the corresponding Stand analytics.

Use the exact site/responder context returned by discovery. For AI activation and badge clicks, preserve standinProfileId and a null/omitted human repId. Greeting-variant events require a human repId and greetingVariantId; AI discovery returns both as null, so skip greeting-shown/open for an AI offer. Never invent or look up an owner rep ID to satisfy those endpoints. visitorId, when used, is the server-issued visitor participant ID; omit it before a session exists. Do not substitute visitorExternalId.

Endpoint/v1/events/activate
JSON body / emission ruleRequired: siteId. Optional: activationId, showId, page, repId or standinProfileId, responderType, sourceType, analyticsId, interaction, initialMessagePresent, behaviorRuleId, behaviorTriggerType, behaviorActionType. Optional fields may be null. sourceType: use unknown for a custom surface; interaction describes the action. Create one activationId per activation and reuse it in create; deduplicated for 24 hours.
Endpoint/v1/events/greeting-shown
JSON body / emission ruleRequired: siteId, repId, greetingVariantId. Optional: page, standinProfileId, visitorId. The variant must belong to that rep in the site’s organization. Emit once after the returned human greeting variant is actually visible, not for an AI or custom replacement greeting.
Endpoint/v1/events/greeting-open
JSON body / emission ruleSame fields as greeting-shown. Emit only for opening from that rendered greeting.
Endpoint/v1/events/badge-click
JSON body / emission rulesiteId, page, exactly one of repId / standinProfileId, visitorId?, repName?. Send for a real Stand attribution click; server derives the canonical name. No server idempotency promise.

9. Implementation brief

12

Verify the whole conversation before launch.

Test with your registered site and its configured human and AI responders. Compare the visitor experience with the actual conversation in Stand, then exercise the skills you have enabled. During beta, retain a way to return to the supplied widget if your client cannot handle a contract change.

Feature reference

Custom chat UI (Beta)

Review availability, prerequisites, supported behavior, and limitations before deciding what to replace.

Read the feature reference

Before publishing a custom client

  • Discovery succeeds on the registered domain/path and fails gracefully on a wrong domain, unavailable responder, or exhausted capacity.
  • One interaction creates exactly one session; initial text and opening greetings appear once in both the visitor transcript and Stand dashboard.
  • Human and AI text, link cards, and unknown/malformed events render safely. If enabled, streamed previews, typing indicators, and localized controls handle their transient events correctly.
  • AI-to-human handoff and unanswered-human recovery update identity correctly; an offered email form submits, handles a late rep reply, and reaches a closed state.
  • Explicit OpenAPI confirmation works only after visitor action when the configured Stand-in has that skill.
  • Reload, dropped socket, reconnect races, duplicate echoes, REST retries, expired tokens, and capped transcript recovery do not lose drafts or duplicate accepted messages. A terminal event during an unresolved send preserves text without claiming delivery.
  • Ending the chat stops reconnects, clears local credentials, and requires an explicit action to start a new conversation.
  • Tokens and query-string credentials are absent from your logs/analytics; unsafe links, raw HTML, internal prompts, and private metadata are not rendered.
  • Keyboard focus, screen-reader labels, new-message announcements, mobile sizing, loading/error states, and reduced-motion behavior work.
  • The configured notice and Stand attribution appear correctly, and optional interaction telemetry reflects actual actions.

Questions

Common reader notes

Do I need an API key?

No. Discovery and visitor session creation are public with site validation. Subsequent requests use the opaque visitor token issued for that conversation. Never use rep or admin credentials in the browser.

Can I try the visitor API before registering a site?

Yes. Use demo as the siteId. Stand Chat’s demo Stand-in answers from any page domain. Your own responders, instructions, knowledge, skills, and conversation history require your registered site ID.

Can I customize inline chat without rebuilding the client?

Yes. Use stand-chatbox for the complete supplied conversation inside your page, with CSS variables, shadow parts, slots, and capped or growing height. Use stand-button, stand-card, or the public JavaScript API for an invitation. Tune Chat Behavior documents these elements. Use this beta when you need to own the whole visitor interface and its lifecycle.

Does a custom UI bypass plan or branding limits?

No. The same server-side entitlements and quotas apply. Preserve returned notices and attribution, and implement UI behavior for the configured skills.

Are the reel examples working integrations?

Yes. The reel shows recorded previews of examples.stand.chat projects. Open a live example to interact and follow its source link to copy it. The fictional brands, page interactions, animations, and any sample questions belong to each example; adapt them for your own site and test with your configured responders.

Try the guide on one real page.