Documentation

Build with VoiceAgent.sbs

Everything you need to configure an AI voice agent, connect a phone number, ground it in your own content, and embed a live voice-call widget on your website - with real, copy-pasteable examples.

On this page

Overview

VoiceAgent.sbs is made up of three services that work together: a Next.js dashboard (where you configure agents), a Django API (organizations, agents, knowledge base, calls, billing-adjacent data), and a FastAPI realtime voice server (the actual speech-to-text → LLM → text-to-speech loop, for both phone calls and the website widget). Telephony runs over Twilio or Telnyx; call audio and transcripts, knowledge base documents, and post-call analysis are all stored per-organization.

This page documents the parts you'll actually configure and integrate: agents, the knowledge base, phone numbers, and - in the most depth - the embeddable website voice widget, since that's the piece you wire into an external site's own codebase.

Architecture at a glance

A call - whether it arrives by phone or through the website widget - always flows through the same pipeline:

StageWhat happens
TransportTwilio/Telnyx (phone) or a browser WebSocket (widget) streams live audio to the FastAPI voice server.
Speech-to-textIncoming audio is transcribed in real time.
Reasoning (RAG)An LLM decides what to say, grounded in the agent's knowledge base - it won't invent answers outside that content.
Text-to-speechThe reply is streamed back as audio, with support for the caller interrupting mid-sentence.
PersistenceTranscript, recording (if enabled) and call metadata are saved via the Django API as the call progresses and on hang-up.
Post-call analysisAfter the call ends, an LLM pass extracts a summary, sentiment, and any structured fields you've configured (e.g. a booking) from the transcript.

Note

Phone calls and website-widget calls share this exact same pipeline end to end - the only difference is how audio gets to the voice server (a telephony webhook vs. a browser WebSocket). Everything downstream (transcripts, recording, post-call analysis, bookings) works identically for both.

Quickstart

To get an agent live in a few minutes:

  1. Create an agent - open the console and go to Agents → New Agent. Set a greeting message and a system prompt, or start from a template (Customer Support, Sales & Booking, Lead Qualification, Technical Helpdesk).
  2. Pick a voice and tune speech settings (response eagerness, interruption sensitivity, background sound) under Speech Settings.
  3. Add knowledge - upload documents or crawl a website under Knowledge Base so the agent answers from real content (see below).
  4. Test it - use the built-in Live Call (Mic) tester on the agent page to talk to it directly from your browser before connecting a phone number or the widget.
  5. Go live - either connect a phone number (Phone Numbers) for inbound/outbound calling, or enable the Website Widget to embed a voice button on your own site.

Knowledge Base

Every answer your agent gives is grounded in documents you provide - this is retrieval-augmented generation (RAG), not free-form guessing. Open Knowledge Base, pick a library, and add content in either of two ways:

Upload a file

Supports .pdf, .docx and .txt. The file is parsed, split into overlapping ~1000-character chunks, and each chunk is embedded into a vector database so the agent can retrieve the most relevant passages for any question asked mid-call.

Add a website

Switch to the Add Website tab and paste a URL. The platform crawls that page (checking /sitemap.xml first, then following same-origin links up to a page limit), strips navigation/boilerplate, and embeds the extracted text the same way as an uploaded file.

Note

The crawler only fetches publicly reachable pages - it resolves and validates the target's IP address and refuses private, loopback, or link-local addresses (and re-checks every redirect hop), so it can't be used to reach internal/admin systems. A URL that isn't publicly reachable is rejected immediately with a 400 error.

Either way, documents move through the same lifecycle you can watch live in the dashboard: PENDING → PROCESSING → COMPLETED (or FAILED with an error message if something goes wrong).

Phone Numbers & Calls

Under Phone Numbers, connect a Twilio or Telnyx number and assign it to an agent. Once assigned:

  • Inbound calls to that number are answered automatically by the assigned agent.
  • Outbound campaigns can place calls from that number using the same agent configuration.
  • Call status (ringing, in-progress, completed) updates in the dashboard in real time via telephony status callbacks.
  • Recording can be toggled per agent (Call Settings) - when on, both sides of the call are captured and stored as playable audio.

Calendar & Bookings

If a caller books an appointment during the conversation, post-call analysis extracts the date, time, type and any other details you've configured under Post-Call Data Extraction on the agent, and writes them to the call record. The Calendar page reads directly from that extracted data and only shows entries where a real booking was actually discussed - calls that didn't result in a booking won't clutter the calendar.

Call History

The Calls page lists every conversation - phone or widget - with direction (Inbound, Outbound, or Website Widget), duration, live/turn-by-turn transcript, recording playback (if enabled), and the structured post-call analysis output. This is the single place to review what an agent actually said on any given call.

Website Voice Widget

The widget lets a visitor on your own website talk to an agent by microphone directly in the browser - no phone number, no app install. Clicking the embedded button opens a small floating call window (an iframe hosted on VoiceAgent.sbs) that streams the visitor's microphone audio to the same real-time pipeline described above, and plays the agent's voice back in the browser.

Two things make this safe to expose publicly:

  • A per-agent domain allowlist - only websites you explicitly authorize can start a session with your agent.
  • Short-lived, server-issued call credentials - the browser never sees or needs any permanent secret; each call gets a fresh, single-use token.

1. Enable & configure the widget

  1. Open the agent you want to expose, expand Website Widget in its settings.
  2. Toggle Enable Website Widget on.
  3. Under Allowed Domains, add every exact origin the widget will run on - scheme + host, no path, no trailing slash, e.g. https://yourcompany.com. Add www and non-www separately if your site answers on both - they count as different origins.
  4. Click Save Agent. This list is a fail-closed allowlist: if it's empty, or the requesting site isn't on it, the widget refuses to start a session.

Tip

This allowlist is the actual security boundary for who can use your widget - there's nothing else to configure elsewhere (no separate environment variable, no extra approval step).

2. Embed it on your site

Copy the ready-made snippet from the Embed Snippet box (it already has your real agent key filled in) and paste it just before the closing </body> tag. No package to install, no build step - it's a single script tag.

HTML
<script
  src="https://voiceagent.sbs/widget-loader.js"
  data-agent-key="YOUR_WIDGET_KEY"
  async
></script>

In a Next.js / React app, use next/script instead of a raw tag:

TSX
import Script from "next/script";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://voiceagent.sbs/widget-loader.js"
          data-agent-key="YOUR_WIDGET_KEY"
          data-label="Talk to us"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

For a plain HTML / WordPress / Webflow site, paste the same script tag directly into the page footer or a custom HTML block.

Configuration reference

All configuration is passed as attributes on the script tag itself:

AttributeRequiredDescription
data-agent-keyYesThe public widget key for the agent, copied from the Embed Snippet box.
data-labelNoText shown on the button. Defaults to "Voice Agent".
data-api-baseNo*Base URL of the voice server, if it's hosted on a different domain than the widget script itself (see Production deployment below). Defaults to the script's own origin.

Production deployment

Read this before going live

The widget's microphone access only works over HTTPS (or localhost for local testing) - browsers block microphone access on plain http:// pages, and an HTTPS page cannot load a script or call an API over plain HTTP at all (mixed-content blocking). Both the page embedding the widget and the voice server it talks to must be HTTPS.

The widget makes two kinds of requests: an HTTPS POST /widget/session to start a call, and a wss:// WebSocket for the call audio itself. Both are served by the FastAPI voice server, not the Next.js dashboard. In production, put a reverse proxy in front of all three services under one domain so every request the browser makes - static assets, the API, and the WebSocket - resolves correctly without extra configuration:

Nginx
# voiceagent.sbs - one domain in front of all three services
server {
    listen 443 ssl;
    server_name voiceagent.sbs;

    # Next.js dashboard + widget-loader.js + the /widget/:key iframe page
    location / {
        proxy_pass http://127.0.0.1:3000;
    }

    # Django API
    location /api/ {
        proxy_pass http://127.0.0.1:8000;
    }

    # FastAPI voice server - session start + WebSocket upgrade
    location /widget/session {
        proxy_pass http://127.0.0.1:8001;
    }
    location /ws/ {
        proxy_pass http://127.0.0.1:8001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

If instead you run the voice server on its own subdomain (e.g. api.voiceagent.sbs), set data-api-base on the script tag to that URL so the session-start request goes to the right place:

HTML
<script
  src="https://voiceagent.sbs/widget-loader.js"
  data-agent-key="YOUR_WIDGET_KEY"
  data-api-base="https://api.voiceagent.sbs"
  async
></script>

Warning

The data-api-base override only affects the session-start request. The call's WebSocket always connects back to whatever host served the widget's iframe page - so with a split-subdomain setup you still need that domain to proxy /ws/* traffic through to the voice server, as shown in the Nginx example above.

Testing checklist

  1. Confirm the agent's Website Widget toggle is on and your site's exact origin is in Allowed Domains.
  2. Deploy (or tunnel) the voice server so it's reachable over HTTPS from the public internet - localhost is only reachable from your own machine.
  3. Open your live page, hard-refresh, and confirm the floating button appears.
  4. Click it, allow microphone access, and confirm you hear the agent's greeting and can talk back.
  5. Check the Calls page - the call should appear with direction Website Widget.
  6. If a booking was discussed, confirm it shows up on the Calendar page - this proves the full pipeline (transcript → analysis → booking) ran end to end, not just the audio.

Troubleshooting

SymptomCauseFix
Button doesn't appear at allScript failed to load, or the wrong data-agent-key.Check the browser console/network tab for a failed request to widget-loader.js. Confirm the src URL is correct and reachable.
403The page's origin isn't in the agent's Allowed Domains list.Add the exact origin (check http vs https, and www vs non-www) and save the agent.
Script or session request silently fails on an HTTPS pageMixed content - the widget script or API is being loaded over plain http:// from an https:// page.Serve the voice server and widget-loader.js over HTTPS. Browsers block this before any error reaches your code.
429The agent hit its concurrent widget-call limit.Wait for an existing call to end, or reach out if you expect higher concurrent volume.
Call connects but no audio either directionMicrophone permission was denied, or the WebSocket never reached the voice server.Re-check browser mic permissions for the site, and confirm /ws/* is actually proxied through to the voice server in production (see Production deployment above).
Call never appears in Call HistorySession never actually started (fails before the WebSocket opens).Work backwards through the checklist above - a Call row is only created once /widget/session succeeds.

Security model

  • Dashboard auth - JWT-based, scoped per organization; every dashboard API call is authorized against the organization you belong to.
  • Service-to-service auth - the voice server talks to the API using a private internal key, never a user's credentials.
  • Encrypted credential storage - third-party API keys/secrets you connect (telephony, etc.) are encrypted at rest, not stored in plain text.
  • Rate limiting - sensitive endpoints (including widget session starts and knowledge-base URL ingestion) are rate-limited per IP and per key to prevent abuse.
  • SSRF-safe crawling - the website knowledge-base ingester validates and re-validates every URL and redirect against private/internal IP ranges before fetching.
  • Widget domain allowlisting - short-lived, single-use call tokens issued only to origins you've explicitly authorized, as described above.
  • Signed telephony webhooks - inbound requests from Twilio/Telnyx are signature-verified before being trusted.

FAQ

Do phone calls and the website widget use different agents?

No - the same agent (same prompt, voice, knowledge base and settings) can serve phone calls and the website widget simultaneously. Enabling the widget doesn't change how the agent behaves on calls.

Can I run the widget on more than one website?

Yes - add every site's origin to the same agent's Allowed Domains list, and use the same embed snippet on each.

Does the widget work on mobile browsers?

Yes, as long as the browser supports microphone capture over HTTPS (all modern mobile browsers do), the same button and call flow work on mobile.

Is widget call audio recorded the same way as phone calls?

Yes - it's subject to the same per-agent recording toggle under Call Settings, and appears in Call History identically to a phone call.

Support

Can't find what you need here? Reach out at [email protected] or use the contact form.

Ready to configure your first agent?

Open the console and deploy an agent, then come back here whenever you need the exact widget snippet or a config reference.

Open the console