Skip to Content
DocumentationVoice SDKCreating an Agent

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:

#MethodBuild step?You control the UI?Best for
1Script tag❌ (prebuilt)Landing pages, CMS sites, fastest possible embed
2Prebuilt UI❌ (prebuilt)Bundled apps that want the ready-made widget
3Headless / custom UI✅ (fully)Fully branded experiences, non-widget layouts
4React✅ / ❌ (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 call
  • createVoiceAgent(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", });
OptionTypeDefaultMeaning
targetHTMLElementdocument.bodyMount point
inlinebooleanfalseFill the container instead of floating; hides the FAB
openChatbooleanfalseOpen the panel immediately
closeButtonHidebooleanfalseHide the panel’s close button
rootIdstring"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 support

Cleanup

client.destroy(); // ends any active call + removes all listeners

See 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 useVoiceAgent in React).
  • React app?Method 4: the hook for custom UI, the component for the prebuilt one.

Next steps

Last updated on