Appearance
Embed Widget
Add an AI-powered assistant to any website with a single <script> tag. The Universal API embed widget supports text chat, voice conversations, or both — with a toggle for end users to switch between modes.
Quick Start
1. Create an Embed Token
bash
curl -X POST https://api.universalapi.co/embed/create \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agentId": "YOUR_TEXT_AGENT_ID",
"voiceAgentId": "YOUR_VOICE_AGENT_ID",
"allowedDomains": ["yourdomain.com", "localhost"],
"greeting": "Hi! How can I help you today?"
}'2. Add the Script Tag
Paste the returned snippet into your HTML:
html
<script src="https://cdn.universalapi.co/embed/widget.js"
data-text-agent="YOUR_TEXT_AGENT_ID"
data-voice-agent="YOUR_VOICE_AGENT_ID"
data-token="emb_pk_live_..."
async></script>That's it! A floating chat button appears on your page.
Modes
The widget supports three modes, controlled by the data-mode attribute or set at token creation:
| Mode | Description | Default when |
|---|---|---|
text | Text chat only (streaming HTTP) | Only agentId provided |
voice | Voice only (WebSocket audio via Nova Sonic) | Only voiceAgentId provided |
both | Both with toggle tabs | Both agents provided |
Text Only
html
<script src="https://cdn.universalapi.co/embed/widget.js"
data-text-agent="TEXT_AGENT_ID"
data-token="emb_pk_live_..."
async></script>Voice Only
html
<script src="https://cdn.universalapi.co/embed/widget.js"
data-voice-agent="VOICE_AGENT_ID"
data-token="emb_pk_live_..."
async></script>Both (with Toggle)
html
<script src="https://cdn.universalapi.co/embed/widget.js"
data-text-agent="TEXT_AGENT_ID"
data-voice-agent="VOICE_AGENT_ID"
data-token="emb_pk_live_..."
data-mode="both"
async></script>When mode is both, users see tabs to switch between 💬 Chat and 🎙️ Voice.
Why Separate Agents?
Text and voice agents use fundamentally different technologies:
- Text agents use streaming HTTP with models like Claude, Nova, or GPT — optimized for detailed, formatted responses with markdown, code blocks, and tool use.
- Voice agents use WebSocket audio with Amazon Nova Sonic — optimized for real-time, natural conversation with low latency audio.
By specifying separate agent IDs, you can optimize each experience independently. For example, use a tool-heavy research agent for text and a fast conversational agent for voice.
How It Works Under the Hood
Understanding the underlying architecture helps when debugging or building custom native integrations.
Text Chat Flow
Browser → POST /embed/chat (embed token)
← { bearerToken, agentId }
Browser → POST stream.api.universalapi.co/agent/{agentId}/chat (bearer token)
← Streaming response (text chunks + control markers)- Authentication — The widget exchanges your
emb_pk_live_*token for temporary streaming credentials viaPOST /embed/chat - Streaming — Messages are sent to the agent's streaming endpoint. The response is an HTTP stream containing markdown text interspersed with control markers (
__META__,__TOOL_START__,__TOOL_END__,__DONE__) - Conversations — A
conversationIdis returned in the stream metadata. The widget saves it and sends it back on subsequent messages for multi-turn memory.
Voice Chat Flow
Browser → WebSocket wss://voice.api.universalapi.co/ws/{agentId}?token=emb_pk_live_...
↔ Bidirectional audio (PCM Int16, base64-encoded JSON)- Connection — The widget opens a WebSocket directly to the voice runtime with the embed token as a query parameter
- Audio capture — Microphone audio is captured at 16kHz, converted to Int16 PCM, base64-encoded, and sent as JSON messages
- Audio playback — The server sends agent speech at 24kHz (Int16 PCM, base64) which is decoded and played through the Web Audio API
- Transcripts — Real-time transcription messages arrive alongside audio for displaying what's being said
Build Your Own UI
Want to skip the widget and integrate these protocols directly into your own app? See the Native Integration Guide for complete protocol documentation, code examples, and a React component.
Creating the Right Agent for Your Widget
The embed widget works with any UAPI agent, but you'll get the best results by designing agents specifically for the embed context.
Text Agent Best Practices
Your text agent uses create_agent() just like any other agent. Tips for embed use:
python
from strands import Agent
from strands.models.bedrock import BedrockModel
def create_agent():
model = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-20250514-v1:0",
region_name="us-east-1"
)
agent = Agent(
model=model,
system_prompt="""You are a helpful customer support agent for Acme Corp.
Rules:
- Keep responses concise (2-3 paragraphs max) since users are chatting in a small widget
- Use markdown formatting sparingly — bullet points are fine, but avoid large code blocks
- If you don't know something, say so and offer to connect them with a human
- Be friendly and professional""",
session_manager=session_manager
)
return agent, []Key design principles:
- Keep responses short — Widget panels are small; users don't want to scroll through essays
- Avoid heavy markdown — Code blocks and tables render poorly in compact UIs
- Be greeting-aware — If you use
data-initial-prompt, your system prompt should support context-aware greetings - Add tools judiciously — MCP tools work great, but each tool call adds latency the user sees as "typing"
Voice Agent Best Practices
Voice agents use create_bidi_agent() (not create_agent()) and run on Amazon Nova Sonic:
python
from strands import Agent
from strands.models.bedrock import BedrockModel
def create_bidi_agent():
model = BedrockModel(
model_id="amazon.nova-sonic-v1:0",
region_name="us-east-1"
)
agent = Agent(
model=model,
system_prompt="""You are a friendly voice assistant for Acme Corp.
Rules:
- Keep responses to 1-2 sentences — long responses feel unnatural in voice
- Use conversational language, not formal writing
- Avoid lists, bullet points, or anything that sounds weird spoken aloud
- If you need to convey complex info, suggest the user switch to text chat
- Confirm understanding before taking actions"""
)
return agentKey design principles:
- Ultra-short responses — In voice, anything over 2 sentences feels like a monologue
- Conversational tone — Write your system prompt as if coaching a human phone agent
- No visual formatting — Markdown, links, and code are meaningless in audio
- Use
initialPrompt— Set it at the agent or embed level so the agent speaks first without waiting for silence detection - Consider latency — Avoid tools that take >2 seconds; voice users expect instant responses
Combining Text + Voice (Both Mode)
When using mode: "both", users can switch between tabs. Design each agent for its medium:
bash
curl -X POST https://api.universalapi.co/embed/create \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agentId": "TEXT_AGENT_ID",
"voiceAgentId": "VOICE_AGENT_ID",
"allowedDomains": ["yourdomain.com"],
"greeting": "Hi! Type a question or click the mic to talk.",
"initialPrompt": "A visitor just opened the support widget. Greet them briefly."
}'initialPrompt applies to text only
The initialPrompt field sends a hidden message to the text agent on load. For voice agents, use the initialPrompt field on the agent record itself (via POST /agent/create or PUT /agent/update) to make the voice agent speak first.
Configuration Options
| Attribute | Description | Default |
|---|---|---|
data-text-agent | Text chat agent ID | — |
data-voice-agent | Voice agent ID | — |
data-token | Embed token (emb_pk_live_...) | Required |
data-mode | "text", "voice", or "both" | Auto-detected |
data-position | "bottom-right" or "bottom-left" | "bottom-right" |
data-color | Brand color (hex) | "#6366f1" |
data-greeting | Greeting message | "Hi! How can I help you today?" |
data-title | Header title (overrides agent name) | Agent name |
data-subtitle | Secondary text below the title | — |
data-logo | Circular avatar image URL in header | 🤖 emoji |
data-initial-prompt | Auto-sent on load; agent's reply becomes the opening message | — |
data-placeholder | Input field placeholder text | "Ask me anything..." |
data-size | "compact", "standard", or "large" | "standard" |
Customization
Branding
Add your logo, title, and subtitle to make the widget match your brand:
html
<script src="https://cdn.universalapi.co/embed/widget.js"
data-text-agent="YOUR_AGENT_ID"
data-token="emb_pk_live_..."
data-title="Acme Support"
data-subtitle="We typically reply instantly"
data-logo="https://yourdomain.com/logo.png"
data-color="#0066cc"
async></script>Initial Prompt (Contextual Greeting)
Instead of a static greeting, use data-initial-prompt to send a hidden message to the agent on load. The agent's response becomes the opening message — perfect for context-aware greetings:
html
<script src="https://cdn.universalapi.co/embed/widget.js"
data-text-agent="YOUR_AGENT_ID"
data-token="emb_pk_live_..."
data-initial-prompt="The user just landed on the pricing page. Greet them and offer to help compare plans."
async></script>The prompt is invisible to the user — they only see the agent's reply as the first message.
Size Presets
Choose from three sizes to fit your layout:
| Size | Dimensions | Best for |
|---|---|---|
compact | 340 × 480 | Mobile-first sites, sidebars |
standard | 400 × 580 | Most websites (default) |
large | 440 × 640 | Desktop-focused, complex agents |
html
<script src="https://cdn.universalapi.co/embed/widget.js"
data-text-agent="YOUR_AGENT_ID"
data-token="emb_pk_live_..."
data-size="large"
async></script>Auto-Open on Page Load
Make the widget open automatically when the page loads — great for landing pages, onboarding flows, or support pages:
html
<!-- Open immediately on load -->
<script src="https://cdn.universalapi.co/embed/widget.js"
data-text-agent="YOUR_AGENT_ID"
data-token="emb_pk_live_..."
data-auto-open
async></script>
<!-- Open after 2 seconds (useful to let cookie banners settle) -->
<script src="https://cdn.universalapi.co/embed/widget.js"
data-text-agent="YOUR_AGENT_ID"
data-token="emb_pk_live_..."
data-auto-open
data-auto-open-delay="2000"
async></script>You can also configure auto-open via the API when creating the embed token:
bash
curl -X POST https://api.universalapi.co/embed/create \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agentId": "YOUR_AGENT_ID",
"autoOpen": true,
"autoOpenDelay": 1500
}'TIP
The auto-open uses a two-phase timing strategy (requestAnimationFrame + setTimeout) to reliably open after the browser has painted the widget. This prevents race conditions with cookie consent banners, third-party chat widgets, and other elements that compete for z-index on page load.
JavaScript API
Control the widget programmatically via window.UniversalAPIWidget:
javascript
// Open/close the widget
UniversalAPIWidget.open();
UniversalAPIWidget.close();
// Switch between text and voice (only in "both" mode)
UniversalAPIWidget.switchTo('voice');
UniversalAPIWidget.switchTo('text');
// Listen for events
UniversalAPIWidget.on('ready', () => console.log('Widget loaded'));
UniversalAPIWidget.on('open', () => console.log('Panel opened'));
UniversalAPIWidget.on('close', () => console.log('Panel closed'));
UniversalAPIWidget.on('voiceStart', () => console.log('Voice session started'));
UniversalAPIWidget.on('voiceEnd', () => console.log('Voice session ended'));
UniversalAPIWidget.on('transcript', (msg) => console.log('Transcript:', msg));
// Check state
console.log(UniversalAPIWidget.getMode()); // "text" | "voice" | "both"
console.log(UniversalAPIWidget.getActiveTab()); // "text" | "voice"
console.log(UniversalAPIWidget.isReady()); // true/false
console.log(UniversalAPIWidget.version); // "3.0.0"Security
- Domain allowlist — Tokens only work on approved domains
- Rate limiting — Per-day and per-IP limits prevent abuse
- Token revocation — Instantly disable a widget by revoking its token
- Shadow DOM isolation — Widget styles don't leak into your page
- No credentials exposed — The
emb_pk_live_token is a restricted publishable key; your API keys are never sent to the browser
Billing
Widget usage is billed to the token owner's account at standard rates:
- Text chat: Normal agent invocation credits (varies by model + tools used)
- Voice: ~50 credits/minute while connected (Nova Sonic real-time audio)
See Pricing for details.