Skip to Content
DocumentationGuidesAPI Integration

When to use the API directly

The widget handles everything for a standard website embed. Use the REST API directly when you need to:

  • Start voice sessions from your own backend (inject user context, keep API keys server-side)
  • Programmatically create or update agent characters
  • Connect your APIs as agent tools (HTTP, MCP)
  • Retrieve form submissions after calls
  • Build a native mobile app or custom front-end without the widget

Authentication

Get a JWT by logging in:

curl -X POST https://api.oshara.ai/api/auth/login/ \ -H "Content-Type: application/json" \ -d '{"email": "you@yoursite.com", "password": "yourpassword"}'
{ "access": "eyJhbGci...", "refresh": "eyJhbGci..." }

Include the access token in every request:

Authorization: Bearer eyJhbGci...

Tokens are valid for 10 days. Refresh before expiry:

curl -X POST https://api.oshara.ai/api/auth/refresh/ \ -d '{"refresh": "eyJhbGci..."}'

Pattern 1 — Start a session from your backend

This is the most important integration pattern. Rather than letting the widget call Oshara directly, your backend fetches the token and passes it through. This way:

  • Your users never need their own Oshara credentials
  • You can inject user-specific context (name, email, account tier, etc.)
  • You control session limits and logging
Browser → POST /your-api/voice-token → your backend → POST /api/agents/agent-session/ → Oshara Browser ← { token, livekit_url } ◄──────────────────────────────────────────────────────────┘

Your backend endpoint

// Node.js / Express app.post("/your-api/voice-token", requireAuth, async (req, res) => { const user = req.user; const session = await fetch("https://api.oshara.ai/api/agents/agent-session/", { method: "POST", headers: { "Content-Type": "application/json", "Origin": "https://yoursite.com" }, body: JSON.stringify({ agent: "support-bot", language: req.body.language || "en", metadata: { user_id: user.id, user_name: user.fullName, user_email: user.email, account_tier: user.plan, // "free" | "pro" | "enterprise" org_id: user.orgId } }) }).then(r => r.json()); // Return only what the browser needs res.json({ token: session.token, livekit_url: session.livekit_url, session_id: session.session_id }); });

Python equivalent

import httpx from fastapi import APIRouter, Depends router = APIRouter() @router.post("/voice-token") async def get_voice_token(user=Depends(get_current_user)): async with httpx.AsyncClient() as client: r = await client.post( "https://api.oshara.ai/api/agents/agent-session/", headers={"Origin": "https://yoursite.com"}, json={ "agent": "support-bot", "metadata": { "user_id": str(user.id), "user_name": user.full_name, "user_email": user.email, } } ) data = r.json() return {"token": data["token"], "livekit_url": data["livekit_url"]}

Pattern 2 — Connect your API as an agent tool

Give the agent the ability to look up data or take actions in your system.

Step 1 — Create an endpoint the agent can call

Your endpoint receives the LLM’s tool arguments as a JSON POST body and returns JSON.

// Your API endpoint app.post("/api/oshara/ticket-lookup", verifyOsharaToken, async (req, res) => { const { ticket_id } = req.body; const ticket = await db.tickets.findById(ticket_id); if (!ticket) return res.status(404).json({ error: "Ticket not found" }); res.json({ id: ticket.id, status: ticket.status, subject: ticket.subject, priority: ticket.priority, updated: ticket.updatedAt }); });

Secure your endpoint with a static token — pass it in the tool’s config.headers. The agent worker sends it on every call.

Step 2 — Register the tool on your character

curl -X PATCH https://api.oshara.ai/api/ai-characters/support-bot/ \ -H "Authorization: Bearer <your-token>" \ -H "Content-Type: application/json" \ -d '{ "widget_appearance": { ... }, "tools": [ { "name": "ticket_lookup", "kind": "http", "description": "Look up a support ticket by its ID. Use when the user mentions a ticket number like TICKET-123.", "input_schema": { "type": "object", "properties": { "ticket_id": { "type": "string", "description": "Ticket ID, e.g. TICKET-123" } }, "required": ["ticket_id"] }, "config": { "url": "https://yoursite.com/api/oshara/ticket-lookup", "method": "POST", "headers": { "X-Oshara-Secret": "your-shared-secret" } } } ] }'

The agent can now look up tickets during a call. If a user says “what’s the status of ticket TICKET-456?”, the agent calls the tool, gets the response, and answers the user — all in real time.

Multiple tools

"tools": [ { "name": "ticket_lookup", "kind": "http", "description": "...", "config": { "url": "https://yoursite.com/api/oshara/ticket-lookup", "method": "POST" } }, { "name": "create_ticket", "kind": "http", "description": "Create a new support ticket. Use when the user reports an issue and wants to log it.", "input_schema": { "type": "object", "properties": { "subject": { "type": "string" }, "priority": { "type": "string", "enum": ["low", "normal", "high"] }, "details": { "type": "string" } }, "required": ["subject", "details"] }, "config": { "url": "https://yoursite.com/api/oshara/tickets", "method": "POST" } }, { "name": "search_docs", "kind": "kb", "description": "Search our knowledge base for policy and product information.", "input_schema": { "type": "object", "properties": { "query": { "type": "string" } }, "required": ["query"] }, "config": {} } ]

Pattern 3 — Retrieve form responses after calls

If you used submit_url: null (managed storage), fetch submissions from your backend:

// Nightly sync job const responses = await fetch( "https://api.oshara.ai/api/agents/support-bot/form-responses/?form_id=book-demo", { headers: { Authorization: `Bearer ${OSHARA_TOKEN}` } } ).then(r => r.json()); for (const submission of responses) { await crm.createLead({ name: submission.values.name, email: submission.values.email, source: "voice-widget", session: submission.session_id }); }

Filter by session to get all forms from a specific call:

GET /api/agents/support-bot/form-responses/?session_id=sess_abc123

Pattern 4 — Programmatically manage characters

Useful if you’re building a SaaS platform on top of Oshara and need to create an agent per customer.

// Create a character for a new customer async function provisionAgent(customer) { const character = await fetch("https://api.oshara.ai/api/ai-characters/", { method: "POST", headers: { Authorization: `Bearer ${OSHARA_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ slug: `${customer.id}-bot`, name: `${customer.companyName} Assistant`, system_prompt: `You are a support agent for ${customer.companyName}. ${customer.agentInstructions}`, greeting: `Hi! I'm ${customer.companyName}'s assistant. How can I help?`, language: "en", allowed_origins: [`https://${customer.domain}`], llm_model: "gpt-4o-mini", widget_appearance: { name: customer.companyName, theme: { primary_color: customer.brandColor } } }) }).then(r => r.json()); return character.slug; // e.g. "cust-42-bot" }

Pattern 5 — Fetch form schema and render it in your own UI

Use this when you want to render Oshara forms in your own app (a React page, a mobile screen, a custom panel) instead of relying on the built-in widget UI.

Step 1 — Fetch form definitions from the appearance endpoint

GET /api/agents/support-bot/appearance/

The response includes a forms array with the complete schema for every form defined on the character:

{ "name": "Support Bot", "theme": { ... }, "forms": [ { "id": "book-demo", "title": "Book a demo", "subtitle": "Tell us about your use case", "submit_url": "https://yoursite.com/api/demo-requests", "submit_label": "Confirm booking", "success_message": "We'll be in touch within one business day!", "fields": [ { "name": "name", "label": "Your name", "type": "text", "required": true }, { "name": "email", "label": "Work email", "type": "email", "required": true }, { "name": "company", "label": "Company", "type": "text", "required": false }, { "name": "use_case", "label": "Use case", "type": "select", "required": true, "options": ["Customer support", "Sales assistant", "Other"] }, { "name": "date", "label": "Preferred date","type": "date", "required": true } ] }, { "id": "contact", "title": "Send us a message", "submit_url": null, "fields": [ { "name": "name", "label": "Name", "type": "text", "required": true }, { "name": "message", "label": "Message", "type": "textarea", "required": true, "rows": 4 } ] } ] }

Each form in the array is self-describing — you have the id, every field’s name, type, label, required flag, and options (for select/radio). You have everything needed to render it with no hardcoding.

Step 2 — Fetch and extract forms (JavaScript)

async function getAgentForms(slug) { const res = await fetch(`https://api.oshara.ai/api/agents/${slug}/appearance/`, { headers: { Origin: "https://yoursite.com" } }); const appearance = await res.json(); return appearance.forms ?? []; // array of FormDefinition objects } // Find a specific form by id const forms = await getAgentForms("support-bot"); const demoForm = forms.find(f => f.id === "book-demo");

Step 3 — Render fields dynamically

Map each field’s type to your UI component:

// React example function FormField({ field, value, onChange }) { switch (field.type) { case "text": case "email": case "tel": case "number": return ( <input type={field.type} name={field.name} value={value} placeholder={field.placeholder ?? ""} required={field.required} onChange={e => onChange(field.name, e.target.value)} /> ); case "textarea": return ( <textarea name={field.name} rows={field.rows ?? 3} value={value} required={field.required} onChange={e => onChange(field.name, e.target.value)} /> ); case "select": return ( <select name={field.name} value={value} required={field.required} onChange={e => onChange(field.name, e.target.value)} > <option value="">Select…</option> {field.options.map(opt => { const val = typeof opt === "string" ? opt : opt.value; const label = typeof opt === "string" ? opt : (opt.label ?? opt.value); return <option key={val} value={val}>{label}</option>; })} </select> ); case "radio": return ( <div> {field.options.map(opt => { const val = typeof opt === "string" ? opt : opt.value; return ( <label key={val}> <input type="radio" name={field.name} value={val} checked={value === val} onChange={() => onChange(field.name, val)} /> {val} </label> ); })} </div> ); case "date": case "time": return ( <input type={field.type} name={field.name} value={value} required={field.required} onChange={e => onChange(field.name, e.target.value)} /> ); case "checkbox": return ( <input type="checkbox" name={field.name} checked={value === "true"} onChange={e => onChange(field.name, e.target.checked ? "true" : "false")} /> ); case "display": return <p>{field.label}</p>; // read-only text, no input default: return ( <input type="text" name={field.name} value={value} onChange={e => onChange(field.name, e.target.value)} /> ); } }

Rendering a multi-step form

Multi-step forms have a steps array instead of a top-level fields array. Each step has its own fields:

{ "id": "onboarding", "title": "Get started", "steps": [ { "id": "details", "title": "Your details", "fields": [...] }, { "id": "use-case", "title": "Your use case", "fields": [...] }, { "id": "schedule", "title": "Book a time", "fields": [...] } ] }
function MultiStepForm({ form }) { const [stepIndex, setStepIndex] = React.useState(0); const [values, setValues] = React.useState({}); // Determine whether to use steps or flat fields const steps = form.steps ? form.steps : [{ id: "default", fields: form.fields }]; const currentStep = steps[stepIndex]; const isLastStep = stepIndex === steps.length - 1; const handleChange = (name, value) => setValues(v => ({ ...v, [name]: value })); const handleNext = () => setStepIndex(i => i + 1); const handleBack = () => setStepIndex(i => i - 1); const handleSubmit = () => submitForm(form, values); return ( <div> <h2>{form.title}</h2> {/* Step progress indicator */} {form.steps && ( <p>Step {stepIndex + 1} of {steps.length}: {currentStep.title}</p> )} {/* Render current step's fields */} {currentStep.fields.map(field => ( <div key={field.name ?? field.label}> {field.type !== "display" && <label>{field.label}</label>} <FormField field={field} value={values[field.name] ?? field.default_value ?? ""} onChange={handleChange} /> {field.help_text && <small>{field.help_text}</small>} </div> ))} {/* Navigation */} <div> {stepIndex > 0 && <button onClick={handleBack}>Back</button>} {isLastStep ? <button onClick={handleSubmit}>{form.submit_label ?? "Submit"}</button> : <button onClick={handleNext}>Next</button> } </div> </div> ); }

Step 4 — Submit the form

Collect all values across steps into one flat object and POST it to submit_url.

async function submitForm(form, values) { // submit_url: null means POST to Oshara managed storage const url = form.submit_url ?? `https://api.oshara.ai/api/agents/support-bot/form-responses/`; const body = form.submit_url ? values // your endpoint: flat values object : { form_id: form.id, values }; // Oshara managed: wrap with form_id const res = await fetch(url, { method: form.submit_method ?? "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); if (!res.ok) throw new Error("Submission failed"); return form.success_message ?? "Submitted successfully."; }

Step 5 — Field validation before submit

Use the schema fields to validate before sending:

function validate(fields, values) { const errors = {}; for (const field of fields) { if (field.type === "display") continue; const val = (values[field.name] ?? "").toString().trim(); if (field.required && !val) { errors[field.name] = `${field.label} is required`; continue; } if (field.type === "email" && val && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) { errors[field.name] = "Enter a valid email address"; } if (field.pattern && val && !new RegExp(field.pattern).test(val)) { errors[field.name] = `${field.label} format is invalid`; } if (field.min !== undefined && Number(val) < field.min) { errors[field.name] = `Minimum value is ${field.min}`; } if (field.max !== undefined && Number(val) > field.max) { errors[field.name] = `Maximum value is ${field.max}`; } } return errors; // empty = valid }

Field schema reference (what the API returns per field)

FieldTypeDescription
namestringKey used in the submitted values object. undefined for display fields.
labelstringDisplay label for the field.
typestringInput type — text, email, tel, textarea, select, number, date, time, checkbox, radio, display.
requiredbooleanWhether the field must be non-empty before submission.
placeholderstringPlaceholder text for text inputs.
optionsstring[] or {value, label?}[]Choices for select and radio.
rowsnumberRow count for textarea.
default_valuestringPre-filled value. Use as initial state.
help_textstringSub-label hint shown below the field.
patternstringRegex pattern for client-side validation.
minnumberMin value (number) or min character length (text).
maxnumberMax value or max character length.
width"full" | "half"Hint for layout: "half" fields go side-by-side in a two-column grid.

Keep your Oshara token out of source code:

# .env OSHARA_API_URL=https://api.oshara.ai OSHARA_TOKEN=eyJhbGci... OSHARA_AGENT_SLUG=support-bot
const OSHARA_HEADERS = { Authorization: `Bearer ${process.env.OSHARA_TOKEN}`, "Content-Type": "application/json" };

Common errors

ErrorCauseFix
403 Forbidden on session startDomain not in Allowed OriginsAdd your domain in the dashboard
404 Not Found on session startWrong agent slugCheck the slug in the dashboard
401 UnauthorizedExpired or missing JWTRefresh your token
Tool call returns errorYour endpoint returned non-2xxCheck your endpoint logs; ensure it returns 2xx
Form submission not receivedsubmit_url misconfiguredCheck the URL is absolute and returns 2xx
Last updated on