The four moving parts

  1. Define the form on the character (via PATCH /api/ai-characters/{slug}/)
  2. Render it — either via the widget, or in your own UI by reading GET /api/agents/{slug}/appearance/
  3. Submit — widget/UI POSTs values to your submit_url, or to Oshara’s managed storage if submit_url: null
  4. 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 });
});
BehaviourStatus code
Success → widget shows success_message, agent continuesany 2xx
Failure → widget shows error, agent notified to retryany 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:
ParamEffect
form_id=book-demoOnly this form
session_id=sess_abc123Only 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

SymptomCauseFix
Submission not receivedsubmit_url returns non-2xx or wrong URLLog requests; confirm absolute URL
403 on appearance/ GETOrigin not whitelistedAdd domain to allowed_origins
Agent never opens formForm id not in system prompt instructionsAdd “when X happens, open form Y” to prompt
disabled: true on formForm hidden from agent on purposeSet disabled: false to expose