The four moving parts
- Define the form on the character (via
PATCH /api/ai-characters/{slug}/) - Render it — either via the widget, or in your own UI by reading
GET /api/agents/{slug}/appearance/ - Submit — widget/UI POSTs values to your
submit_url, or to Oshara’s managed storage ifsubmit_url: null - Retrieve — for managed storage, list submissions via
GET /api/agents/{slug}/form-responses/
Define forms via API
Forms live inside widget_appearance.forms on the character:
curl -X PATCH https://api.oshara.ai/api/ai-characters/support-bot/ \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"widget_appearance": {
"forms": [
{
"id": "book-demo",
"title": "Book a demo",
"submit_url": "https://yoursite.com/api/demo-requests",
"submit_label": "Confirm booking",
"success_message": "We will 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" },
{ "name": "date", "label": "Preferred date","type": "date", "required": true }
]
}
]
}
}'The moment you save the form, the agent gains a callable tool named after the form’s id (hyphens become underscores: book-demo → tool book_demo).
Multi-step (stepper) forms
Use steps instead of fields:
{
"id": "onboarding",
"title": "Get started",
"submit_url": "https://yoursite.com/api/onboarding",
"steps": [
{
"id": "details",
"title": "Your details",
"fields": [
{ "name": "first_name", "label": "First name", "type": "text", "required": true, "width": "half" },
{ "name": "last_name", "label": "Last name", "type": "text", "required": true, "width": "half" },
{ "name": "email", "label": "Work email", "type": "email", "required": true }
]
},
{
"id": "use-case",
"title": "Your use case",
"fields": [
{
"name": "use_case",
"label": "What are you building?",
"type": "select",
"required": true,
"options": ["Customer support", "Sales assistant", "Internal tool", "Other"]
}
]
}
]
}Full FormDefinition / FormFieldDef schema: Widget → Forms.
Receive submissions at your endpoint
Set submit_url to an absolute URL on your backend. The widget (or your custom UI) POSTs a flat JSON object — field name → value:
// Express handler
app.post("/api/demo-requests", express.json(), async (req, res) => {
const { name, email, company, date } = req.body;
await db.demoRequests.create({ name, email, company, scheduledDate: date });
await slack.notify(`New demo request from ${name} <${email}>`);
// Any 2xx tells the widget the submission succeeded
res.status(200).json({ ok: true });
});| Behaviour | Status code |
|---|---|
Success → widget shows success_message, agent continues | any 2xx |
| Failure → widget shows error, agent notified to retry | any non-2xx |
submit_method
Defaults to POST. Override on the form definition:
{ "submit_url": "https://yoursite.com/api/leads", "submit_method": "PUT" }Submit to Oshara managed storage
Set submit_url: null and the widget POSTs to:
POST /api/agents/{slug}/form-responses/The widget calls this automatically. If you’re submitting from a custom UI, send:
{
"form_id": "book-demo",
"session_id": "sess_a1b2c3",
"values": {
"name": "Alice Smith",
"email": "alice@acme.com",
"date": "2025-07-01"
}
}The session_id is what POST /api/agents/agent-session/ returned at session start.
Retrieve submissions
GET /api/agents/{slug}/form-responses/
Authorization: Bearer <token>Filter with query params:
| Param | Effect |
|---|---|
form_id=book-demo | Only this form |
session_id=sess_abc123 | Only this call |
curl "https://api.oshara.ai/api/agents/support-bot/form-responses/?form_id=book-demo" \
-H "Authorization: Bearer <token>"[
{
"id": 42,
"form_id": "book-demo",
"session_id": "sess_a1b2c3",
"values": {
"name": "Alice Smith",
"email": "alice@acme.com",
"date": "2025-07-01"
},
"created_at": "2026-05-10T14:23:00Z"
}
]Nightly sync into your CRM:
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-agent",
session: submission.session_id,
});
}Rendering forms in your own UI
Fetch the schema via:
GET /api/agents/{slug}/appearance/Then walk appearance.forms[*].fields (or appearance.forms[*].steps[*].fields for steppers) and render each one with your component library. Validate before submit using required, pattern, min/max on the field schema.
async function getForms(slug) {
const r = await fetch(`https://api.oshara.ai/api/agents/${slug}/appearance/`, {
headers: { Origin: "https://yoursite.com" }
});
return (await r.json()).forms ?? [];
}
const forms = await getForms("support-bot");
const demoForm = forms.find(f => f.id === "book-demo");For the agent to open and fill the form by voice during the call, see Voice Form Control. For pre-filling values from your DB or session metadata, see Form Pre-fill.
Common errors
| Symptom | Cause | Fix |
|---|---|---|
| Submission not received | submit_url returns non-2xx or wrong URL | Log requests; confirm absolute URL |
403 on appearance/ GET | Origin not whitelisted | Add domain to allowed_origins |
| Agent never opens form | Form id not in system prompt instructions | Add “when X happens, open form Y” to prompt |
disabled: true on form | Form hidden from agent on purpose | Set disabled: false to expose |