- Guides
- Build your own chat widget
Build your own chat widget
Replace Espejo's chat widget with a support chat UI of your own. Use createChatClient on top of our backend, bring your own model with espejo.agent, or speak the HTTP protocol directly.
Our chat widget is one way to put the support agent on your site. When it does not fit your design, your framework or your product, you can build the chat yourself and still use as much of Espejo as you want. This guide covers three levels, from the least work to the most.
Three ways to build it
Section titled “Three ways to build it”| What you do | What Espejo does | |
|---|---|---|
1. Your UI on our backend (createChatClient, recommended) | The interface: bubbles, input, confirmation, errors. | The provider and its key, the tool loop, the actions on the page, guides, tickets, the daily cap and the conversation. |
2. Your UI and your own model (window.espejo.agent) | The interface, the model, its key and the tool loop, on your server. | The page context and the actions on the page. |
| 3. The HTTP protocol directly | The interface, the stream reader and the action loop. | The same backend as level 1. |
Level 1 is what our widget runs on. You get the same behavior (retries, the queue, confirmation, identity) and only draw the interface. Most of this guide is about it.
Level 2 does not use Espejo’s chat backend at all: your code reads the page
with getContext(), offers the tools with tools() to your model and runs
what it asks for with act(), with a policy and a confirmation handler. It is
documented in full in
Path 1: your own chat and your own agent.
Level 3 is for when you cannot or do not want to load our script for the
chat. You still need espejo.agent in the page to run the actions. See
The HTTP protocol.
Levels 1 and 3 need the support agent configured and on in the console (the Support agent tab), exactly like the widget.
Load the client
Section titled “Load the client”Load espejo.chat.js without data-key. Without a key it mounts nothing:
no button, no panel. It leaves window.EspejoChat with createChatClient,
installPresence, installChatWidget and identify, plus chat, the
mounted widget, which stays null without a key:
<script src="https://app.espejo.dev/sdk/espejo.chat.js"></script><script> const { createChatClient } = window.EspejoChat;</script>Load it without async or defer if an inline script right after it uses
window.EspejoChat, or wait for the load event. The bundle brings its own
copy of the espejo.agent layer, so it runs the actions on its own. It works
with or without espejo.js: when the recorder is on the page, the agent also
sends the visitor’s recent actions and errors.
EspejoChat.identify belongs to the mounted widget. With your own UI, set the
identity on your client instead (see Identity and tickets).
A minimal chat
Section titled “A minimal chat”A complete chat in plain HTML and JavaScript: it streams the answer, shows each action while it runs, asks before acting and shows errors.
<section id="support"> <ol id="log"></ol> <form id="form"> <input id="input" autocomplete="off" placeholder="Ask anything" /> <button>Send</button> </form></section>
<script src="https://app.espejo.dev/sdk/espejo.chat.js"></script><script> const log = document.getElementById("log"); const rows = new Map(); let bubble = null;
// Always textContent: the answer is text, never HTML. function line(kind, text) { const li = document.createElement("li"); li.className = kind; li.textContent = text; log.append(li); li.scrollIntoView({ block: "end" }); return li; }
function errorText({ code, retryAfter }) { if (code === "rate_limited") return `Too many messages. Try again in ${retryAfter ?? 60} seconds.`; if (code === "daily_cap") return "The assistant reached today's limit. Try again tomorrow."; if (code === "no_agent") return "Support chat is not available on this site."; if (code === "network") return "The connection dropped. Send your message again."; return "Something went wrong. Try again."; }
const chat = window.EspejoChat.createChatClient({ key: "pk_live_...", // Click, fill and navigate wait for this answer. Use your own dialog. onConfirm: ({ label }) => window.confirm(`Let the assistant ${label}?`), onEvent(event) { switch (event.type) { case "text": bubble ??= line("assistant", ""); bubble.textContent += event.delta; break; case "tool_start": bubble = null; // text after an action goes in a new bubble rows.set(event.call.id, line("action", `${event.label}...`)); break; case "tool": { const row = rows.get(event.call.id) ?? line("action", ""); row.textContent = event.result.ok ? `${event.label}: done` : `${event.label}: ${event.result.error}`; break; } case "done": bubble = null; break; case "error": bubble = null; line("error", errorText(event)); break; } }, });
document.getElementById("form").addEventListener("submit", (e) => { e.preventDefault(); const input = document.getElementById("input"); const text = input.value.trim(); if (!text) return; line("visitor", text); input.value = ""; chat.send(text); // never rejects });</script>label is a short description in English (click "Save",
navigate to https://example.com/billing). To write your own text, in any
language, use target, value and destination, which come in the
confirmation request and in the tool_start and tool events.
createChatClient in depth
Section titled “createChatClient in depth”Options
Section titled “Options”| Option | Default | Meaning |
|---|---|---|
key | none | The project’s public key (pk_...). Required. |
baseUrl | https://app.espejo.dev | Base URL of the API. |
onConfirm | none | Called before click, fill and navigate with { action, label, target?, value?, destination? }. Return true (or a promise of true) to run the action. Without it, those actions come back as confirmation_required and do not run. |
onEvent | none | Receives every event below. An exception thrown in it does not stop the chat. |
agent | one of its own | An agent you already built, for example window.espejo.agent. Its policy is left as is. Without it, the client builds one that allows the five actions: the console decides which ones the model is offered. |
allow | the five actions | Narrows the actions of the agent the client builds. Ignored with agent. |
storage | sessionStorage | Where the conversation id is kept. null keeps nothing, so every page load starts a new conversation. |
identity | none | { email, hash }, the visitor’s verified identity. |
Methods and properties
Section titled “Methods and properties”| Member | What it does |
|---|---|
send(message) | Sends a message right away, cutting short the turn in progress, and pauses the queue. Resolves when the client is free again. Never rejects. |
enqueue(message) | Sends right away when the client is free and the queue is not paused. Otherwise adds the message to the queue. Returns false when the message is blank or 5 are already waiting. |
removeQueued(id) | Drops a message from the queue. |
editQueued(id, message) | Replaces the text of a queued message. A blank text drops it. |
sendQueuedNow(id) | Cuts the current turn short and sends that queued message first. The rest of the queue follows. |
clearQueue() | Empties the queue. |
resumeQueue() | Takes the queue out of pause. If the client is free, it sends the first message. |
reset() | Cuts the current turn short, empties the queue and starts a new conversation. |
abort() | Cuts the current turn short, keeps the conversation and pauses the queue. |
identify(identity) | Changes the identity, or removes it with null. With another person it does a reset(). |
destroy() | Cuts the current turn short, empties the queue and detaches from the agent. |
getConfig() | The console settings for the widget. See below. |
conversationId | The id of the current conversation. |
busy | true while a turn is running. |
queue | The waiting messages, in order, as { id, text }[]. |
paused | true while the queue is paused. Always false when it is empty. |
The queue moves only when a turn ends with done, and pauses after an
error, after abort() and after a direct send(). It lives in memory: a
reload drops it. The full rules are in
createChatClient: our backend, your UI.
Events
Section titled “Events”type | Fields | When |
|---|---|---|
text | delta | A piece of the answer. Append it to the current bubble. |
tool_start | call, label, target?, value?, destination? | An action is about to run, before the confirmation. |
tool | call, label, target?, value?, destination?, result | The same action after it ran or was declined. Match it with tool_start by call.id. result is { ok, error?, snapshot? }, where snapshot is the page after the action. |
done | conversationId | The assistant finished answering the message. |
error | code, message, retryAfter? | The turn stopped. retryAfter (seconds) only comes with rate_limited. |
queue | queue, paused | The queue changed. |
dequeued | id, message | A queued message is going out. Show it in the conversation here. |
call is { id, name, input }, where name is highlight, scroll_to,
click, fill or navigate. result.error is a short code such as
declined, confirmation_required, stale_ref, not_allowed or
cross_origin.
Each turn that runs to the end emits exactly one done or one error. A
blank message is not sent, and a turn cut short by abort(), reset(),
destroy(), a newer send() or sendQueuedNow() emits nothing more, not
even the tool event of an action that was waiting for confirmation.
What the client does for you
Section titled “What the client does for you”- It trims the message and cuts it at 4,000 characters, the API’s limit. A blank message is not sent.
- It reads the page before each request and trims the context to the API’s limits.
- It runs the actions. When a turn ends with pending actions, it runs
each one in the page (asking through
onConfirmwhen needed) and sends the results back, until the assistant stops asking. It stops after 25 rounds in one message withiteration_limit. - It retries once on its own when the conversation expired
(
conversation_expired) or belongs to another identity (identity_changed): it starts a new conversation and sends the message again. When the conversation is busy with another request (conversation_busy, for example from another tab), it waits 1.5 seconds and retries once in the same conversation. Only if the retry fails too do you get theerrorevent. - It keeps the conversation id in
sessionStorage, so a reload in the same tab continues the same conversation. Only the id is kept, never the messages: after a reload your UI starts empty while the assistant still remembers. If you prefer a clean start, callreset()on load or passstorage: null. - It attaches the last recording. When
espejo.jsis on the page and the visitor made a report, the request carries itssession_id, so a ticket the assistant opens can link the replay.
Error codes
Section titled “Error codes”code | Cause | What to show |
|---|---|---|
no_agent | The project has no agent on (404). | Hide your chat, or say it is not available. |
forbidden | Invalid or revoked key, origin not allowed, or account deactivated (401 or 403). | A generic error. Check the key and the allowed origins. |
bad_request | The API rejected the request (400, 413 or any other 4xx not listed here). | A generic error. |
rate_limited | More than 30 requests per minute from one IP to the project (429). | Ask to wait retryAfter seconds. |
daily_cap | The project reached its messages per day (429). | Say the assistant is back tomorrow (midnight UTC). |
conversation_limit | The conversation is too long (409). | Offer a new conversation and call reset(). |
conversation_expired | The retry with a new conversation also failed (409). | Offer a new conversation. |
conversation_busy | Another request was still answering after the retry (409). | Ask to try again in a moment. |
identity_changed | The retry with a new conversation also failed (409). | Offer a new conversation. |
conflict | Any other 409. | Call reset() and try again. |
unavailable | The agent is misconfigured or the service is down (5xx). | Try again later. |
network | The request failed or the stream closed before the end. | Ask to send the message again. |
provider_auth, provider_rate_limited, provider_error, timeout, iteration_limit, internal | The turn failed after the answer started. See Stream errors. | A generic error. iteration_limit: ask to narrow the request. |
message is readable English text from the API or the client. It never
includes the provider’s response.
Follow the console settings: getConfig()
Section titled “Follow the console settings: getConfig()”The owner sets the assistant’s name, icon, theme and sections in the
console’s Widget tab. getConfig() reads them (GET /v1/agent/config) so
your UI can follow them too:
const config = await chat.getConfig();// { enabled, allowed_actions, theme, name, icon, sections }enabled: whether the assistant answers. Withfalse, do not show the chat.allowed_actions: the actions the owner allowed, amonghighlight,scroll_to,click,fillandnavigate.theme:auto,lightordark.automeans “adapt to the page”.name: the assistant’s name (up to 40 characters), ornullfor your own default. Always insert it as text.icon:chat,sparkles,headset,bot,help, thehttps://URL of an image, ornullfor the default.sections: the sections to show, in the owner’s order, amonghome,support(the chat),ticketsandguides.supportis only there whileenabledistrue.ticketsneeds a visitor with a verified identity too: hide it when you have none.
It never rejects. When the project has nothing to show it resolves to
{ enabled: false, allowed_actions: [], theme: "auto", name: null, icon: null, sections: [] },
and to null when it could not be read (network, origin not allowed, server
down). The browser can keep the answer for 60 seconds and the API for 30,
so a change in the console can take up to about 90 seconds to reach your UI.
Show the visitor what the assistant does
Section titled “Show the visitor what the assistant does”Our widget shows a notice with a Stop button, a colored border and a
cursor while the assistant acts. You get the same with installPresence. It
needs the agent that runs the actions, so load espejo.agent.js as well and
give that agent to the client:
<script src="https://app.espejo.dev/sdk/espejo.agent.js"></script><script src="https://app.espejo.dev/sdk/espejo.chat.js"></script><script> const agent = window.espejo.agent; // Its default policy only allows highlight and scroll_to. agent.policy({ allow: ["highlight", "scroll_to", "click", "fill", "navigate"] });
const chat = window.EspejoChat.createChatClient({ key: "pk_live_...", agent, onConfirm: ({ label }) => window.confirm(`Let the assistant ${label}?`), onEvent(event) { if (event.type === "done" || event.type === "error") presence?.end(); // ...render the rest as in the minimal chat }, });
const presence = window.EspejoChat.installPresence(agent, { name: "Ana", onStop: () => chat.abort(), });
function sendMessage(text) { presence?.start(); chat.send(text); }</script>Call start() at the beginning of each turn: after Stop, every action
resolves as declined until you do. The options (lang, text, name,
theme, onStop, idleMs) are in
Show what the agent is doing.
Identity and tickets
Section titled “Identity and tickets”With the visitor’s verified identity, the assistant can look up and open
that person’s tickets, and your UI can list them. Your server computes the
hash as hex(HMAC-SHA256(identity secret, email)), with the email trimmed and
in lowercase. How to get the secret and sign on your server, with Node,
Python and PHP, is in
Verified identity for tickets.
The secret never goes to the browser.
If you already know who the visitor is when the page loads, pass the
identity in identity when you create the client. That way a reload keeps
the conversation saved for that person:
const chat = window.EspejoChat.createChatClient({ key: "pk_live_...",});Use identify() for sign in and sign out during the session. It is not the
same as passing identity at creation: a call that changes the identity
starts a new conversation, so calling it right after creating the client
drops the conversation saved for that person.
// After sign in: the hash comes from your backend.// After sign out.chat.identify(null);- Without an identity, or with one that does not verify, the chat works the same, without tickets. An email or hash with anything that is not ASCII counts as no identity.
- Changing to another person, or to
null, starts a new conversation. The same email and hash again change nothing. - The identity lives in memory only. Set it again on every page load.
- Each request carries it in the
x-espejo-user-emailandx-espejo-user-hashheaders.
Guides and tickets in your UI
Section titled “Guides and tickets in your UI”The client does not include a reader for guides or tickets. Your UI calls the same public endpoints our widget uses, from the browser, with the public key.
Guides
Section titled “Guides”GET /v1/guides, GET /v1/guides/search?q= and GET /v1/guides/{id}, with
the x-espejo-key header. The shapes, the search syntax, the errors and the
cache are in Help guides: In the widget.
Two things to keep in mind when you render them yourself:
renderin the list says how the owner wants guides opened:nativeinside your UI (rendermarkdown), orlinkat the guide’s ownurlin a new tab.markdownis your owner’s content, but render it as untrusted: our widget only activateshttps://links and images and shows any HTML as text.
Tickets
Section titled “Tickets”GET /v1/tickets?status=all|open|resolved (without status, all), with
the x-espejo-key header and the two identity headers. Here the key only
goes in the header, never in ?key=.
const res = await fetch("https://app.espejo.dev/v1/tickets?status=open", { headers: { "x-espejo-key": "pk_live_...", "x-espejo-user-hash": hash, }, credentials: "omit",});const { tickets, waiting } = await res.json();The shape of each ticket, what status and waiting mean, the limits and
the errors are in
Tickets in the widget and in the agent
and HTTP errors of GET /v1/tickets.
A 401 means the email and hash do not verify: hide your tickets view until
you get another identity. Show only the Tickets view when sections
includes tickets.
The HTTP protocol
Section titled “The HTTP protocol”This is the contract createChatClient speaks. Use it when you want to write
the client yourself. You still need window.espejo.agent in the page to read
it and run the actions (load espejo.agent.js).
The gate: key and origin
Section titled “The gate: key and origin”Every public endpoint (/v1/agent/config, /v1/agent/chat, /v1/guides,
/v1/tickets) checks the same things first:
- The
x-espejo-keyheader with the public key. Without it the answer is403Missing x-espejo-key header.With an unknown or revoked key, it is403Invalid ingest key. - The account must be active. Otherwise,
403This account is deactivated. Recording is off. - The
Originmust be allowed. A project with no allowed origins accepts any. With a list, a request from another origin, or with noOriginat all, gets403Origin <origin> is not allowed for this project., orOrigin (none) is not allowed for this project.without the header. Browsers always sendOriginon these requests. A server usually does not, which is why these endpoints are meant to be called from the visitor’s browser.
Send the requests without cookies (credentials: "omit").
GET /v1/agent/config
Section titled “GET /v1/agent/config”Headers: x-espejo-key. You can also add the key as ?key=, next to the
header and never instead of it: the browser cache keys on the URL, so two
projects on the same site do not share an entry. When both are sent they
must match, or the answer is 403.
{ "enabled": true, "allowed_actions": ["highlight", "scroll_to", "click"], "theme": "auto", "name": "Ana", "icon": "sparkles", "sticky_container_id": "main-content", "sections": ["home", "support", "guides"]}If you omit sticky_container_id, the SDK looks for <main> and common roots such as #__next or #root; for a generic root containing <main>, it uses that main content. An explicit ID is honored as written, including root. Docking temporarily reduces that element width and places the fixed widget beside it as a sibling, without wrapping or moving app nodes; undocking restores its width. Choose an element that can safely shrink; without a suitable element, the widget stays floating. When a modal dialog (aria-modal="true") opens, the panel moves behind it and reappears when it closes, keeping its position and mode. Manually minimizing it also keeps the dock attachment.
The fields are the ones described in
getConfig(). It answers 404 when
the project has nothing to show (no agent on, and no tickets or guides). A
200 carries Cache-Control: private, max-age=60, and any other answer carries
no-store. At most 120 requests per minute from one IP to one project.
POST /v1/agent/chat
Section titled “POST /v1/agent/chat”Headers:
| Header | Value |
|---|---|
content-type | application/json |
x-espejo-key | The public key. |
accept | text/event-stream |
x-espejo-user-email, x-espejo-user-hash | Optional. The verified identity. |
Body:
{ "conversation_id": "0f8b1c1e-5a0e-4c52-9d0b-7f1e8f3a2b64", "message": "Where do I change my plan?", "context": { "page": "espejo-snapshot/1 ...", "url": "https://example.com/settings", "recent": ["click button \"Billing\""], "errors": [] }, "session_id": "3f2a9c0d4b6e8f1a2c3d4e5f6a7b8c9d"}| Field | Rules |
|---|---|
conversation_id | A UUID your client generates (crypto.randomUUID()) and reuses for the whole conversation. Required. |
message | What the visitor wrote. Not blank, up to 4,000 characters. |
tool_results | The results of the pending actions. See The action loop. |
context | What the visitor sees now: page (the snapshot), url, recent and errors, from agent.getContext(). Send it with every request, results included. |
session_id | Optional. The session_id of the last report espejo.js made in this page (window.espejo.lastReportId, 32 hex characters). Anything else is ignored. |
Send exactly one of message or tool_results. Both, or neither, is a
400. The context is cut to the limits in
AGENT_CHAT_LIMITS, and the
whole body cannot exceed 769,528 bytes (413).
The stream
Section titled “The stream”Errors that can be decided before answering (key, origin, agent off, rate
limits, invalid body, conversation state) arrive as a normal HTTP status
with a JSON body. Otherwise the answer is 200 with
content-type: text/event-stream, and each event is:
event: <name>data: <json>| Event | data | Meaning |
|---|---|---|
text | { "delta": "..." } | A piece of the answer. There can be many. |
tool_call | { "id": "...", "name": "click", "input": { "ref": "e12" } } | An action for the page. There can be several in one turn. |
done | { "conversation_id": "...", "awaiting_tools": true } | The turn ended. With awaiting_tools: true, the server waits for the results. |
error | { "code": "timeout", "message": "..." } | The turn failed. The codes are in Stream errors. |
The stream ends with exactly one done or one error. A stream that closes
without either is a dropped connection: send the message again.
The action loop
Section titled “The action loop”The five page actions (highlight, scroll_to, click, fill,
navigate) run in the browser. Only the ones the owner allowed in the
console ever reach it. Everything else (reading the page again, searching
the guides, looking up and opening tickets) is resolved on Espejo’s server
and never arrives as a tool_call.
When a turn ends with awaiting_tools: true:
- Run every
tool_callof that turn, in order, with the same agent that produced thecontext:agent.act({ ...call.input, type: call.name }). Therefvalues point at that agent’s snapshot. - Send another
POSTin the same conversation withtool_resultsinstead ofmessage, and a freshcontext. One result per pending action, with its exactid:{ "id": "...", "ok": true }, pluserror(up to 1,000 characters) andsnapshot(up to 24,000) whenact()returned them. At most 8 results per request. - Read the new stream. Repeat while it ends with
awaiting_tools: true.
A missing result, a repeated id or an id that was not requested is a
400. Results when nothing is pending are a 409. If the visitor writes a
new message instead, the pending actions are closed as not executed and the
conversation goes on from the new message.
window.espejo.agent allows only highlight and scroll_to by default, and
asks before click, fill and navigate. Widen its policy with
agent.policy({ allow: config.allowed_actions }) and answer the confirmation
with agent.on("action_confirm", handler). Without a handler those actions
return confirmation_required: send that back as the result. See
The policy.
The conversation lives on Espejo
Section titled “The conversation lives on Espejo”Do not send the history: Espejo keeps it by conversation_id. A
conversation expires 24 hours after its last request. A new message in an
expired conversation starts it over, empty, with the same id. tool_results
in an expired one gets 409 conversation_expired. After 60 requests (results
included) or 256 KB stored, the conversation answers 409
conversation_limit: start a new one with a new UUID. Only one request at a
time per conversation: a second one gets 409 conversation_busy.
The first request with a valid identity ties it to the conversation. Later
requests with another identity, or with none, get 409 identity_changed.
Example: read the stream with fetch
Section titled “Example: read the stream with fetch”EventSource only does GET, so read the POST answer with fetch and a
stream reader:
const API = "https://app.espejo.dev";const KEY = "pk_live_...";const agent = window.espejo.agent; // from espejo.agent.jslet conversationId = crypto.randomUUID();
function context() { const c = agent.getContext(); return { page: c.page.slice(0, 24000), url: c.url.slice(0, 2048), recent: c.recent.slice(-30).map((l) => l.slice(0, 500)), errors: c.errors.slice(-20).map((l) => l.slice(0, 500)), };}
async function post(body, onText) { const res = await fetch(`${API}/v1/agent/chat`, { method: "POST", headers: { "content-type": "application/json", "x-espejo-key": KEY, accept: "text/event-stream" }, credentials: "omit", body: JSON.stringify(body), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw Object.assign(new Error(err.message ?? `HTTP ${res.status}`), { status: res.status, code: err.code }); }
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader(); const calls = []; let end = null; let buffer = ""; for (;;) { const { done, value } = await reader.read(); if (done) break; buffer += value; let cut; while ((cut = buffer.indexOf("\n\n")) >= 0) { const block = buffer.slice(0, cut); buffer = buffer.slice(cut + 2); let name = "message"; const data = []; for (const line of block.split("\n")) { if (line.startsWith("event:")) name = line.slice(6).trim(); else if (line.startsWith("data:")) data.push(line.slice(5).trimStart()); } if (!data.length) continue; const payload = JSON.parse(data.join("\n")); if (name === "text") onText(payload.delta); else if (name === "tool_call") calls.push(payload); else if (name === "done") end = payload; else if (name === "error") throw Object.assign(new Error(payload.message), { code: payload.code }); } } if (!end) throw new Error("The connection closed before the answer finished."); return { calls, awaiting: end.awaiting_tools };}
async function ask(message, onText) { let body = { conversation_id: conversationId, message, context: context() }; for (let round = 0; round < 25; round++) { const { calls, awaiting } = await post(body, onText); if (!awaiting || !calls.length) return; const tool_results = []; for (const call of calls) { const r = await agent.act({ ...call.input, type: call.name }); tool_results.push({ id: call.id, ok: r.ok, ...(r.error ? { error: r.error.slice(0, 1000) } : {}), ...(r.snapshot ? { snapshot: r.snapshot.slice(0, 24000) } : {}), }); } body = { conversation_id: conversationId, tool_results, context: context() }; }}A real client also handles the 409 codes (a new conversationId for
conversation_expired, conversation_limit and identity_changed, a short
wait for conversation_busy) and retry_after on a 429. That is what
createChatClient already does.
Errors and limits for your own client
Section titled “Errors and limits for your own client”The full tables are in Errors and limits. What matters most when you write the client:
- HTTP errors come before the stream, as JSON with
messageand, for some,code:409withconversation_expired,conversation_limit,conversation_busyoridentity_changed, and429withdaily_cap. A429withoutcodeis the rate limit and carriesretry_afterin seconds. - Rate limits: 30 chat requests per minute from one IP to one project
(requests with
tool_resultscount too), and 120 per minute forGET /v1/agent/config. - Daily cap: only requests with
messagecount toward the project’s messages per day. - One stream per request: there is no way to resume a stream that dropped. Send the turn again.
What isn’t available yet
Section titled “What isn’t available yet”- Reading a conversation’s history. There is no endpoint that returns the messages of a conversation. If you want your UI to show them after a reload, keep them on your side.
- Resuming a stream. A stream that drops cannot be picked up where it stopped. Send the message again.
- Tickets outside the assistant. There is no endpoint for the visitor to open or answer a ticket from your UI. The visitor can ask the assistant to open one, when the owner allowed it.
- Calling the chat from a server. When the project restricts its allowed
origins, a request without an allowed
Originis refused. The chat is meant to run in the visitor’s browser. - Server side rendering. The client and the agent need a browser: they read the page and run actions on it. Create the client on the client side, after the page loads.