Skip to content
MeridFlow AiFlow v8.x • self-hosted

Embedding the widget

The fastest way to put a live AiFlow agent on your website is a single <script> tag. It drops a floating chat launcher onto the page; visitors who click it get a voice or text conversation with the agent you specify, no further integration work required.

The drop-in embed

<script
  src="https://api.your-domain.com/widget/aiflow-widget.js"
  data-agent="support-bot"
  data-api-base="https://api.your-domain.com"
></script>
  • data-agent is the target agent's slug, not its numeric ID or display name. See Agents & channels for how slugs work.
  • data-api-base is your deployment's base URL, the same https://api.your-domain.com used everywhere else on this site.

That's it. Add the tag to any page and a launcher button appears, docked to the bottom-right corner by default. If your site already has something else anchored there, a cookie banner, another chat widget, add data-position to dock it bottom-left instead:

<script
  src="https://api.your-domain.com/widget/aiflow-widget.js"
  data-agent="support-bot"
  data-api-base="https://api.your-domain.com"
  data-position="bottom-left"
></script>

Voice vs. text mode

When a visitor opens the widget, they choose voice or text. Either can be switched to the other at any time during the conversation.

  • Voice mode starts listening immediately, with no separate "start talking" click, and supports live interruption: the visitor can talk over the agent mid-response (barge-in), the way a phone call works. From voice mode, the visitor can also turn on their camera or share their screen; the video feed layers onto the live audio channel so the agent can see and describe what it's looking at. Video alone, without an active audio channel, never triggers a response on its own.
  • Text mode is a plain typed conversation with streamed replies. No microphone access is ever requested in this mode.
  • Attaching a file, in either mode, is a separate, opt-in-per-agent control (see allow_file_upload above): a visitor can attach a photo, receipt, spreadsheet, slide deck, or document mid-conversation. A JPEG or PNG is sent the same way a screen-share frame is, straight into the live video channel. Anything else is turned into text first, either by extracting it directly or, for a scanned page or a broader photo format with no text to extract, by describing it with a one-shot vision-capable call, and then joins the conversation exactly like a typed message. Supported types: .txt, .md, .csv, .json, .xml, .yaml/.yml, .log, .html/.htm, .pdf, .docx, .xlsx, .pptx, .jpg/.jpeg, .png, .gif, .webp, .heic/.heif.

Access control

By default, any website can embed any agent's widget, and any visitor can start a session. Two independent controls narrow that down; an admin sets both on the agent itself, which is outside the scope of this site since agent configuration is an admin-dashboard concern:

  • Allowed origins. An agent can be restricted to a list of origins (allowed_origins). A session request from a page whose origin isn't on that list is rejected.
  • A site key. An agent can require a site API key on top of origin restriction. Pass it as data-site-key on the script tag, or as an X-Site-Key header if you're talking to the REST API directly (see below).
<script
  src="https://api.your-domain.com/widget/aiflow-widget.js"
  data-agent="support-bot"
  data-api-base="https://api.your-domain.com"
  data-site-key="your-site-key"
></script>

A rejected origin or a missing or invalid site key comes back as a 403 or 401; see Errors & rate limits for the response shape.

Building a custom client

The drop-in script above covers most integrations. If you're building your own front end instead, say a custom mobile app, or a framework where injecting a raw <script> tag is awkward, you'll make the same three calls the bundled widget makes internally.

1. Fetch the agent's public config

GET /api/v1/agents/{slug}/widget/config is public and unauthenticated. It returns just enough to render a launcher before a session exists:

curl https://api.your-domain.com/api/v1/agents/support-bot/widget/config
import httpx

response = httpx.get(
    "https://api.your-domain.com/api/v1/agents/support-bot/widget/config"
)
response.raise_for_status()
config = response.json()
const response = await fetch(
  "https://api.your-domain.com/api/v1/agents/support-bot/widget/config",
);
const config = await response.json();
{
  "slug": "support-bot",
  "name": "Support Bot",
  "greeting_instruction": "Greet the caller warmly and ask how you can help.",
  "brand_primary_color": null,
  "brand_accent_color": null,
  "require_identity": false,
  "allow_file_upload": false
}

This endpoint doesn't check origin or a site key; it's meant to be safe to call from anywhere before a session is created. brand_primary_color and brand_accent_color are deployment-wide, not per-agent (see White-labeling): they're non-null once an Owner or Admin sets them in Settings → Branding. The drop-in embed (aiflow-widget.ts) already applies them as the launcher and panel's default accent automatically; a custom client built against this endpoint directly would need to apply them itself the same way, by setting --af-accent and --af-accent-2 on whatever element hosts the widget's styles. require_identity and allow_file_upload are both per-agent toggles an Owner or Admin sets on the agent itself: the former gates the pre-session contact form, the latter shows or hides the file-attach control described below.

2. Create a session

POST /api/v1/agents/{slug}/widget/session creates a session and returns a short-lived token used to open the live connection. visitor_name and visitor_email are both optional. If your page already knows who's visiting, a logged-in user, for example, passing visitor_email also enables cross-session lookback, if the agent has that turned on, so it can recall its last conversation with the same person.

This is the request that the origin and site-key checks above apply to.

curl -X POST https://api.your-domain.com/api/v1/agents/support-bot/widget/session \
  -H "Content-Type: application/json" \
  -H "X-Site-Key: your-site-key" \
  -d '{"visitor_name": "Jane Doe", "visitor_email": "jane@example.com"}'
import httpx

response = httpx.post(
    "https://api.your-domain.com/api/v1/agents/support-bot/widget/session",
    headers={"X-Site-Key": "your-site-key"},
    json={"visitor_name": "Jane Doe", "visitor_email": "jane@example.com"},
)
response.raise_for_status()
session = response.json()
const response = await fetch(
  "https://api.your-domain.com/api/v1/agents/support-bot/widget/session",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Site-Key": "your-site-key",
    },
    body: JSON.stringify({
      visitor_name: "Jane Doe",
      visitor_email: "jane@example.com",
    }),
  },
);
const session = await response.json();
{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "session_id": "3f9a1b2c4d5e6f708192a3b4c5d6e7f8",
  "expires_in": 300
}

token is short-lived (expires_in seconds, 300 by default) and is only valid for opening the WebSocket connection below, not as a general-purpose API credential.

Session creation is rate-limited

Like event ingestion, widget session creation is rate-limited per client IP to protect your deployment from a scraping bot or a misbehaving page. Legitimate traffic shouldn't come close to the limit; see Errors & rate limits for what a 429 looks like.

3. Open the live session

WS /api/v1/agents/{slug}/widget/ws?token=<token>&mode=voice|text is the live connection itself: audio or text streaming in both directions for the duration of the conversation. token is the value from step 2, and mode selects voice or text for that connection, matching whichever mode the visitor picked in the UI. There's no REST or cURL equivalent for a WebSocket. This is the same endpoint the bundled aiflow-widget.js script connects to internally, so most integrations won't need to speak this protocol directly unless they're replacing the widget UI entirely with a custom one.