CDN Embed Integration
The fastest way to add Ozwell to any website. No build step, no framework required β just a single script tag.
The widget sends requests to the Ozwell API, which routes them to whichever AI backend is configured (LLM gateway, Ollama, or mock). No backend configuration is needed in the embed code itself.
Use https://api.ozwell.ai/widget for production embeds. The loader at https://ozwellapi.os.mieweb.org/widget is for development.
Quick Startβ
Add this snippet to your HTML, just before the closing </body> tag:
<!-- Production loader -->
<script src="https://api.ozwell.ai/widget"></script>
That's it! A chat widget will appear in the bottom-right corner of your page.
Let Visitors Sign Inβ
The basic embed omits a host key. On a deployment with widget authentication enabled, visitors sign in through the widget.
Opening chat offers the methods configured by that deployment: Google, Apple, email one-time code, or the visitor's own agent/parent key. Google and Apple use a popup; allow popups for the widget origin. Host-configured agent or parent keys continue to work directly and never trigger this sign-in gate.
Sign-in authorizes the current session; it does not add persistent chat history or memory. User-entered keys are session-only by default. Remembering a key is explicit opt-in with a trusted-personal-device warning. Use the widget's forget-key/sign-out control to clear it. Browser storage may be blocked in third-party iframes; sign-in does not require remembering a key.
Conversations remain private to the user and Ozwell, which builds user trust. The host receives lifecycle signals and explicitly configured tool calls, not conversation content. Sharing conversation content is always opt-in.
Operators must configure credentials, email delivery, and account admission before enabling this mode. See the widget authentication deployment guide. The hosted environment may not offer these methods until that rollout is complete.
Optional: Configure an Agentβ
To select an agent for the host page, configure an agent key issued by the same deployment as the loader. Define the configuration before loading the script:
<script>
window.OzwellChatConfig = { apiKey: 'agnt_key-your-agent-key' };
</script>
<script src="https://api.ozwell.ai/widget"></script>
Getting Your Credentialsβ
Use the Manager for the same deployment as your loader. The Manager at
https://ozwellconsole.os.mieweb.org is the development environment; its keys
are for the development loader at https://ozwellapi.os.mieweb.org/widget.
Option A: Agent Keyβ
Agent keys connect to a server-side agent definition that manages the system prompt, model, temperature, and allowed tools. This is the recommended embed path.
- Log in to the Manager for your selected deployment. For development, use development Ozwell Manager with MIE auth (
manager.os.mieweb.orgcredentials). - Ozwell automatically provisions your
ozw_parent key if you do not already have one. - Create an agent in the manager UI.
- Copy the Agent Key from the response (starts with
agnt_key-). - Use it in the embed config.
Option B: Parent API Keyβ
Parent keys give you raw chat-completions access. Use this when the page provides the model, system prompt, and tools inline in window.OzwellChatConfig.
- Log in to the Manager for your selected deployment. For development, use development Ozwell Manager with MIE auth (
manager.os.mieweb.orgcredentials). - Use Show key to copy your parent key (starts with
ozw_). - Put the parent key, model, system prompt, and any page tools in
window.OzwellChatConfig.
Configuration Optionsβ
Configure the loader before loading the script:
<script>
window.OzwellChatConfig = {
apiKey: 'agnt_key-your-agent-key',
title: 'Support',
placeholder: 'Ask a question...',
welcomeMessage: 'How can I help?',
thinkingEnabled: true
};
</script>
<script src="https://api.ozwell.ai/widget"></script>
Available Optionsβ
| Config property | Type | Description |
|---|---|---|
apiKey | string | Agent key (agnt_key-...) or parent key (ozw_...) |
endpoint | string | Chat-completions endpoint; defaults to the loader's origin |
title | string | Title in the host-page widget shell |
placeholder | string | Composer placeholder |
welcomeMessage | string | Initial assistant message |
thinkingEnabled | boolean | Shows the thinking-mode control when reasoning is available |
thinkingDefaultMode | 0 to 3 | 0 hidden, 1 collapsed, 2 auto, 3 expanded |
defaultUI | boolean | Set false when mounting into your own container |
containerId | string | Element ID for a custom mount target |
Proposed page control option (not implemented)β
The following pageControl setting is a proposal. It is not implemented in the current or released loader: adding this configuration alone does not activate page control.
For a host application whose root element is #app, the proposed setup would be:
<script>
window.OzwellChatConfig = {
pageControl: { root: '#app', visual: 'mask' }
};
</script>
<script src="https://api.ozwell.ai/widget"></script>
With that proposed configuration, loading /widget would:
- Load the page controller and its styles on demand.
- Scope the controller to
#appand registerbrowser_snapshot,browser_click,browser_fill,browser_select, andbrowser_scrollwith the existing iframe MCP bridge. - Make those tools available to the assistant using the selected visual feedback. The loader would handle registration and dispatch alongside existing custom page tools.
| Proposed setting | Default | Choices |
|---|---|---|
pageControl | Omitted (off) | Supply the object to opt in. |
root | Required | Selector for one application container, with the widget outside it. |
visual | 'mask' | 'mask', 'highlight', 'both', or 'none'. |
Mask would show the moving cursor and temporarily block manual input. Highlight would mark the target while leaving the page interactive. Both would combine these effects; none would hide them.
The current proof of concept, browser-tools.html, wires the controller explicitly. Its guided run, step-through controls, and speed selector remain demo features. See the canonical Proposed page control option for the design.
JavaScript APIβ
The embed script exposes a global OzwellChat object for programmatic control:
Open/Close the Widgetβ
// Open the chat window
OzwellChat.open();
// Close the chat window
OzwellChat.close();
// Toggle open/closed (coming soon)
// OzwellChat.toggle();
Read Page Data With Toolsβ
window.OzwellChatConfig = {
...window.OzwellChatConfig,
tools: [
{
type: 'function',
function: {
name: 'get_page_data',
description: 'Read current page data',
parameters: {
type: 'object',
properties: {}
}
}
}
]
};
document.addEventListener('ozwell-tool-call', (event) => {
const { name, respond } = event.detail;
if (name !== 'get_page_data') return;
respond({
userId: 'user_123',
page: window.location.pathname
});
});
// Send a message as the user (coming soon)
// OzwellChat.sendMessage('Hello, I need help with...');
Eventsβ
Privacy Note: Ozwell respects user privacy. The host site receives only lifecycle eventsβnever conversation content. Users can ask anything without fear of surveillance.
// Widget is ready
window.addEventListener('ozwell:ready', () => {
console.log('Widget loaded');
});
// Chat window opened
window.addEventListener('ozwell:open', () => {
analytics.track('Chat Opened');
});
// Chat window closed
window.addEventListener('ozwell:close', () => {
analytics.track('Chat Closed');
});
// User explicitly shared data (opt-in only)
window.addEventListener('ozwell:user-share', (event) => {
// Only fires when user chooses to share
console.log('User shared:', event.detail);
});
β οΈ No message content events: ozwell:message and ozwell:user-message do not exist. Conversation content is private between the user and Ozwell.
Tool Callingβ
This is the big one. Ozwell can do more than chat β it can take actions on your page. When a user says "update my email to bob@example.com," Ozwell's AI can call a function you define that actually updates the form field.
Here's how it works:
The Ozwell widget runs in a sandboxed iframe. It cannot touch your page directly β no DOM access, no cookies, no JavaScript variables. Instead, when the AI decides to use a tool, the widget sends an ozwell-tool-call event to your page. Your code runs the action and sends a result back. The AI then uses that result to respond to the user.
Step 1: Load the Widget with an Agent Keyβ
Your agent definition (created via the Agent Registration API) includes the tools Ozwell can offer. The widget discovers them automatically.
<script>
window.OzwellChatConfig = { apiKey: 'agnt_key-your-agent-key' };
</script>
<script src="https://api.ozwell.ai/widget"></script>
That's the same script tag from Quick Start. If your agent has tools defined, they just work β the widget discovers them during its MCP handshake with the server.
Step 2: Handle Tool Callsβ
When the AI calls one of your tools, the loader dispatches an ozwell-tool-call DOM event on document. Listen for it and call respond() with the result:
document.addEventListener('ozwell-tool-call', (e) => {
const { name, arguments: args, respond } = e.detail;
if (name === 'update_form_data') {
// Do whatever you want β update inputs, call your own API, etc.
if (args.name) document.getElementById('input-name').value = args.name;
if (args.email) document.getElementById('input-email').value = args.email;
// Tell Ozwell what happened
respond({ success: true, updated: { name: args.name, email: args.email } });
} else if (name === 'get_form_data') {
// Tools can also READ from your page
respond({
success: true,
data: {
name: document.getElementById('input-name').value,
email: document.getElementById('input-email').value
}
});
}
});
You must call respond() or error(). The AI is waiting for the result. If you don't respond, the loader returns a tool error after a timeout.
Step 3: That's Itβ
There is no step 3. The loader handles the MCP protocol, JSON-RPC messages, and postMessage plumbing. You write normal JavaScript in a normal event listener.
What Goes in the Agent Definition vs. Your Pageβ
| Concern | Where It Lives | Who Manages It |
|---|---|---|
| Tool names and descriptions | Agent definition (server) | You, via the Agent API |
| Parameter schemas (what inputs the tool accepts) | Agent definition (server) | You, via the Agent API |
| System prompt ("you are a helpful assistantβ¦") | Agent definition (server) | You, via the Agent API |
| What happens when a tool is called | Your page (client) | Your JavaScript |
The AI sees the tool name, description, and parameter schema to decide when to call a tool and what arguments to pass. Your page decides what to do with those arguments.
Define Page Tools Inlineβ
Use the agent key generated by Ozwell Manager, then add page-side tools directly in your page config when the agent needs to read from or write to the host page:
<script>
window.OzwellChatConfig = {
apiKey: 'agnt_key-your-agent-key',
tools: [{
type: 'function',
function: {
name: 'update_form_data',
description: 'Updates user profile fields on the page',
parameters: {
type: 'object',
properties: {
name: { type: 'string', description: 'New display name' },
email: { type: 'string', description: 'New email address' }
},
required: []
}
}
}]
};
</script>
<script src="https://api.ozwell.ai/widget"></script>
tools[].function is the OpenAI-style tool schema: name, description, and JSON-schema parameters. Do not put executable JavaScript functions there. You still handle actual execution with ozwell-tool-call β the event listener code doesn't change.
Advanced: Parent Key Direct Chatβ
Parent keys (ozw_...) can be used directly for chat completions when the page provides the chat configuration inline. In that mode, put the parent key, model, system prompt, and any page tools in window.OzwellChatConfig:
<script>
window.OzwellChatConfig = {
apiKey: 'ozw_your-parent-key',
model: 'qwen2.5-coder:3b',
system: 'You are a helpful assistant for managing user profiles.',
tools: [{
type: 'function',
function: {
name: 'update_form_data',
description: 'Updates user profile fields on the page',
parameters: {
type: 'object',
properties: {
name: { type: 'string', description: 'New display name' },
email: { type: 'string', description: 'New email address' }
},
required: []
}
}
}]
};
</script>
<script src="https://api.ozwell.ai/widget"></script>
tools[].function is the OpenAI-style tool schema: name, description, and JSON-schema parameters. Do not put executable JavaScript functions there. You still handle actual execution with ozwell-tool-call β the event listener code doesn't change.
For normal embeds, prefer an agent key from Ozwell Manager so the prompt, model policy, and shared tools stay server-side.
Security: What Crosses the Iframe Boundaryβ
Understanding what data flows where:
| Data | Crosses the boundary? | Direction |
|---|---|---|
| Tool call name + arguments | β Yes | Ozwell β Your page |
Tool result (your respond()) | β Yes | Your page β Ozwell |
| User's chat messages | β No | Stays in iframe |
| AI's text responses | β No | Stays in iframe |
| Your page's DOM, cookies, JS | β No | Stays on your page |
The AI can only call tools you've defined. It cannot access your page's DOM, make arbitrary network requests from your origin, or read anything you haven't explicitly returned via respond().
Tipsβ
- Write good tool descriptions. The AI reads them to decide when to use a tool. "Updates user profile fields on the page" is better than "updates stuff."
- Validate arguments. The AI usually gets the schema right, but treat the incoming
argslike any untrusted input β check types and ranges before acting on them. - Return useful results. The AI uses
respond()data to craft its reply. If you return{ success: true }, the AI can only say "done." If you return{ success: true, updated: { name: { from: "Alice", to: "Bob" } } }, the AI can confirm the details. - Use
debug: trueduring development. It shows tool execution pills in the chat UI so you can see what's happening.
For the full postMessage protocol details (useful if you're building a custom integration without the loader), see the Embed Widget README. For the design inspiration behind this architecture, see MCP postMessage Standard.
Examplesβ
Use the production API origin for the loader, iframe, and API requests:
- Production loader:
https://api.ozwell.ai/widget - Hosted iframe page:
https://api.ozwell.ai/widget/frame/ - Development loader:
https://ozwellapi.os.mieweb.org/widget
Basic Embedβ
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
</head>
<body>
<h1>Welcome to My Site</h1>
<p>Content goes here...</p>
<!-- Ozwell Chat Widget -->
<script>
window.OzwellChatConfig = { apiKey: 'agnt_key-your-agent-key' };
</script>
<script src="https://api.ozwell.ai/widget"></script>
</body>
</html>
Custom Title and Welcome Messageβ
<script>
window.OzwellChatConfig = {
apiKey: 'agnt_key-your-agent-key',
title: 'Product Help',
welcomeMessage: 'What would you like to know?'
};
</script>
<script src="https://api.ozwell.ai/widget"></script>
Enable Thinking Controlsβ
<script>
window.OzwellChatConfig = {
apiKey: 'agnt_key-your-agent-key',
thinkingEnabled: true,
thinkingDefaultMode: 2
};
</script>
<script src="https://api.ozwell.ai/widget"></script>
Triggered by Button Clickβ
<button onclick="OzwellChat.open()">Chat with Us</button>
<script>
window.OzwellChatConfig = { apiKey: 'agnt_key-your-agent-key' };
</script>
<script src="https://api.ozwell.ai/widget"></script>
Complete Page with Tool Callingβ
A full working example β an AI assistant that can read and update form fields on the page:
<!DOCTYPE html>
<html>
<head>
<title>My App β with Ozwell</title>
</head>
<body>
<h1>User Profile</h1>
<form id="profile-form">
<label>Name: <input type="text" id="input-name" value="Alice Johnson" /></label><br/>
<label>Email: <input type="email" id="input-email" value="alice@example.com" /></label><br/>
<label>Zip: <input type="text" id="input-zip" value="90210" /></label>
</form>
<!-- 1. Load Ozwell (agent key β tools defined server-side) -->
<script>
window.OzwellChatConfig = {
apiKey: 'agnt_key-your-agent-key',
welcomeMessage: 'Hi! I can view or update your profile. Just ask.',
debug: true // shows tool pills in chat β turn off in production
};
</script>
<script src="https://api.ozwell.ai/widget"></script>
<!-- 2. Handle tool calls -->
<script>
document.addEventListener('ozwell-tool-call', (e) => {
const { name, arguments: args, respond } = e.detail;
if (name === 'get_form_data') {
respond({
success: true,
data: {
name: document.getElementById('input-name').value,
email: document.getElementById('input-email').value,
zip: document.getElementById('input-zip').value
}
});
} else if (name === 'update_form_data') {
if (args.name) document.getElementById('input-name').value = args.name;
if (args.email) document.getElementById('input-email').value = args.email;
if (args.zip) document.getElementById('input-zip').value = args.zip;
respond({ success: true, updated: { name: args.name, email: args.email, zip: args.zip } });
} else {
respond({ success: false, error: `Unknown tool: ${name}` });
}
});
</script>
</body>
</html>
Try saying: "what's my name?" or "change my email to bob@example.com"
Security & Privacyβ
Conversation Privacyβ
π Conversations are private by default. The dialogue between users and Ozwell is never shared with the host site. Users can ask any questionβeven ones they might feel are "dumb"βknowing their conversation stays between them and Ozwell.
Sharing is always opt-in: only when a user explicitly chooses to share information does it become visible to the host site.
API Key Requirementsβ
β οΈ Every widget instance requires a valid API key.
Recommended: use Ozwell Manager to create an agent and copy its agnt_key-... key. The agent key points to the server-side persona, tools, and model policy for that agent.
Advanced: parent keys (ozw_...) can authenticate direct chat-completions/API requests when the page supplies the needed model/system/tool config in window.OzwellChatConfig.
The widget will display a clear error if no key is configured.
Domain Restrictions (Coming Soon)β
For additional security, configure domain restrictions for your scoped key:
- Go to Settings β API Keys in your dashboard (coming soon)
- Edit your scoped key
- Add allowed domains under Domain Restrictions
- Only requests from listed domains will be accepted
Content Security Policy (CSP)β
If your site uses CSP headers, add Ozwell's domains:
Content-Security-Policy:
script-src 'self' https://api.ozwell.ai https://cdn.ozwell.ai;
frame-src 'self' https://api.ozwell.ai;
connect-src 'self' https://api.ozwell.ai;
Troubleshootingβ
Widget Not Appearingβ
- Check the console for JavaScript errors
- Verify your API key is valid and has correct permissions
- Check domain restrictions if configured
- Ensure the script loads after the page content
Widget Appears But Chat Failsβ
- Check the error message in the chat widget β it will show the specific auth error
- Verify your API key starts with
agnt_key-orozw_ - Verify the agent key exists in Ozwell Manager, or that your parent-key config includes a model and system prompt
- Review network tab for 401 responses
Styling Conflictsβ
The widget renders in an iframe, so styling conflicts are rare. If you need to adjust the container:
/* Adjust the widget container position */
#ozwell-widget-container {
z-index: 9999 !important;
}
Next Stepsβ
- Agent Registration API β Create agents and define tools server-side
- MCP postMessage Standard β The design ideas behind Ozwell's tool-calling architecture
- Embed Widget README β Raw postMessage protocol details for custom integrations
- Framework Integration β For React, Vue, Svelte apps
- Iframe Details β Deep dive on iframe security
- Backend API β Server-side integration