Creating an Agent
This is the complete guide to spinning up a voice agent with the SDK. There are four methods, all built on the same headless core. Pick by how much control over the UI you want:
| # | Method | Build step? | You control the UI? | Best for |
|---|---|---|---|---|
| 1 | Script tag | ❌ | ❌ (prebuilt) | Landing pages, CMS sites, fastest possible embed |
| 2 | Prebuilt UI | ✅ | ❌ (prebuilt) | Bundled apps that want the ready-made widget |
| 3 | Headless / custom UI | ✅ | ✅ (fully) | Fully branded experiences, non-widget layouts |
| 4 | React | ✅ | ✅ / ❌ (both) | React / Next.js apps |
Whichever method you choose, the prerequisites are the same: an agent slug, and — for browser embeds — your domain on the character’s Allowed Origins list. See Authentication.
The core primitive: createVoiceAgent
Methods 2–4 all start by creating a client. Understanding this one function explains all of them.
import { createVoiceAgent } from "@oshara/voice-sdk";
const client = createVoiceAgent({ agentSlug: "support-bot" });
await client.init(); // fetch appearance, prepare the agent (idempotent)
await client.start(); // connect the call (asks for mic permission)
// … talk …
await client.end(); // hang up
client.destroy(); // remove listeners + end any active callcreateVoiceAgent(config)builds the client but does no network I/O. See Configuration for every option.init()fetches the agent’s appearance (theme, labels, forms). Call it once before rendering; it’s safe to call again.start()/end()open and close the live call.- Everything else — orb state, transcript, mute, forms — arrives as events.
The three UI layers below differ only in who calls these methods and who renders the events.
Method 1 — Script tag (no build step)
The fastest path. Drop one tag before </body> and a floating call button appears. This is the embeddable widget, which is itself built on the SDK.
<script
src="https://api.oshara.ai/widget.js"
data-agent="support-bot"
data-api-url="https://api.oshara.ai">
</script>Replace support-bot with your slug. That’s the whole integration.
Full copy-paste page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My Site</title>
</head>
<body>
<h1>Welcome</h1>
<!-- Oshara voice widget -->
<script
src="https://api.oshara.ai/widget.js"
data-agent="support-bot"
data-api-url="https://api.oshara.ai">
</script>
</body>
</html>Common attributes
<script
src="https://api.oshara.ai/widget.js"
data-agent="support-bot"
data-api-url="https://api.oshara.ai"
data-open-chat="true"> <!-- auto-expand the panel on load -->
</script>Window-global alternative (tag managers)
If your site injects scripts dynamically, set globals before loading the widget:
<script>
window.VOICE_AGENT_SLUG = "support-bot";
window.VOICE_API_URL = "https://api.oshara.ai";
window.VOICE_OPEN_CHAT = true;
</script>
<script src="https://api.oshara.ai/widget.js"></script>The script tag is documented in full — every data-* attribute — under Widget → Quickstart and Widget → Configuration. Use the SDK methods below when you need programmatic control.
Method 2 — Prebuilt UI (mountVoiceUI)
You want the exact widget UI (FAB + panel + call/form screens + audio drawer), but inside a bundled app and mounted where you choose. Create a client, then hand it to mountVoiceUI.
import { createVoiceAgent } from "@oshara/voice-sdk";
import { mountVoiceUI } from "@oshara/voice-sdk/ui";
import "@oshara/voice-sdk/styles.css";
const client = createVoiceAgent({ agentSlug: "support-bot" });
await client.init(); // fetch appearance first
const ui = mountVoiceUI(client); // FAB + panel, floating bottom-right
// later, e.g. on route change:
ui.destroy();
client.destroy();mountVoiceUI wires every DOM interaction back to the client and subscribes the UI to the client’s events — you don’t touch either.
Mount options
mountVoiceUI(client, {
target: document.getElementById("voice-slot")!, // where to mount (default document.body)
inline: true, // fill the parent instead of floating (hides the FAB)
openChat: true, // open the panel on mount
closeButtonHide: false,
rootId: "voice-agent-widget-root",
});| Option | Type | Default | Meaning |
|---|---|---|---|
target | HTMLElement | document.body | Mount point |
inline | boolean | false | Fill the container instead of floating; hides the FAB |
openChat | boolean | false | Open the panel immediately |
closeButtonHide | boolean | false | Hide the panel’s close button |
rootId | string | "voice-agent-widget-root" | Host element id |
mountVoiceUI returns { destroy, refs }. Call destroy() to tear down; refs exposes the shadow-DOM elements for advanced host integrations.
The UI lives in a shadow DOM, so your page styles and the widget styles never collide. Import @oshara/voice-sdk/styles.css once so your bundler includes the scoped stylesheet.
Method 3 — Headless / custom UI
You want a fully branded experience — your own buttons, transcript, orb, layout. Create a client, subscribe to its events, and call its methods from your own controls. This is the same surface the prebuilt UI is built on.
import { createVoiceAgent } from "@oshara/voice-sdk";
const client = createVoiceAgent({ agentSlug: "support-bot" });
// 1. Reflect agent + call state
client.on("state", ({ orb, statusLabel }) => renderOrb(orb, statusLabel));
client.on("call:status", ({ status }) => (statusEl.textContent = status));
client.on("connection", ({ phase }) => toggleCallScreen(phase));
client.on("controls", ({ canStart, canMute, canEnd }) =>
setButtons(canStart, canMute, canEnd),
);
client.on("mute", ({ muted }) => muteBtn.classList.toggle("on", muted));
// 2. Render the transcript (coalesce interim → final by role:segmentId)
client.on("transcript:clear", () => (transcriptEl.innerHTML = ""));
client.on("transcript", ({ role, segmentId, text, isFinal }) =>
upsertBubble(`${role}:${segmentId}`, role, text, isFinal),
);
// 3. Prepare, then drive from your own buttons
await client.init();
startBtn.onclick = () => client.start();
endBtn.onclick = () => client.end();
muteBtn.onclick = () => client.toggleMute();Handling agent-triggered forms
An agent can ask for structured input mid-call. When it does, you get form:show; render the fields and push edits back with updateFormValues:
client.on("form:show", ({ definition, draft, stepIndex }) =>
renderFields(definition, draft, stepIndex),
);
// Push on-screen edits into the core model on every input.
// This REPLACES the DOM reads the prebuilt UI does internally.
input.addEventListener("input", () =>
client.updateFormValues(readMyFormValues()),
);
submitBtn.onclick = () => {
client.updateFormValues(readMyFormValues()); // capture before submit
client.submitForm(); // validates → submits or advances
};
client.on("form:validation", ({ errors }) => showInlineErrors(errors));
client.on("form:update", () => rerender(client.getActiveForm())); // agent voice-filled a field
client.on("form:submitted", ({ successMessage }) => showSuccess(successMessage));
client.on("form:error", ({ message }) => showError(message));
client.on("form:close", () => hideForm());The core owns validation, multi-step navigation, the round-trip messages to the agent, and the submission POST. You only render. Full detail in Forms.
Optional: an audio settings UI
client.on("audio", (snapshot) => renderSettings(snapshot)); // prefs + applied + filter status
client.updateAudioSettings({ noiseFilter: "krisp", outputVolume: 70 });
const { inputs, outputs } = await client.enumerateAudioDevices();
const caps = client.getAudioCapabilities(); // setSinkId / voiceIsolation supportCleanup
client.destroy(); // ends any active call + removes all listenersSee the Client API and Events references for the full surface, and Building a Custom UI for the contract in depth.
Method 4 — React
@oshara/voice-sdk/react is a thin layer over the core. It gives you two options.
4a. useVoiceAgent() — your own components
The hook creates and owns a client for the component’s lifetime, subscribes to its events, and returns reactive state + bound actions.
import { useVoiceAgent } from "@oshara/voice-sdk/react";
function VoicePanel() {
const {
ready, connected, orb, statusLabel, muted,
transcript, activeForm, formErrors, formSubmitting,
start, end, toggleMute, submitForm,
} = useVoiceAgent({ agentSlug: "support-bot" });
if (!ready) return <p>Loading…</p>;
return (
<div>
<button onClick={connected ? end : start}>
{connected ? "End" : "Start"}
</button>
{connected && (
<button onClick={toggleMute}>{muted ? "Unmute" : "Mute"}</button>
)}
<p>{orb}{statusLabel ? ` — ${statusLabel}` : ""}</p>
<ul>
{transcript.map((t) => (
<li key={t.key} className={t.isFinal ? "final" : "interim"}>
<b>{t.role}:</b> {t.text}
</li>
))}
</ul>
{activeForm && (
<form onSubmit={(e) => { e.preventDefault(); submitForm(); }}>
{/* render activeForm.definition fields; call updateFormValues onChange */}
{formErrors.map((er) => <small key={er.name}>{er.message}</small>)}
<button type="submit" disabled={formSubmitting}>Submit</button>
</form>
)}
</div>
);
}Returned state: client, ready, orb, statusLabel, callStatus, connected, muted, appearance, audio, transcript (TranscriptItem[]), activeForm ({ definition, values, stepIndex } | null), formErrors, formSubmitting.
Returned actions: start, end, toggleMute, sendText, updateFormValues, submitForm, stepForm, closeForm. Need more? client exposes the full API.
The config is read once on mount. Changing it later does not recreate the client — to switch agents, remount the component with a different React key:
<VoicePanel key={agentSlug} />4b. <VoiceWidget> — the prebuilt UI in React
Drops the full widget UI into a container div. The DOM UI layer is dynamically imported, so a headless-only app that never renders it doesn’t pay for it in the bundle.
import { VoiceWidget } from "@oshara/voice-sdk/react";
export default function App() {
return <VoiceWidget config={{ agentSlug: "support-bot" }} inline openChat />;
}Props: config: VoiceAgentConfig, plus inline, openChat, closeButtonHide, className, style.
In Next.js / SSR, the SDK touches browser APIs, so render these components on the client only — put "use client" at the top of the file (App Router) or load them with next/dynamic and { ssr: false }.
Which method should I use?
- Just need it live on a marketing page? → Script tag.
- Bundled app, happy with the standard widget? → Prebuilt UI (or
<VoiceWidget>in React). - Need it to match your brand / a non-widget layout? → Headless (or
useVoiceAgentin React). - React app? → Method 4: the hook for custom UI, the component for the prebuilt one.
Next steps
- Configuration — every
createVoiceAgentoption. - Authentication — origin allow-listing vs. server keys.
- Client API — all methods.
- Events — everything the client emits.
- Forms — agent-driven data collection.