@convai/web-widget
v0.3.2
Published
Two-line script-tag embed for the Convai chat widget. A <convai-widget> custom element with a closed shadow root and a lazily loaded runtime.
Readme
@convai/web-widget
Add a Convai character to any website with one custom element and one script tag.
Quickstart
Paste this, replace two values, and you have a working widget. It puts your API key in the page, which is fine on your own machine and not fine in production — step two moves it to your server.
<convai-widget character-id="YOUR_CHARACTER_ID" api-key="YOUR_API_KEY"></convai-widget>
<script src="https://cdn.jsdelivr.net/npm/@convai/[email protected]/v1.js" async></script>A launcher bubble appears in the bottom-right corner. Click it: the runtime downloads, the panel opens, and you can talk to your character by text or by voice.
React
The script tag goes in your HTML shell; the element is ordinary JSX.
<!-- index.html -->
<script src="https://cdn.jsdelivr.net/npm/@convai/[email protected]/v1.js" async></script><convai-widget character-id={CHARACTER_ID} api-key={API_KEY} />In Next.js, use next/script with strategy="afterInteractive" — it injects a
classic script tag, which is what the loader needs.
Load
v1.jsfrom a script tag, never from a bundle. The loader derives the runtime chunk's URL from its own<script src>, so a bundled import (or a<script type="module">) leaves it nothing to derive from: the bubble paints and then fails on click withcould not resolve the runtime URL. That applies to the element only. If you are building an app, you probably want the library entries below instead.
Already using @convai/web-sdk or @convai/embody?
Then you already have a ConvaiClient, and you should not let the element build
a second one: that is two connections, two conversation histories and two
billed sessions against the same character, with no error to tell you.
Import the chat UI instead and hand it the client you have. These entries keep
@convai/web-sdk, React and livekit-client external, so your bundler links
one copy of each — the app's own.
React
import { useConvaiClient } from '@convai/web-sdk/react'
import { ConvaiWidget } from '@convai/web-widget/react'
export function Chat() {
const client = useConvaiClient({ apiKey: API_KEY, characterId: CHARACTER_ID })
return <ConvaiWidget convaiClient={client} />
}convaiClient takes any ConvaiClient, not only useConvaiClient's: one you
built with new ConvaiClient(...), or the character's own client from
@convai/embody. The widget subscribes to its events itself and never opens a
second connection.
import { useState } from 'react'
import type { ConvaiClient } from '@convai/web-sdk/core'
import { ConvaiCharacter } from '@convai/embody/react'
import { ConvaiWidget } from '@convai/web-widget/react'
export function CharacterWithChat() {
const [client, setClient] = useState<ConvaiClient | null>(null)
return (
<>
<ConvaiCharacter apiKey={API_KEY} characterId={CHARACTER_ID} onReady={(c) => setClient(c.client)} />
{client && <ConvaiWidget convaiClient={client} />}
</>
)
}No framework
import { ConvaiClient } from '@convai/web-sdk/core'
import { createConvaiWidget } from '@convai/web-widget/vanilla'
const client = new ConvaiClient({ apiKey: API_KEY, characterId: CHARACTER_ID })
createConvaiWidget(document.body, { convaiClient: client })The vanilla entry is hand-written DOM and CSS. It imports no React, styled-components or framer-motion, so none of them reach your bundle.
| Entry | What it is | For |
|---|---|---|
| v1.js (script tag) | the <convai-widget> element, SDK bundled in | a page with no build step |
| @convai/web-widget/react | ConvaiWidget, MarkdownRenderer, useCharacterInfo | React apps |
| @convai/web-widget/vanilla | createConvaiWidget | apps without a framework |
These moved here from @convai/web-sdk, which now creates clients and ships no
UI. Install @convai/web-sdk alongside this package; it is a peer dependency.
Before you ship
api-key is visible to anyone who views source, never expires, and is scoped
to your whole account — the widget logs a one-time warning saying so. Replace
it with token-endpoint, a single route you host whose entire job is to relay
one call:
app.post('/api/convai-token', async (req, res) => {
const upstream = await fetch('https://api.convai.com/user/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'CONVAI-API-KEY': process.env.CONVAI_API_KEY },
body: '{}',
})
res.status(upstream.status).json(await upstream.json())
})Then swap one attribute:
- <convai-widget character-id="YOUR_CHARACTER_ID" api-key="YOUR_API_KEY"></convai-widget>
+ <convai-widget character-id="YOUR_CHARACTER_ID" token-endpoint="/api/convai-token"></convai-widget>The widget mints on first engagement and reuses the token for about an hour, so
this runs once per hour of use, not once per conversation. token-endpoint
outranks api-key, so leaving both on during a migration is safe — but delete
the key anyway, it is still in your page source.
A third mode, connect-endpoint, proxies the full connect request for teams who
want to inspect or gate it server-side. All three are covered in the docs.
If your character has Long-Term Memory enabled, add end-user-id — the
connect request is rejected without it. Each distinct value consumes an account
resource, so read the guidance before pointing it at production traffic.
Why it is built this way
- The script tag costs 8 KB. It paints the launcher immediately and nothing else. The full SDK — around 230 KB gzipped — is fetched only when a visitor actually engages, so a page nobody talks to pays almost nothing.
- Your CSS and the widget's cannot collide. Everything renders inside a closed shadow root, so the host page cannot reach in and the widget cannot leak out. Theming happens through eight documented custom properties.
- Your API key stays on your server once you move to
token-endpoint.
Documentation
Authentication in depth with working Express, Next.js and Cloudflare Worker
recipes; the complete attribute and event reference; a React and Next.js usage
guide; theming and ::part() hooks; CSP requirements; and guidance on
end-user-id and Long-Term Memory.
Versioning
0.3.2 is a stable release: npm install @convai/web-widget and
jsDelivr's unversioned URL both resolve it. Pin the exact version anyway for
reproducible deploys —
<script src="https://cdn.jsdelivr.net/npm/@convai/[email protected]/v1.js" async></script>npm install @convai/[email protected] # only needed to self-host the two filesThis package is early, and it bundles a prerelease of @convai/web-sdk. The
bundle is self-contained, so you install nothing from that — but expect the
surface to move before 1.0.
Browser support
Any browser with custom elements and shadow DOM: Chrome, Edge, Firefox, and Safari 16.4+. The widget needs microphone access for voice mode; text chat works without it.
License
Licensed under the Apache License 2.0. Copyright 2026 Convai.
