diff --git a/docs/deployment/introduction.mdx b/docs/deployment/introduction.mdx
index f8a328c..23d11fc 100644
--- a/docs/deployment/introduction.mdx
+++ b/docs/deployment/introduction.mdx
@@ -8,5 +8,5 @@ You can deploy Dograh Platform using Docker on a remote server using Docker, eit
- [Docker](docker)
- [Custom Domain](custom-domain)
-- [Web Widget](web-widget)
+- [Add to Website](web-widget)
- [Heroku](heroku)
diff --git a/docs/deployment/web-widget.mdx b/docs/deployment/web-widget.mdx
index 5cd7fab..60d770f 100644
--- a/docs/deployment/web-widget.mdx
+++ b/docs/deployment/web-widget.mdx
@@ -1,11 +1,11 @@
---
-title: Web Widget
-description: You can deploy and embed a Voice Agent that you create on Dograh on any Website or Mobile App, where the visitor of the website can interact with your Voice Agent.
+title: Add to Website
+description: Add your Dograh voice agent to any website so visitors can talk to it.
---
-### How to deploy
+### How to add it
-You can embed your Voice Agent on any external website using the Deploy Agent dialog in your agent's settings.
+Add your voice agent to any website using the Deploy Agent dialog in your agent's settings.
Step 1: Open the agent settings by clicking the gear icon in the top-right of the agent editor.
@@ -15,10 +15,90 @@ Step 2: Scroll to the **Deployment** section and click **Configure Embed**.

-Step 3: Enable embedding, add your website's domain to **Allowed Domains**, choose either **Floating Widget** or **Inline Component**, customize the button (position, color, text), and click **Save Configurations**.
+Step 3: Enable embedding, add your website's domain to **Allowed Domains**, choose **Floating Widget**, **Inline Component**, or **Headless (Bring Your Own UI)**, customize the button (position, color, text) if applicable, and click **Save Configurations**.

Step 4: Copy the generated embed code and paste it into your web page to test your agent.

+
+## Embed modes
+
+| Mode | What it renders | When to use |
+| --------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
+| **Floating Widget** | A circular call button anchored to a corner of the page. | You want a turn-key chat-bubble experience that doesn't disturb your existing layout. |
+| **Inline Component** | A panel rendered inside a `
` that you place in your page. | You want the agent embedded in a specific section (landing-page hero, support tab, etc.). |
+| **Headless** | No UI. Only the audio pipeline plus a JavaScript API on `window.DograhWidget`. | You want full control over the UI — your own buttons, design system, framework state, animations. |
+
+## Headless mode
+
+In Headless mode the widget injects no UI of its own. You render whatever buttons, banners, or in-call indicators you want, and call the JavaScript API to start and end calls.
+
+### JavaScript API
+
+| Method / Callback | Description |
+| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
+| `window.DograhWidget.start()` | Begin a voice call. Must be called from inside a user-gesture handler (e.g. `click`) so the browser grants microphone access. |
+| `window.DograhWidget.end()` | End the active call. |
+| `window.DograhWidget.onStatusChange(cb)` | Fires on every status change. Values: `idle`, `connecting`, `connected`, `failed`. |
+| `window.DograhWidget.onCallStart(cb)` | Fires once the call is connected. |
+| `window.DograhWidget.onCallEnd(cb)` | Fires when the call ends. |
+| `window.DograhWidget.onError(cb)` | Fires on any error (mic permission denied, server error, etc.). |
+| `window.DograhWidget.getState()` | Returns the current widget state, including `connectionStatus`. |
+
+### Recommended pattern
+
+Mirror the call status into a state variable that you own, then render whatever UI you like from it.
+
+#### Vanilla JS
+
+```html
+
+
+
+```
+
+#### React
+
+```tsx
+function TalkButton() {
+ const [status, setStatus] = useState('idle');
+
+ useEffect(() => {
+ window.DograhWidget?.onStatusChange(setStatus);
+ }, []);
+
+ const isLive = status === 'connected' || status === 'connecting';
+ const label = { idle: 'Talk to AI', connecting: 'Connecting…', connected: 'End Call', failed: 'Retry' }[status];
+
+ return (
+
+ );
+}
+```
+
+
+`start()` must run inside a real user-gesture handler (`click`, `touchend`, etc.). Browsers refuse to grant microphone access to scripts that request it outside of one — calling `start()` from a `setTimeout` or on page load will fail with a permission error.
+
diff --git a/docs/images/copy-deployment-code.png b/docs/images/copy-deployment-code.png
index 5178390..5dff882 100644
Binary files a/docs/images/copy-deployment-code.png and b/docs/images/copy-deployment-code.png differ
diff --git a/docs/images/save-configurations.png b/docs/images/save-configurations.png
index 12feda6..3cc5fe5 100644
Binary files a/docs/images/save-configurations.png and b/docs/images/save-configurations.png differ
diff --git a/ui/public/embed/dograh-widget.js b/ui/public/embed/dograh-widget.js
index 647a4be..d6b5bc3 100644
--- a/ui/public/embed/dograh-widget.js
+++ b/ui/public/embed/dograh-widget.js
@@ -125,13 +125,14 @@
state.isInitialized = true;
- // Load styles
- injectStyles();
-
// Create widget UI based on mode
if (state.config.embedMode === 'inline') {
+ injectStyles();
createInlineWidget();
+ } else if (state.config.embedMode === 'headless') {
+ createHeadlessWidget();
} else {
+ injectStyles();
createFloatingWidget();
}
@@ -298,6 +299,18 @@
state.audioElement = audio;
}
+ /**
+ * Create headless widget (no UI — host page drives everything via window.DograhWidget API)
+ */
+ function createHeadlessWidget() {
+ const audio = document.createElement('audio');
+ audio.id = 'dograh-widget-audio';
+ audio.autoplay = true;
+ audio.style.display = 'none';
+ document.body.appendChild(audio);
+ state.audioElement = audio;
+ }
+
/**
* Toggle call (start or stop based on current state)
*/
@@ -582,6 +595,10 @@
// Use appropriate update function based on mode
if (state.config.embedMode === 'inline') {
updateInlineStatus(status, text, subtext);
+ } else if (state.config.embedMode === 'headless') {
+ if (state.callbacks.onStatusChange) {
+ state.callbacks.onStatusChange(status, text, subtext);
+ }
} else {
updateFloatingButton(status);
}
diff --git a/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx b/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx
index ace9e02..55e2421 100644
--- a/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx
+++ b/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx
@@ -61,7 +61,7 @@ export function EmbedDialog({
const [isEnabled, setIsEnabled] = useState(false);
const [domains, setDomains] = useState([]);
const [newDomain, setNewDomain] = useState("");
- const [embedMode, setEmbedMode] = useState<"floating" | "inline">("floating");
+ const [embedMode, setEmbedMode] = useState<"floating" | "inline" | "headless">("floating");
const [position, setPosition] = useState("bottom-right");
const [buttonText, setButtonText] = useState("Start Call");
const [buttonColor, setButtonColor] = useState("#10b981");
@@ -81,7 +81,7 @@ export function EmbedDialog({
// Load settings
if (response.data.settings) {
const settings = response.data.settings as Record;
- setEmbedMode((settings.embedMode as "floating" | "inline") || "floating");
+ setEmbedMode((settings.embedMode as "floating" | "inline" | "headless") || "floating");
setPosition(settings.position || "bottom-right");
setButtonText(settings.buttonText || "Start Call");
setButtonColor(settings.buttonColor || "#10b981");
@@ -266,7 +266,7 @@ export function EmbedDialog({
{/* Embed Mode Selection */}
-
+
+
@@ -306,26 +322,28 @@ export function EmbedDialog({
- {/* Shared: Button Color */}
-
-
-
- setButtonColor(e.target.value)}
- className="w-14 h-10 cursor-pointer"
- />
- setButtonColor(e.target.value)}
- placeholder="#10b981"
- className="flex-1"
- />
+ {/* Shared: Button Color (skipped in headless — host renders its own UI) */}
+ {embedMode !== "headless" && (
+
• Add the embed script tag to your page (see below).
+
• The widget renders no UI — render your own buttons.
+
• Call window.DograhWidget.start() to begin a call.
+
• Call window.DograhWidget.end() to end it.
+
• Subscribe to onCallStart, onCallEnd, onStatusChange, onError to drive your UI.
+
• start() must run inside a user-gesture handler (click) so the browser grants microphone access.
+
+
+
+
+
Example — track status in your own state
+
+ Mirror the call status into a variable you control, then render whatever UI you like from it. The status values are idle, connecting, connected, failed.
+