Operations
Your handle’s Allowed websites list controls custom apps. In your Ada dashboard, go to Channels > Chat and add your app’s origin. Until the list allows your origin, a configured appUrl is dropped with a console warning and Ada’s default app mounts. Local development needs no entry when both your app and the embedding page run on loopback hosts. See Local development.
client.operations provides typed helpers over the raw bridge event contract. Each helper carries the guards, debounces, and correlation logic that Ada’s own conversation app uses. Prefer these helpers over hand-built sendEvent calls. The raw events remain available as an escape hatch. See State and events for the underlying contract.
How correlation works
The bridge has no request/response channel. The core frame answers every action through display-state transitions. The operations layer hides this:
- Fire-and-forget helpers return
void. Core reports any outcome through state, or not at all. - Correlated helpers return a handle or a
Promise. A handle’ssettled(opts?)method returns aPromisethat settles from the state transition that answers the operation. settled({ timeoutMs })rejects with anErrorwhen the answer does not arrive in time. WithouttimeoutMs, an unanswered operation stays pending until you destroy the client.client.destroy()then rejects every pendingsettled()promise with aBridgeClientDestroyedError. Detect that rejection byerror.name === "BridgeClientDestroyedError".- Core publishes a failure signal when it drops a correlated action instead of running it. This covers a host-driven
resetin progress, a startup that cannot safely accept the action yet, a disabled transcript feature, and invalid submission arguments. Each affected handle settles with that failure rather than hanging. ApartialCSAT submit is the one exception: it reports no terminal outcome by contract, so its drop is silent too. On an older core document without these signals, passtimeoutMs.
Each helper below states whether it is fire-and-forget or correlated, and which state transition settles it.
A cached copy of the bridge runtime can predate the operations layer. Feature-detect before use: check client.operations before calling it, and check typeof bridge.messageKey === "function" before using a derivation helper.
Conversation
addReaction
addReaction(messageId: string, reaction: 1 | -1): void;
Reacts to an AI Agent message with a thumbs vote. Fire-and-forget.
cancelCapture
cancelCapture(label?: string): void;
Cancels the active capture field. Uses the block’s own cancel response and label; label overrides the visible label. No-op when no capture is active or the block has no cancel response. Fire-and-forget.
cancelListSelection
cancelListSelection(label?: string): void;
Cancels the active selectable list. Uses the block’s own cancel response and label; label overrides the visible label. No-op when no list selection is active or the block has no cancel response. Fire-and-forget.
canRetryMessage
canRetryMessage(message: Message): boolean;
Returns whether the retry affordance should show for a message. True when the message is a text message that core marked isRetryable, no disconnected Zendesk SDK blocks it, and the message belongs to the current conversation or predates conversation stamping. Pure read; sends nothing.
reportLinkClick
reportLinkClick(messageId: string, opts?: { navigateInHost?: boolean }): void;
Reports that the user activated a link message. Core resolves the stored message for tracking. With navigateInHost, core also performs validated same-tab navigation on the host page. Fire-and-forget.
retryMessage
retryMessage(messageId: string): void;
Retries a failed send by message id. Gate the affordance on canRetryMessage. Fire-and-forget.
selectOption
selectOption(option: OptionItem, messageId: string): void;
Answers an options block. The label falls back to the option id when the option has no label. Fire-and-forget.
selectQuickReply
selectQuickReply(reply: QuickReply, messageId: string): void;
Answers a quick-reply chip. Pass the chip object and its message id verbatim from state. Fire-and-forget.
Before you enable any interactive row (quick replies, options, retry), consult isHistoricalRow and chat.answeredInteractiveIds. Answering a row from a previous conversation sends its target into the current one.
sendMessage
sendMessage(body: string, opts?: { secret?: boolean }): SendHandle;
Sends a chat message. Correlated: the returned SendHandle carries the minted tempMessageUuid, and settled() resolves with the row in chat.messages whose clientKey equals it. The optimistic row appears immediately, and the durable server echo keeps the same clientKey across the id swap.
settled() resolves with the first matching row, which is the optimistic one, so the resolved row has no cursor yet. The cursor arrives on the durable echo. Drive markRead from your chat.messages subscription, never from the settled row: markRead(message.cursor ?? "") on a settled row passes an empty cursor, which the helper drops silently.
- The body is trimmed. An all-whitespace body sends nothing, and
settled()rejects. settled()rejects when core reports this send’s error before the row appears (rate limit, over-length body, session not ready, dispatch failure). Core stamps the failing send’s key onchat.error.clientKey, so the handle rejects only on its own failure. An unrelated error in the same window never rejects a delivered send. The rejection carries thechat.errortext, and a repeat of the identical error still rejects.secret: truesends a masked message. The raw body never enters the transcript, so no correlatable row exists andsettled()never resolves. PasstimeoutMsor skipsettled()for secret sends. Whileui.secretMessage.isOpenis true, send withsecret: trueto match the composer’s mode.
submitCapture
submitCapture(value: string): CaptureHandle;
Submits the active capture field. The dataId comes from conversation.stateData.capture. Each call mints its own submitId and sends it with the event. Correlated: settled() resolves with the verdict for this submit only. An accepted value removes the capture block and resolves { ok: true }. A server validation rejection resolves { ok: false, message } with the validation error, observed on the isSubmitting falling edge. When no capture field is active, settled() rejects immediately.
Core stamps every other failure on the capture’s lastSubmitFailure, keyed with the submit’s own submitId: no session to round-trip, a value over the 500-character cap, a transport failure, a duplicate submit while one is in flight, or a host-driven reset. The handle resolves { ok: false, message } from that stamp. The handle never reads the global chat.error, so an unrelated failure in the same window cannot masquerade as this capture’s verdict. A second submitCapture sent while one is in flight settles on its own drop; it does not inherit the first submit’s verdict. On a core document that predates lastSubmitFailure, these failures are not capture-attributable, so pass timeoutMs.
submitListSelection
submitListSelection(selectedIds: string[]): void;
Submits the active selectable list. The dataId comes from conversation.stateData.listSelection. No-op when no list selection is active. Fire-and-forget.
Read and unread
markRead
markRead(cursor: string, opts?: { conversationId?: string }): void;
Advances the persisted read watermark. Call it with message.cursor for each message that scrolls into view. Do not reimplement read tracking; this helper carries the contract the reference app validates:
- Reports are debounced 400 ms, so one scroll gesture produces one report.
- The watermark is a monotonic lexical maximum over cursor strings. Calling with an older cursor never lowers it.
- The accumulator and any pending report reset when
chat.conversationIdchanges. A cursor collected in conversation A can never commit against conversation B.
One rule stays with the caller: the transcript preserves the previous conversation’s rows, so exclude messages stamped with a different conversationId before reporting their cursors. See the worked example.
Fire-and-forget. The committed watermark comes back on chat.lastReadCursor.
History
loadOlderMessages
loadOlderMessages(): void;
Requests an older page of history. No-op unless chat.canLoadMore is true and no load is already in flight. Fire-and-forget; the page arrives in chat.messages.
onHistoryLoaded
onHistoryLoaded(callback: (seq: number) => void): () => void;
Invokes callback each time restored or baseline history loads or reloads, when chat.recentMessagesLoadSeq advances. This is the one-shot trigger for “scroll to the bottom of restored history” and for seeding an unread divider. The subscription re-arms silently when the sequence resets to 0 on a chat clear. Returns an unsubscribe function.
Surveys and End Chat
checkEndChatEligibility
checkEndChatEligibility(opts?: SettleOptions): Promise<EndChatEligibilityResult>;
Asks core whether End Chat should present a survey. Correlated: the helper latches csat.endChatEligibility.seq and resolves when it advances, because the contract has no request id. Resolves with { eligible, surveyTarget }. A check core drops (a host-driven reset in progress, or a startup that cannot accept it) resolves as ineligible. Pass timeoutMs to bound the wait.
resetApp
resetApp(): void;
Resets the app frame’s conversation state. Fire-and-forget.
skipCsatAndEndChat
skipCsatAndEndChat(): void;
Ends the chat without presenting a survey. Fire-and-forget.
startNewConversation
startNewConversation(opts?: { cooldownMs?: number }): boolean;
Starts a new conversation. Applies a local cooldown (default 5 seconds) that swallows rapid re-clicks. Returns false while cooling down, without sending anything.
submitCsat
submitCsat(data: CsatSubmitData, opts: { surveyType: string; partial?: boolean; conversationId?: string | null }): CsatSubmitHandle;
Submits a CSAT survey. Correlated: settled() resolves { ok: true } when csat.submitSuccess.seq advances with a matching surveyType and conversation, and { ok: false, message } on the mirror-image error triple. Error verdicts correlate on the csat.submitError.seq advance, so a repeat of the identical error text still settles. A non-partial submit core drops resolves { ok: false, message } the same way. This includes an invalid or non-numeric score and a submit with no resolvable conversation. The helper matches conversationId only when you passed one.
Pass the right conversation scope:
- For a survey rendered in the transcript, pass
conversationId: message.conversationId. A transcript row can outlive the conversation it rates, and matching on survey type alone would let a late answer from a dead conversation settle the wrong survey. - For End Chat and proactive flows, omit
conversationId. Core resolves the conversation itself. Passingnullis the same as omitting it, on the wire and in correlation.
A partial submit (an early score tap) reports no terminal outcome, so its settled() only settles through timeoutMs.
trackCsatShown
trackCsatShown(surveyType: string, conversationId?: string | null): void;
Reports that a survey was displayed, for analytics. Fire-and-forget.
Composer
notifyComposerChanged
notifyComposerChanged(opts?: { secret?: boolean }): void;
Reports a content-free composer-changed edge, which drives live-agent typing indicators. The event carries no payload by contract: composer text never crosses the frame boundary. The helper suppresses the event entirely in secret mode, when you pass secret: true or when ui.secretMessage.isOpen is true. Even a content-free typing edge signals that a secret is being typed. Fire-and-forget.
toggleSecretMessage
toggleSecretMessage(open: boolean): void;
Opens or closes the masked (secret) composer mode. Clear your local composer text on every transition. Fire-and-forget.
Files
startFileUpload
startFileUpload(file: File): FileUploadHandle;
Starts a live-agent file upload. The helper mints an uploadId and retains the exact { file, uploadId } pair, so the handle’s retry() replays exactly it. A fresh pair would orphan the original error state in core.
Progress and errors land on file.isUploading, file.transfer, and file.uploadError. Watch both flags: core can reject a selection and report file.uploadError without ever setting file.isUploading to true. The handle’s dismissError() clears a reported error.
Settings
downloadTranscript
downloadTranscript(opts?: SettleOptions): Promise<void>;
Downloads the transcript. Correlated: resolves or rejects when transcript.download.status reaches success or error with transcript.download.status.seq advanced past the call’s baseline. A request core drops without ever broadcasting pending (a host-driven reset in progress, or an AI Agent without transcript downloads enabled) still rejects. Check config.downloadTranscriptEnabled before you show the affordance. On a core document without the seq key, the helper falls back to the pending to terminal edge. The status key is global, so treat the request as single-flight. A call made while one is in flight joins the in-flight request and settles on its verdict. Pass timeoutMs to bound the wait.
emailTranscript
emailTranscript(email: string, opts?: SettleOptions): Promise<void>;
Emails the transcript. Correlated on transcript.email.status and its seq, with the same settle contract as downloadTranscript. An AI Agent without email transcripts enabled rejects every request; check config.emailTranscriptEnabled before you show the affordance. The helper trims the email and sends the trimmed value. An empty result rejects immediately without sending, because core drops an empty email silently. The single-flight rule is address-aware. A call for the SAME address as the in-flight request joins it and settles on its verdict. A call for a different address cannot join. Core rejects only that call, matched on the address it sent. The in-flight call is not affected: its request completes, writes the final status, and settles its own handle with the true outcome. A conflict rejection never means the in-flight email was not sent. On an older core document without the conflict signal, the conflicting call settles on the in-flight verdict instead.
requestNotificationPermission
requestNotificationPermission(): void;
Asks the host page to request Web Notification permission. Permission is per-origin, so your frame’s own permission value is not the governing one. Fire-and-forget. The result arrives on this state key (the prefix is legacy naming):
setAlertSound
setAlertSound(enabled: boolean): void;
Toggles the new-message alert sound. Fire-and-forget.
setLanguage
setLanguage(language: string): void;
Changes the conversation language. Pass a BCP 47 tag the AI Agent offers. The offered set is config.clientDefaultLanguage plus config.translatedLanguages; neither list alone is complete. Fire-and-forget.
setTextSize
setTextSize(size: "small" | "default" | "large"): void;
Sets the user’s in-widget text-size override. The persisted value comes back on appearance.userTextSize. Fire-and-forget.
setTheme
setTheme(theme: "light" | "dark" | "auto"): void;
Sets the user’s in-widget theme override. The persisted value comes back on appearance.userTheme. Fire-and-forget.
Chrome
close
close(): void;
Activates the header close affordance. Ends the chat when endChat.canEnd is true. Fire-and-forget.
dismissError
dismissError(): void;
Clears chat.error in core. Always clear the error in core rather than hiding it locally: chat.error keeps its value when the session’s second identical error arrives (only chat.error.seq advances), so a locally hidden error would render nothing the second time. Fire-and-forget.
dismissToast
dismissToast(): void;
Dismisses the visible toast. Fire-and-forget.
minimize
minimize(): void;
Minimizes the widget. Fire-and-forget.
reportAppError
reportAppError(error: unknown, componentStack?: string): void;
Reports a fatal app error to core, which renders the fallback UI and records telemetry. Callable from anywhere with a client reference, including a class error boundary outside any React provider. Fire-and-forget.
retryAfterOutage
retryAfterOutage(): void;
Retries after a response outage banner. Fire-and-forget.
setHostViewportExpanded
setHostViewportExpanded(expanded: boolean): void;
Asks the SDK host to expand or restore the app’s viewport surface. Fire-and-forget.
Observe state changes
Two primitives on the client complement the full subscribe(callback) subscription. Both return an unsubscribe function. client.destroy() releases every subscription and rejects every pending settled() promise with a BridgeClientDestroyedError.
subscribeKey(key, callback, opts?) fires only when one key’s value changes:
select(selector, callback, opts?) does the same for a derived projection:
Options for both:
Object.is is a sound default for subscribeKey. State crosses the frame boundary by structured clone, and the client restores each structurally unchanged key’s previous reference before it notifies. An object key’s identity therefore changes exactly when its content does. For chat.messages, that is every streaming delta. For select, a selector that builds a new object per call defeats Object.is: pass a custom equals, or select a primitive signature (for example, a joined string of ids). See State and events.
In React, useBridgeStateKey(key) delegates to subscribeKey and re-renders only when its key’s value changes.
Derivation helpers
The loaded module exports pure helpers ported from Ada’s reference app. They take state or messages and return values. No client is needed:
The derivation helpers are runtime-only. The npm package declares their types but ships no implementation, and createMockBridgeClient does not include them. In unit tests, inject the helpers into the code under test as plain function parameters or props. To exercise the real implementations, load the module with loadMessagingBridge() in an integration test. Do not re-implement them: a copy drifts from the runtime’s edge cases.
Use messageKey as your render key and as your unread-boundary anchor. A raw message id changes twice: a streaming bubble swaps to its final id, and an optimistic send swaps to its durable id. Keying on the raw id replays animations, double-fires arrival effects, and breaks unread anchors.
Worked example: an unread badge
A headless integration that shows an unread count (a “red dot”) and clears it when the user opens the panel.
One trap sits in the first-contact case, which is exactly when a proactive badge matters most. The watermark starts empty for a user who has never had a read reported, and selectUnread yields count: 0 for an empty watermark. Without seeding, a proactive or agent message that arrives before the user ever opens the panel shows no badge, silently, until the first markRead commits. Seed it yourself: while chat.lastReadCursor is empty, count every non-user row as unread.
The badge count falls to zero on its own: markRead commits the debounced watermark, core advances chat.lastReadCursor, and the select subscription re-derives the count. To also render an unread divider, pass your filtered transcript as messages, anchor the divider on selectUnread(state, { messages }).firstUnreadId, and keep it stable with the boundaryId option.
Types
Handle and option types ship in the npm package. Import them from @ada-cx/messaging-bridge.
BridgeClientDestroyedError
The rejection every pending settled() promise receives when client.destroy() runs. The npm package exports the type only. Detect it by error.name === "BridgeClientDestroyedError", never by instanceof: the class value lives in the CDN runtime.
CaptureHandle
{ settled(opts?: SettleOptions): Promise<OperationResult> }
Returned by submitCapture.
CsatSubmitHandle
{ settled(opts?: SettleOptions): Promise<OperationResult> }
Returned by submitCsat.
EndChatEligibilityResult
Resolved by checkEndChatEligibility. surveyTarget names which survey End Chat should present.
FileUploadHandle
{ uploadId: string; retry(): void; dismissError(): void }
Returned by startFileUpload.
ObserveOptions
{ equals?: (a: T, b: T) => boolean; emitInitial?: boolean }
Options for subscribeKey and select.
OperationResult
{ ok: true } | { ok: false; message: string }
The outcome of a correlated submit.
SendHandle
{ tempMessageUuid: string; settled(opts?: SettleOptions): Promise<Message> }
Returned by sendMessage. tempMessageUuid is the uuid put on the wire; the sent message is findable in chat.messages by clientKey === tempMessageUuid.
SettleOptions
{ timeoutMs?: number }
Accepted by every settled() method, by checkEndChatEligibility, and by the transcript helpers. When set, the promise rejects with an Error after timeoutMs milliseconds without an answer.