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.

Available Hosted runtime · voice and text · no build step

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.

index.html
<!-- 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.

AttributeValuesWhat 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-langde | enInterface language of the widget itself. Defaults to de. The conversation follows the visitor.
data-modesboth | voice | textWhich modes the launcher offers. Anything other than voice or text falls back to both.
data-color#RRGGBBAccent 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-avatarURL | data:Avatar image. A data URI avoids a second request.
data-teasersa|b|cSpeech-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-apiURLAlternative API base. Defaults to https://api.bitpull.ai; you will rarely touch this.
One discrepancy worth knowing

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-note appends an industry caveat such as "no legal advice" - but the AI notice itself is meant to stay.
  • Answers in the visitor's language. data-lang sets 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 http origins, 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:

inline variant
<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.

server - never expose the API key
// 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 })
}
browser - join and render
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 }))
})
Why the session is created server-side here

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.

starting point - verify wsUrl against a real session
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 styles

WordPress, 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:

PlatformWhere the tag goes
WordPressTheme footer, or a header-and-footer snippet plugin.
Shopifytheme.liquid, before </body>.
WebflowProject settings → Custom code → Footer code.
Wix / SquarespaceCustom code injection, site-wide, footer position.
Next.js / NuxtA defer script in the document body, not in <head>.