Methods
init() returns the VoiceForm object, and every method is also on window.VoiceForm. It is a singleton, one widget per page: calling init() again destroys the previous widget first.
Before the widget has loaded, the methods that act on a session (open, close, mute, unmute, reportWriteResult, reportAdvanceBlocked) are refused with onError({ code: 'widget_not_ready' }), never queued. The ones that set state (reportPage, setInitialValues, setSchema, setLanguage, setVoiceGender, hide, show) are kept and applied once it has loaded.
VoiceForm.init(options)
- Call it: once, after the loader tag, with the options and callbacks.
- You must: pass
modeandwidgetKey, plusschemafor a developer-defined key. - If you don't:
initthrows. An option name it doesn't know is ignored with a console warning ([AmphiVox] unknown option "…" is ignored). - Modes: both.
Your form lives in AmphiVox; your page mirrors it.
VoiceForm.init({ mode: 'form-linked', widgetKey: 'YOUR_WIDGET_KEY', target: '#application-form' });
You pass the schema in code; nothing is stored.
VoiceForm.init({ mode: 'developer-defined', widgetKey: 'YOUR_WIDGET_KEY', schema, onField });
VoiceForm.reportPage(snapshot)
- Call it: before the first session, after every change to the page's values, and on every page change. It is a page snapshot (opens in a new tab): the whole page, never only what changed.
- You must: when you fill your inputs, pass
{ page, values }, withvaluesnaming exactly the page's field ids (nullfor no answer). When the SDK fills them (target), pass{ page }, and addvaluesonly for fields you own, such as a file. - If you don't:
open()fails withwidget_page_required; a missing or extra field id fails withwidget_page_invalid; a stale report means the agent works from old values. - Modes: both.
VoiceForm.reportPage({ page: 1, values: { goal: null, budget: 5000 } }); // you fill your inputs
VoiceForm.reportPage({ page: 1 }); // the SDK fills them
VoiceForm.reportWriteResult(result)
- Call it: to refuse a write after
onFieldhas returned, for example after an async check. To refuse straight away, return the refusal fromonFieldinstead. Both are the same refusal: see Refuse an answer (opens in a new tab). - You must: pass the
fieldIdandwriteIdfromonField,ok: false, and acause('validation'or'write', the default) with a shortreason. - If you don't: the write counts as accepted.
- Modes: both.
VoiceForm.reportWriteResult({ fieldId, writeId, ok: false, cause: 'validation', reason: 'That coupon has expired.' });
VoiceForm.reportAdvanceBlocked()
- Call it: from
onAdvancePage, when your Next did not move. Report the unchanged page first. - If you don't: the agent waits ~15 seconds for a page change, then treats the move as blocked anyway.
- What happens: if a field on the page is still unanswered, the agent goes back to it; otherwise the session ends as
hands_off. See Page turns (opens in a new tab). - Modes: both.
VoiceForm.reportPage({ page: currentPage, values: valuesOfPage(currentPage) });
VoiceForm.reportAdvanceBlocked();
VoiceForm.setInitialValues(values)
- Call it: before a session, with answers or remembered skips you already hold, from any page, keyed by field id.
- What happens: the agent doesn't ask for them; see Known answers (opens in a new tab). They apply to the next session.
nullclears them. - Modes: both.
VoiceForm.setInitialValues({ email: user.email, newsletter: VoiceForm.SKIP });
VoiceForm.setSchema(schema)
- Call it: to change a developer-defined schema between sessions. See Schema.
- If you call it during a session: it is refused with
widget_schema_lockedand discarded, not kept for later. Call it again once the session has ended. - Modes: developer-defined only. With a form-linked key it is refused with
widget_schema_forbidden: the form's schema is the one stored in AmphiVox.
VoiceForm.setSchema({ fields: [...baseFields, ...extraFieldsFor(plan)] });
VoiceForm.open() / VoiceForm.close()
- Call them: to start or end a session from your own controls.
close()ends it with the endingmanual. - If you call
open()too early: before the widget has loaded, it fails withwidget_not_ready. Call it from a user action. - Modes: both.
startButton.addEventListener('click', () => VoiceForm.open());
VoiceForm.mute() / VoiceForm.unmute()
- Call them: to turn the visitor's microphone off and on.
onMuteChangereports every change, including ones made in the dock. - Note: muting is not pausing (opens in a new tab). A muted visitor counts as silent and the session can time out.
- Modes: both.
muteButton.addEventListener('click', () => VoiceForm.mute());
VoiceForm.setLanguage(code) / VoiceForm.setVoiceGender(gender)
- Call them: to choose the language and voice for the next session. A session already running keeps its own. Codes: Languages and voices (opens in a new tab).
- Note: a voice other than
'female'or'male'falls back to'female'when given at start; passed to the method that changes the voice later, it is ignored and the current voice stays. The visitor can also change both in the dock before starting. - Modes: both.
VoiceForm.setLanguage('hi');
VoiceForm.setVoiceGender('male');
VoiceForm.hide() / VoiceForm.show()
- Call them: to hide or restore the built-in dock at runtime. A live session keeps running.
- Modes: both.
VoiceForm.hide();
VoiceForm.destroy()
- Call it: in a single-page app, when the view that owns the widget unmounts. It removes the iframe and every listener.
- Modes: both.
router.beforeEach(() => VoiceForm.destroy());
Options
Everything init() takes besides its callbacks:
| Option | Default | Description |
|---|---|---|
mode | — | Required. 'form-linked' or 'developer-defined'; must match the key. |
widgetKey | — | Required. From the Widgets area. |
schema | — | Developer-defined only, and required there. See Schema. |
initialValues | — | Known answers or remembered VoiceForm.SKIP markers from any page, keyed by field id. |
target | — | CSS selector of your form. Pass it to turn on autofill: the SDK fills your inputs itself (Autofill (opens in a new tab), and What has to match (opens in a new tab)); works in both modes. |
fieldMap | {} | With target: field id → your control's id or name, where they differ; for an address, an object of each part's control. |
position | 'bottom-right' | Or 'bottom-left'. |
offsetX / offsetY | 0 | Pixels from the anchored side / bottom. |
language | 'en' | The first session's language. |
voiceGender | 'female' | The first session's voice: 'female' or 'male'. |
showTranscript | true | Show the live transcript inside the dock. |
hidden | false | Hide the built-in dock (see Build your own UI (opens in a new tab)). |
Callbacks
Functions you pass to init() alongside the options. Every one is optional, but a working integration handles onAdvancePage and onSessionEnd, plus onField unless you use autofill.
onField(fieldId, value, writeId)
- Fires when: the agent has an answer for a field (a write (opens in a new tab)), or the visitor skipped one (
value === VoiceForm.SKIP,writeId === null). - You must: check for
SKIPfirst; put the value in your form; report the page again. The samewriteIdcan arrive twice, so applying it again must change nothing. - To refuse: return
{ ok: false, cause: 'validation', reason }or{ ok: false, cause: 'write' }(orfalse). To refuse later, callreportWriteResult. See Refuse an answer (opens in a new tab). - If you don't handle it: when you fill your inputs, the answers never reach your form. When the SDK fills them, it has already written the value, so
onFieldis optional.
onField: (fieldId, value, writeId) => {
if (value === VoiceForm.SKIP) return;
setValue(fieldId, value);
reportCurrentPage();
},
onAdvancePage()
- Fires when: a page is complete and the visitor agreed to move on. See Page turns (opens in a new tab).
- You must: run your own Next, then report the new page, or the unchanged page plus
reportAdvanceBlocked(). - If you don't handle it: after ~15 seconds the move counts as blocked, so every multi-page session ends on its first page.
onAdvancePage: () => {
const moved = goToNextPage();
reportCurrentPage();
if (!moved) VoiceForm.reportAdvanceBlocked();
},
onSessionStart()
- Fires when: the session is live.
onSessionStart: () => setLive(true),
onSessionEnd(ending)
- Fires when: a session that started has ended, exactly once.
endingis{ reason, detail? }; see Endings (opens in a new tab). - You must: submit only on
reason === 'complete'. See Handle every ending (opens in a new tab).
onSessionEnd: ({ reason }) => { if (reason === 'complete') submitMyForm(); },
onError({ code, message })
- Fires when: a session couldn't start, or the widget rejected something your page sent. A running session carries on after a rejected message.
- You must: switch on
codeand keep a default branch. See Handle errors (opens in a new tab).
onError: ({ code, message }) => console.warn('[voice]', code, message),
onReconcileRequest()
- Fires when: the agent asked for a fresh page report. The SDK has already answered with your last one (when it fills your inputs, it reads the page again), so this is only a notice.
Live status callbacks
For drawing your own indicators (Build your own UI (opens in a new tab)):
| Callback | Fires when |
|---|---|
onTranscript(speaker, text, index) | A transcript line, new or updated. speaker is 'user' or 'bot'. Replace the line at index; don't append. |
onThinking(active) | The agent starts or stops working on an answer. |
onSpeaking(who, active) | who ('user' or 'bot') starts or stops speaking. |
onMuteChange(muted) | The microphone was muted or unmuted, including from the dock. |
Constants
VoiceForm.SKIP
- What it is: the value
onFieldreceives, with anullwrite id, when the visitor skips an optional field. See Skips (opens in a new tab). - You must: check for it first in
onField, and never put it in your form. - Use it to: send a skip back (in
setInitialValuesor a page report) so the next session doesn't ask again. See Remember a skip across sessions (opens in a new tab).
if (value === VoiceForm.SKIP) return;
Schema
The schema lists the fields the agent asks for and the rules it checks answers against: the field types and rules (opens in a new tab) of the contract. It never describes how a field looks (control type, placeholder, page titles); that stays in your page.
Your form lives in AmphiVox; your page mirrors it.
Your schema is the form in AmphiVox, and it lives there, never in code: the widget loads it by key, so init() takes no schema. Edit it in the widget's editor in the Widgets area, or, for a widget linked to one of your published forms, in that form's own editor.
Keeping your page in step with it is your job. If the form gains a required field, renames an id or moves a field to another page, your page must follow. Until it does, the agent asks for fields your page can't hold, and you see refused writes, blocked page turns or a hands_off. An edit takes effect from the next session; a session already running keeps the version it started with.
The form builder enforces the limits: up to 200 fields in total, 60 per page, and 10 pages. File limits are the builder's too: maxSizeMb from 1 to 20, and extensions from the supported upload list.
You pass the schema in code; nothing is stored.
You pass the schema as schema, and it is checked on the server before any session starts:
- An optional
description(up to 500 characters), which gives the agent context for its opening line. fields: up to 200 in total and 60 per page, with unique ids. Each hasid,label(up to 120 characters),typeandpage, plus any rules (opens in a new tab). Any other key (pages,title,ui,placeholder) is rejected.pageis the field's page number. Pages run0, 1, 2…with none skipped, up to 10 pages.typeis one ofstr,long,int,float,bool,date,enum,email,phone,url,file,address,legal.- Your page stores the files, so the file limits are yours:
maxSizeMbis any positive number, andextensionsany list of up to 50, each up to 20 characters. Case and a leading dot don't matter (.pdfandPDFare the same): they are normalised toPDF.
One schema using every type and rule:
VoiceForm.init({
mode: 'developer-defined',
widgetKey: 'YOUR_WIDGET_KEY',
schema: {
description: 'A short application for our design internship.',
fields: [
{ id: 'full_name', label: 'Full name', type: 'str', required: true, maxLength: 80, page: 0 },
{ id: 'email', label: 'Email', type: 'email', required: true, page: 0 },
{ id: 'phone', label: 'Phone', type: 'phone', region: 'IN', page: 0 }, // read without +91 as Indian
{ id: 'age', label: 'Age', type: 'int', min: 18, max: 99, required: true, page: 0 }, // 18 and 99 allowed
{ id: 'national_id', label: 'National ID', type: 'str', pii: true, page: 0 }, // never asked by voice
{ id: 'role', label: 'Role', type: 'enum', options: ['Design', 'Research', 'Engineering'], required: true, page: 1 },
{ id: 'tools', label: 'Tools you use', type: 'enum', multi: true, options: ['Figma', 'Sketch', 'Framer'], page: 1 },
{ id: 'start_date', label: 'Earliest start date', type: 'date', page: 1 },
{ id: 'relocate', label: 'Open to relocating?', type: 'bool', page: 1 },
{ id: 'hourly_rate', label: 'Expected hourly rate', type: 'float', min: 0, page: 1 },
{ id: 'why', label: 'Why this internship?', type: 'long', maxLength: 2000,
desc: 'What draws them to the role, in their own words.', page: 1 },
{ id: 'home', label: 'Home address', type: 'address', page: 2, components: [
{ subType: 'street', required: true },
{ subType: 'city', required: true },
{ subType: 'zip' },
{ subType: 'country', default: 'IN' }, // assumed when not said
] },
{ id: 'portfolio', label: 'Portfolio link', type: 'url', page: 2 },
{ id: 'cv', label: 'CV', type: 'file', maxSizeMb: 5, extensions: ['pdf', 'docx'], page: 2 },
{ id: 'terms', label: 'I accept the terms', type: 'legal', required: true, page: 2 },
],
},
});
What each rule does, and its default when left out, is in Rules a field can carry (opens in a new tab). A field without maxLength gets 150 characters (str) or 1,000 (long); maxLength itself must be 50 to 5,000.
Rules that don't exist, so the server rejects them as unknown keys: a minimum length, a
word count, a text pattern (regular expression), and number limits that exclude the end
value ("greater than 18" rather than "at least 18"). Check those in your own code as each
answer arrives, and refuse with cause: 'validation' and a short reason (Refuse an
answer (opens in a new tab)): the agent tells the visitor why and asks for a
different answer.
The schema is fixed for a session: it is read when the call connects and never changes during it. Send a new one with setSchema while no session is running; one sent during a session is refused and discarded. See Change the schema between sessions (opens in a new tab).