Web SDK
How the copilot behaves on a website — the tag, version pinning, signed-in visitors, allowed domains, dark mode, CSP, actions, screens and events.
The web SDK is one JavaScript file with no dependencies. It does not care what your site is built with, and it speaks the same protocol as the Flutter SDK: the same components, the same actions from the console, the same persona. One app in the console can serve your mobile app and your website with the same key.
Set up with AI adds it: your coding agent puts the tag on every page,
wires sign-in, registers screens and fills in the console. This page is what
that leaves in your site and how the SDK behaves, for when you change it later.
The live key works once the app is live, which the setup does by itself when its
checks pass; until then it is refused
with app_not_live and the site runs on the test key.
To change any of it, start from a task: each opens with the prompt for your coding agent. This page is what that prompt relies on.
The tag
<!-- Inside <head>, on every page. Swap in the live key when the site goes live.
data-app-key: your app's key — Rendel console → Settings → Keys & environments → Copy.
data-languages: "tr" Turkish only, "tr,en" Turkish or English,
"en" English only. Leave it out to answer in whatever the visitor writes. -->
<script
src="https://rendel.ai/sdk/web/0.23.4/rendel.js"
data-app-key="rd_pk_test_…"
data-languages="tr"
></script>It sits inside <head> on every page; in Next.js it is a <Script> with
strategy="beforeInteractive" in the root layout. The tag also reads
data-locale and data-api-base-url, and data-launcher="false" hides the
corner button that opens the copilot.
Pinning
The version is in the address. 0.23.4 stays 0.23.4 after the next release
ships, so your site changes only when you change the number. The
changelog says what each version brings.
Who is signed in
// Right after your sign-in succeeds:
Rendel.identify({ userId: user.id });
// On every page: the signed-in person's current token, or null, so the
// copilot can reach your API as them — no API key.
Rendel.setUserToken(() => getAccessToken());
// When the user signs out:
Rendel.reset();setUserToken is how the copilot answers from your own API — "my order", "my
routine" — as the person asking, with no API key to issue: in the console,
those endpoints are set to the signed-in user's token. The function is asked
before every question, so it returns the current token (refreshed there if it
has run out), or null when nobody is signed in. It goes on to your endpoints
as Authorization: Bearer, is never stored, and each person reaches only what
their own token opens. It is called on every page, since a function cannot be
remembered across loads.
An endpoint that changes something — "book me in on Tuesday" — is asked on a confirmation card first. Once the person confirms, Rendel makes the call as them and answers with what happened; there is no handler to write on the page.
userId is the id your own backend knows the person by — the same one your
mobile app passes, so a person has one history across both. The SDK remembers
them across page loads and visits, so a returning visitor needs no second call.
traits ({ plan: "pro" }) carries anything the copilot should know about
them, and userHmac is needed when
identity verification is on.
Without these, every visitor is anonymous and their conversations belong to the browser. That is the right default for a site with no sign-in.
The conversation follows the visitor from page to page within the tab: a link to another page of your site does not start it over.
Allowed domains
The test key works on localhost and 127.0.0.1, on any port, straight away.
Before the site goes live, add its domain in the console under Settings →
Security → Allowed domains. A key in a web page can be read by anyone who opens
its source; the list is what stops it working on their site. Until the domain
is listed, the live key answers that site with origin_not_allowed.
musteri.com means https://musteri.com. www.musteri.com is a separate
entry, and *.musteri.com covers every subdomain. Your mobile apps send no
domain and are not affected by the list.
Dark mode
With the default themeMode: "system", the panel follows your page first and
the browser second: a data-theme="light" or data-theme="dark" on <html>
(or else <body>), which is what most sites' own appearance switches set, and
without one the visitor's prefers-color-scheme. Both are watched, so a switch
repaints the panel. "light" or "dark" is final, and
Rendel.setThemeMode(...) changes the mode while the page runs. The dark
colours are darkTheme, or derived from theme when it is left out; see
Map your brand to the catalog.
Permissions
Nothing to add to any file. The "+" offers Photos and Files, which open the browser's own file picker (pictures; PDFs and text files), and Camera, which the browser asks the visitor for the first time it is used. The microphone uses the browser's speech recognition — the browser asks the visitor for the microphone the first time it is pressed. It opens voice mode: what the visitor says is written in the middle of the panel as they say it, and sent on its own when they stop.
- The microphone needs https (
localhostcounts). Firefox has no speech recognition, so it is not shown there; everything else works. - Inside an
<iframe>, the frame needsallow="microphone; camera". - If your site sends a Content-Security-Policy, add these sources to its directives:
script-src https://rendel.ai;
connect-src https://api.rendel.ai https://tiles.openfreemap.org;
img-src https: blob:;
worker-src blob:;tiles.openfreemap.org and worker-src blob: are for the map block (0.20.0
and later). The map engine loads from rendel.ai beside rendel.js, and because a
browser will not start a worker from another origin, it runs its worker from a
blob: URL. If your policy also sets style-src or default-src, add
https://rendel.ai there for the map's stylesheet. blob: in img-src is for
photo previews and your logo.
Actions
Actions you connect in the console run on our servers and work on the website with no code. An action that should run in the page, as the signed-in visitor, is registered the way the Flutter SDK registers one:
Rendel.registerAction({
name: "get_order_status",
description: "Fetch the delivery status of an order",
params: {
type: "object",
properties: { order_id: { type: "string" } },
required: ["order_id"],
},
risk: "read",
handler: async ({ order_id }) => {
const response = await fetch(`/api/orders/${order_id}`);
return response.json();
},
});params is JSON Schema. Return what the copilot should know, or throw to say it
failed; keep the result under 16 KB. write and destructive actions get the
same server-built confirmation card as on a phone, and confirmTemplate words
it.
Screens
A screen of your site the copilot may offer on a
screens card:
Rendel.registerRoute({
name: "order_detail",
title: "My orders › Order",
description: "One order: its items, delivery and payment.",
params: { order_id: { type: "string", required: true, fromActions: true } },
open: ({ order_id }) => router.push(`/orders/${order_id}`),
});A tap shrinks the panel into a bubble and calls open. The bubble brings the
panel back. bubbleOffset (px; 104 on a phone-wide page, 24 otherwise) keeps
the bubble above your own bottom bar.
From 0.15.0. A param marked fromActions must equal a value one of your
actions returned in this conversation. Names are snake_case; a title is 1–60
characters, a description up to 300; up to 8 params and 50 routes.
Options
The data attributes cover the common case. For the rest, leave data-app-key
off the tag and call init yourself:
Rendel.init({
appKey: "rd_pk_test_…",
languages: ["tr"],
assistantName: "Pati",
suggestions: ["Milo bugün nasıl?", "Aşı takvimini göster"],
launcher: false, // your own button: Rendel.open()
});| Option | |
|---|---|
languages, locale | What it answers in; locale is your site's language picker. |
assistantName, launcherLabel, suggestions, emptyStateTitle, emptyStateBody, composerHint | The words on screen. |
strings | Your own words for the SDK's chrome, by key (confirm, goBack, irreversible, composerHint, launcher, …), laid over the shipped set for the answer language; a key you leave out keeps the shipped translation. rendel.d.ts exports the Strings interface, so a misspelt key is a type error. |
attachIcon, voiceIcon, sendIcon | The composer's three glyphs. "off" removes attach or voice. |
theme | surface, onSurface, accent, onAccent and the rest of the console's appearance roles. |
themeMode, darkTheme | system (the default), light or dark, and the colours for dark. See Dark mode. |
launcher | false hides the corner button. Open it with Rendel.open(), close() or toggle(). |
enabled | false turns the copilot off and wins over anything published. |
remoteConfig | false ignores what is published in the console. |
actions | The same as registerAction, all at once. |
userToken | The same as Rendel.setUserToken. |
onOpenUrl | Route a card's link yourself instead of opening a new tab. |
bubbleOffset | px above the bottom edge for the bubble while a screen the copilot opened is showing: 104 on a phone-wide page, 24 otherwise. |
apiBaseUrl, streamIdleTimeoutMs | Where it calls, and how long a turn may go silent (ms, default 45000). |
onLog | Receives the SDK's warnings instead of the browser console. See Log codes. |
Appearance and the opening surface published from the console override these, on the website and the app alike, without a release.
Rendel.on("opened" | "closed" | "messageSent" | "actionExecuted", fn)
subscribes to what happens; Rendel.deviceId is the anonymous id to quote
in a support ticket. Rendel.isOpen, Rendel.isInitialized and
Rendel.version report state. Rendel.refreshConfig() re-reads the published
appearance. Rendel.pair(code) pairs a test browser with the console
(Pair a test app).
Log codes
What onLog receives, in record.code. The code is stable and safe to switch
on; message is for humans and may be reworded.
code | When |
|---|---|
already_initialized | init was called again with a different appKey; the first one stands. |
invalid_action / invalid_route | A registered action or route was malformed and skipped; the message names the field. |
invalid_user | identify needs a userId of 1 to 128 characters. |
insecure_page | Actions and routes need an https page (or localhost). |
theme_contrast | A theme colour pair failed contrast. |
listener_failed | An on(...) listener threw. |
handler_failed | An action handler threw, or returned something that is not JSON. |
handler_timeout | A handler passed its timeoutMs. |
result_too_large | A handler returned more than 16 KB; the copilot is told it failed. |
context_provider_timeout / context_provider_failed | A context provider missed its 300 ms budget, or threw. |
route_open_failed | A route's open threw (route_failed before 0.21.1). |
manifest_sync_failed | Your actions and routes did not reach the server, or it rejected them. Tried again the next time the panel opens. |
transport_failed | A network call failed. |
config_unreadable | The published config could not be read, so it was ignored. |
surface_stale | Debug: a block arrived for a surface_id with a revision no newer than the one on screen, and was not drawn. |
paired | Debug: pair was accepted. |
pair_test_only / pair_not_found / pair_rate_limited / pair_failed | Why pair did not pair: a live key; a wrong, used or expired code; too many tries in a minute; anything else. |
A turn the server ends with an error is logged under that error's own code
(see Errors). A key that is not a key is reported as
invalid_key in the browser console whatever onLog is, because the copilot
does not start.
TypeScript
Types are published beside the file. Save
rendel.d.ts
into your sources and window.Rendel is typed.