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 reference server, which routes them to whichever AI backend is configured (LLM gateway, Ollama, or mock). No backend configuration is needed in the embed code itself.
Quick Startβ
To get an API key and create an agent, see the Agent Registration API or contact adamerla128@gmail.com.
Add this snippet to your HTML, just before the closing </body> tag:
<!-- Development server (current) -->
<script>
window.OzwellChatConfig = { apiKey: 'agnt_key-your-agent-key' };
</script>
<script src="https://ozwell-dev-refserver.opensource.mieweb.org/embed/ozwell-loader.js"></script>
<!-- Production (coming soon) -->
<!--
<script
src="https://cdn.ozwell.ai/embed.js"
data-api-key="agnt_key-your-agent-key"
></script>
-->
That's it! A chat widget will appear in the bottom-right corner of your page.
Getting Your Credentialsβ
Ozwell supports two authentication modes:
Option A: Agent Key (Recommended)β
Agent keys connect to a server-side agent definition that manages the system prompt, model, temperature, and allowed tools.
- Get an API key (
ozw_prefix) β contactadamerla128@gmail.comorhorner@mieweb.com - Create an agent via the Agent Registration API
- 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 completions access β you provide the system prompt, model, and tools inline in your client config.
- Contact
adamerla128@gmail.comorhorner@mieweb.comfor an API key - Use the key directly (starts with
ozw_)
Self-service key creation via the Ozwell Dashboard is coming soon.
Configuration Optionsβ
The data-* attribute configuration method is not yet supported. Use window.OzwellChatConfig (shown above) for now.
Customize the widget using data-* attributes:
<script
src="https://cdn.ozwell.ai/embed.js"
data-api-key="agnt_key-your-agent-key"
data-theme="dark"
data-position="bottom-left"
data-primary-color="#10b981"
data-auto-open="false"
data-greeting="Hi! How can I help you today?"
></script>
Available Optionsβ
| Attribute | Type | Default | Description |
|---|---|---|---|
data-api-key | string | required | Agent key (agnt_key-...) or parent key (ozw_...) |
data-theme | "light" | "dark" | "auto" | "auto" | Color theme |
data-position | "bottom-right" | "bottom-left" | "bottom-right" | Widget position |
data-primary-color | string (hex) | "#4f46e5" | Accent color |
data-width | string | "400px" | Chat window width |
data-height | string | "600px" | Chat window height |
data-auto-open | "true" | "false" | "false" | Open on page load |
data-greeting | string | Agent default | Initial greeting message |
data-placeholder | string | "Type a message..." | Input placeholder |
data-button-icon | string (URL) | Ozwell logo | Custom launcher icon |
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();
Update Contextβ
// Set context data (passed to the agent)
OzwellChat.updateContext({
userId: 'user_123',
page: window.location.pathname,
customData: { ... }
});
// 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://ozwell-dev-refserver.opensource.mieweb.org/embed/ozwell-loader.js"></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, message: 'Fields updated' });
} 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(). The AI is waiting for the result. If you don't respond, the conversation will hang.
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.
Alternative: Define Tools Inline (Parent Key)β
If you're using a parent API key (ozw_...) instead of an agent key, you can define tools directly in your page config. This gives you full client-side control but means you also need to provide the system prompt and model yourself:
<script>
window.OzwellChatConfig = {
apiKey: 'ozw_your-api-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://ozwell-dev-refserver.opensource.mieweb.org/embed/ozwell-loader.js"></script>
You still handle ozwell-tool-call the same way β the event listener code doesn't change.
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, message: "Name changed 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β
The examples below use cdn.ozwell.ai/embed.js which is the future production CDN. For now, use:
- Dev:
https://ozwell-dev-refserver.opensource.mieweb.org/embed/ozwell-loader.js - Production:
https://ozwellai-refserver.opensource.mieweb.org/embed/ozwell-loader.js
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
src="https://cdn.ozwell.ai/embed.js"
data-api-key="agnt_key-your-agent-key"
></script>
</body>
</html>
Dark Theme with Custom Positionβ
<script
src="https://cdn.ozwell.ai/embed.js"
data-api-key="agnt_key-your-agent-key"
data-theme="dark"
data-position="bottom-left"
data-primary-color="#f59e0b"
></script>
Auto-Open with Custom Greetingβ
<script
src="https://cdn.ozwell.ai/embed.js"
data-api-key="agnt_key-your-agent-key"
data-auto-open="true"
data-greeting="π Welcome! I'm here to help you find what you're looking for."
></script>
Triggered by Button Clickβ
<button onclick="OzwellChat.open()">Chat with Us</button>
<script
src="https://cdn.ozwell.ai/embed.js"
data-api-key="agnt_key-your-agent-key"
></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://ozwell-dev-refserver.opensource.mieweb.org/embed/ozwell-loader.js"></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, message: 'Profile updated' });
} 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. Use either:
- Agent key (
agnt_key-...) β recommended; persona and tools managed server-side - Parent key (
ozw_...) β for advanced use; you provide all config inline
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://cdn.ozwell.ai;
frame-src 'self' https://embed.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 via
curl GET /v1/agentsif using an agent key - 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