Forms
Agents can collect structured data mid-conversation (book a demo, make a reservation, capture a lead). The agent decides when to show a form and can voice-fill fields; your UI renders and submits.
How a form surfaces
- The agent sends a data message (e.g. on topic
form.book-demo). The SDK matches it against the appearance’sformsand emitsform:showwith theFormDefinition, an initialdraft, thestepIndex, and whether a call is active. - As the user types, call
client.updateFormValues(values). The core debounces aform.statepublish so the agent always sees current values (and can fill remaining fields by voice). - When the agent fills fields, the SDK emits
form:update— re-render fromclient.getActiveForm(). client.submitForm()validates. On failure →form:validation(and aform_submit_failedmessage to the agent so it doesn’t claim success). On a multi-step form it advances; on the final step it POSTs and emitsform:submitted+ a confirmation message to the agent. On network failure →form:error.- The form auto-closes shortly after success (
form:close).
FormDefinition
interface FormDefinition {
id: string;
title: string;
fields: FormFieldDef[]; // single-page
steps?: FormStep[]; // OR multi-step
submit_url?: string; // external endpoint; omit → managed storage
submit_method?: "POST" | "PUT" | "PATCH";
success_message?: string;
confirmation_type?: string; // message type sent to the agent on success
confirmation_topic?: string; // default "voice.user_text"
// …layout / labels
}
interface FormFieldDef {
name: string;
label: string;
type: "text" | "email" | "tel" | "textarea" | "select" | "radio"
| "checkbox" | "number" | "date" | "time" | "display";
required?: boolean;
options?: FormFieldOption[]; // select / radio / checkbox
min?: number; max?: number; pattern?: string;
}Submission
- Managed storage (no
submit_url):POST {apiUrl}/api/agents/{slug}/form-responses/with{ form_id, form_data: [{ name, label, value }], session_id? }. The platform stores it and charges one form-submission billing event. Thex-api-key(if configured) is attached only on this first-party path. - External (
submit_urlset): POST/PUT/PATCH the rawvaluesto your endpoint. No platform key is ever sent to a third-party URL.
Where do form definitions come from? They live in the agent’s appearance.forms (served by the appearance endpoint). You can also define them in code and call client.openForm(definition) directly.
Validation
validateFields runs in the browser before any submit: required fields, email / phone / number formats, min / max, and custom pattern. On a multi-step form, an error on an earlier step jumps the user back to it.
Rendering forms in a custom UI
client.on("form:show", ({ definition, draft, stepIndex }) =>
renderFields(definition, draft, stepIndex),
);
// Push on-screen edits into the core model on every input.
input.addEventListener("input", () =>
client.updateFormValues(readMyFormValues()),
);
submitBtn.onclick = () => {
client.updateFormValues(readMyFormValues()); // capture before submit
client.submitForm();
};
client.on("form:validation", ({ errors }) => showInlineErrors(errors));
client.on("form:update", () => rerender(client.getActiveForm()));
client.on("form:submitted", ({ successMessage }) => showSuccess(successMessage));
client.on("form:error", ({ message }) => showError(message));
client.on("form:close", () => hideForm());The core handles validation, multi-step navigation, the form_submit_failed and confirmation messages back to the agent, and the managed-storage POST. You never touch the network for forms.
Opening a form in code
You don’t have to wait for the agent — open a form yourself:
client.openForm(
{
id: "lead",
title: "Get in touch",
fields: [
{ name: "name", label: "Name", type: "text", required: true },
{ name: "email", label: "Email", type: "email", required: true },
],
},
{ email: user.email }, // optional prefill draft
);