API reference · For developers

Vanilla JS API reference

On this pageMethods

Every method, option and callback widget.js exposes, and the schema it accepts. New to the widget? Start with the Vanilla JS guide (opens in a new tab), which puts these together; what the agent does with each of them is in The AmphiVox Voice Agent (opens in a new tab).

Each entry says when to call it or when it fires, what you must do, what happens if you don't, and which widget modes it applies to. Entries that differ between the modes carry a Form-linked / Developer-defined switch: pick yours on any of them and every such entry follows.

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 mode and widgetKey, plus schema for a developer-defined key.
  • If you don't: init throws. 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 }, with values naming exactly the page's field ids (null for no answer). When the SDK fills them (target), pass { page }, and add values only for fields you own, such as a file.
  • If you don't: open() fails with widget_page_required; a missing or extra field id fails with widget_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 onField has returned, for example after an async check. To refuse straight away, return the refusal from onField instead. Both are the same refusal: see Refuse an answer (opens in a new tab).
  • You must: pass the fieldId and writeId from onField, ok: false, and a cause ('validation' or 'write', the default) with a short reason.
  • 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. null clears 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_locked and 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 ending manual.
  • If you call open() too early: before the widget has loaded, it fails with widget_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. onMuteChange reports 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:

OptionDefaultDescription
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 / offsetY0Pixels from the anchored side / bottom.
language'en'The first session's language.
voiceGender'female'The first session's voice: 'female' or 'male'.
showTranscripttrueShow the live transcript inside the dock.
hiddenfalseHide 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 SKIP first; put the value in your form; report the page again. The same writeId can arrive twice, so applying it again must change nothing.
  • To refuse: return { ok: false, cause: 'validation', reason } or { ok: false, cause: 'write' } (or false). To refuse later, call reportWriteResult. 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 onField is 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)

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 code and 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)):

CallbackFires 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

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 has id, label (up to 120 characters), type and page, plus any rules (opens in a new tab). Any other key (pages, title, ui, placeholder) is rejected.
  • page is the field's page number. Pages run 0, 1, 2… with none skipped, up to 10 pages.
  • type is one of str, long, int, float, bool, date, enum, email, phone, url, file, address, legal.
  • Your page stores the files, so the file limits are yours: maxSizeMb is any positive number, and extensions any list of up to 50, each up to 20 characters. Case and a leading dot don't matter (.pdf and PDF are the same): they are normalised to PDF.

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).