Embedding
Put an AI agent on your website
One script tag for the hosted widget, one config object for the self-hosted variant, or the headless route where you render everything yourself and only borrow the session.
The script tag
The fastest path: one tag before the closing </body>. It loads the hosted widget runtime, which renders a launcher in the bottom-right corner and handles the whole conversation - session creation, the room connection, microphone permission, transcript rendering and the AI transparency notice.
<!-- bitpull AI agent -->
<script src="https://bitpull.ai/widget/template.js"
data-key="YOUR_AGENT_API_KEY"
data-lang="en"
data-modes="both"
data-color="#2962ff"
data-label="ACME SUPPORT"
data-title="How can we help?"
data-teasers="Opening hours?|Book an appointment"
defer></script>The widget generator on this site builds this tag with a live preview, so you can see what each attribute changes before pasting it into a template.
Attributes
Configuration comes from data-* attributes on the script tag itself. Only data-key is required; without it the runtime logs an error and renders nothing.
| Attribute | Values | What it does |
|---|---|---|
data-key | - | Required. The agent API key from Agent → Website/Deploy → API access. Public by design; regenerate it from the dashboard if it leaks somewhere unwanted. |
data-lang | de | en | Interface language of the widget itself. Defaults to de. The conversation follows the visitor. |
data-modes | both | voice | text | Which modes the launcher offers. Anything other than voice or text falls back to both. |
data-color | #RRGGBB | Accent colour for the launcher, header and buttons. |
data-label | - | Small line above the title - usually the company name. |
data-title | - | Headline inside the widget header. |
data-avatar | URL | data: | Avatar image. A data URI avoids a second request. |
data-teasers | a|b|c | Speech-bubble prompts shown before the conversation starts, pipe separated. |
data-note | - | Industry caveat appended to the AI transparency notice - "no legal advice", "no medical advice". |
data-api | URL | Alternative API base. Defaults to https://api.bitpull.ai; you will rarely touch this. |
The product documentation lists chat as a value for data-modes. The runtime checks for voice and text, and treats anything else - including chat - as both. If you want text only, write data-modes="text".
What the runtime does on its own
- Refuses to load twice. A global guard means a second copy of the tag on the same page is a no-op rather than two launchers.
- Identifies itself as AI. The transparency notice is built into the texts, in line with Article 50 of the EU AI Act. It is customisable -
data-noteappends an industry caveat such as "no legal advice" - but the AI notice itself is meant to stay. - Answers in the visitor's language.
data-langsets the interface language; the agent itself replies in whatever language the visitor uses, within what the agent is configured for. - Needs HTTPS for voice. Microphone access is blocked on plain
httporigins, and the widget says so rather than failing silently.
Self-hosted variant
The dashboard's deploy tab can also emit the full runtime inline instead of a script reference - for teams that want the code in their own repository, or a strict content security policy with no third-party script source. Configuration then comes from a global object instead of data attributes:
<script>
window.__BPW_CONFIG__ = {
apiKey: 'YOUR_AGENT_API_KEY',
apiBase: 'https://api.bitpull.ai',
language: 'en', // 'de' | 'en'
modes: 'both', // 'both' | 'voice' | 'text'
accentColor: '#2962ff',
headerLabel: 'ACME SUPPORT',
headerTitle: 'How can we help?',
avatarUrl: '',
teasers: ['Opening hours?', 'Book an appointment'],
note: ''
}
</script>
<script>/* … the full runtime, inlined from the deploy tab … */</script>Same runtime, same behaviour. The trade-off is that you no longer get fixes automatically - an inline copy is a fork from the moment you paste it.
Headless: your own interface
When the widget's look is not the point - an agent inside a logged-in app, a kiosk, a native shell - skip the runtime entirely. Create the session on your server, hand the browser only the room credentials, and render whatever you want around it.
// POST /api/agent/session - your server, your key.
export default async function handler(req, res) {
// Your own gate: signed-in user, rate limit, bot check. The hosted widget
// cannot do this for you; a headless integration can.
if (!(await allowed(req))) return res.status(429).json({ error: 'rate_limited' })
const upstream = await fetch('https://api.bitpull.ai/api/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.BITPULL_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ language: 'en', interactionMode: 'VOICE' })
})
if (!upstream.ok) return res.status(502).json({ error: 'session_failed' })
// Only the room credentials cross to the browser - never the API key.
const { sessionId, wsUrl, token } = await upstream.json()
res.json({ sessionId, wsUrl, token })
}import { Room, RoomEvent } from 'livekit-client'
const { sessionId, wsUrl, token } = await fetch('/api/agent/session').then((r) => r.json())
const room = new Room()
room.on(RoomEvent.TrackSubscribed, (track) => track.attach(document.body))
await room.connect(wsUrl, token)
await room.localParticipant.setMicrophoneEnabled(true)
// Ending the session is a server call - the browser must not hold the key.
// pagehide fires on mobile tab switches where beforeunload does not.
window.addEventListener('pagehide', () => {
navigator.sendBeacon('/api/agent/end', JSON.stringify({ sessionId }))
})The hosted widget carries the agent key in the page by design - it has to, to create sessions from the browser. In a headless integration you have the option not to, so take it: your server holds the key, applies your own rate limit or sign-in check, and hands out only a room token that expires.
Content security policy
The hosted variant needs the script source allowed, plus the API origin and the LiveKit WebSocket for the connection. The exact LiveKit host comes back in wsUrl at runtime, so read it once from a real session rather than guessing it.
Content-Security-Policy:
script-src 'self' https://bitpull.ai;
connect-src 'self' https://api.bitpull.ai wss://<livekit-host-from-wsUrl>;
img-src 'self' data:;
style-src 'self' 'unsafe-inline'; # the runtime injects its own stylesWordPress, Shopify, Webflow and friends
There is no plugin or app for any of them, and none is needed - every platform in that list has somewhere to paste a script tag:
| Platform | Where the tag goes |
|---|---|
| WordPress | Theme footer, or a header-and-footer snippet plugin. |
| Shopify | theme.liquid, before </body>. |
| Webflow | Project settings → Custom code → Footer code. |
| Wix / Squarespace | Custom code injection, site-wide, footer position. |
| Next.js / Nuxt | A defer script in the document body, not in <head>. |