Integrate the SDK › Implementation

Wire up the voice session

In this step you build the voice session — the piece of your code that will listen and fill the form at runtime. You create one per form from the client, with the form object you built in the previous step:

  • React: useSpeechToForm()
  • Angular: injectSpeechToForm()
  • JavaScript: client.speechToForm()

The options are the same everywhere, and so is what you get back: the controls to start and stop listening, one state object to render from, and the recognized values keyed by your field ids.

The examples on this page share one scenario, a patient-intake form: a receptionist presses Speak and lets the patient talk. The whole wiring, using the main options, per framework — the switch compares your form as it is today with the same form once the session is in:

With Speechineer
tsx
import { useForm } from "react-hook-form";
import { useSpeechToForm } from "@speechineer/react";
import { intakeForm } from "./intake-form";
const savedDraft = { insurance: "Public" }; // the draft your form holds — not a submitted record
export function IntakeForm() {
const { register, handleSubmit, setValue, getValues } = useForm<Intake>({ defaultValues: savedDraft });
const voice = useSpeechToForm({
form: intakeForm,
spokenLanguage: "en", // the language the patient will speak
transcript: true, // also stream the spoken text
initialValues: getValues(), // what the form holds now — resent to Speechineer on every start
onFieldValue: (id, value) => value != null && setValue(id as keyof Intake, value as never, { shouldDirty: true }),
onError: (error) => console.warn(error.code, error.message),
});
return (
<form onSubmit={handleSubmit(save)}>
<label>Full name <input {...register("full_name")} /></label>
<label>Date of birth <input type="date" {...register("date_of_birth")} /></label>
<label>Insurance
<select {...register("insurance")}>
<option>None</option><option>Public</option><option>Private</option>
</select>
</label>
<label>Reason for visit <textarea {...register("reason")} /></label>
<button type="submit">Save</button>
{/* isListening & co. live directly on voice — the hook re-renders on every state change, no getState() needed */}
<button type="button" onClick={voice.isListening ? voice.stop : () => void voice.start()} disabled={voice.isConnecting}>
{voice.isConnecting ? "Connecting…" : voice.isListening ? "Stop" : "Speak"}
</button>
{voice.error && <p role="alert">{voice.error.message}</p>}
<label>Transcript {/* the live transcription, streaming while the user speaks */}
<textarea rows={4} readOnly aria-live="polite" value={voice.transcript} />
</label>
</form>
);
}

added · changed — everything else is untouched.

Options

Everything a session takes is one options object:

The ones you will reach for first — each option has its own section below:

form

The form object from the previous step — one configured in your workspace or one defined in code. It is read when a session starts.

Changed while a session runs: takes effect only for the next session, after end().

account

The end user this session is for, when it differs from the client's default account — a shared kiosk where the person changes between sessions, say. Ignored when the client authenticates with a token: the token carries the account. The account object:

Changed while a session runs: takes effect only for the next session, after end().

spokenLanguage

The language the user will speak — independent of the form's language, which is the language of the definition. Detected automatically when omitted; set it when you know it, for faster and more reliable recognition. The two settings being separate is what makes mixed situations work: a German clinic keeps its form defined in German (language: "de") — the record belongs to the clinic, whose staff read the labels and the answers in German — and when a patient happens to speak English, spokenLanguage: "en" is all that changes; the values still land in the same German form, with no second form to maintain.

Changed while a session runs: takes effect at the next start().

transcript

true streams the spoken text alongside the values — transcript in the session state fills while the user speaks, and onTranscript fires with it. Set it when the user should see what was heard.

Changed while a session runs: takes effect only for the next session, after end().

initialValues

Values your form already holds, keyed by field id — a draft, not a submitted record. A common practice is to keep them in sync with the state of your form: recognition then continues from what the user already sees, including anything typed while the session was paused.

Changed while a session runs: takes effect at the next start().

onFieldValue

Called once per recognized value, with (fieldId, value). Values reach you twice — as accumulating session state (values, keyed by field id) and per field through this callback. A refined value replaces the earlier one for the same field, so always overwrite.

Speechineer's backend validates every value against its field type before sending it, so there is nothing to re-check — but the SDK types them unknown, and a field nothing has been extracted for yet arrives as null. A common pattern is to narrow once at the boundary:

tsx
// Speechineer's backend has already validated each value against its field type — no re-checking needed:
onFieldValue: (id, value) => {
  if (value == null) return; // nothing extracted for this field yet
  setValue(id as keyof Intake, value as never, { shouldDirty: true });
},

Changed while a session runs: takes effect immediately — the latest function is the one called.

Tip

A value can arrive for a field your page doesn't render (the form may define more than you show). Ignore unknown ids deliberately rather than crashing on them.

onTranscript

Called with the transcript so far — the full accumulated text, not just the newest words, so you can render it directly. Only fires with transcript set to true.

Changed while a session runs: takes effect immediately — the latest function is the one called.

onStateChange

Called with the whole new state, every time anything in it changes — the same state described in getState. The framework bindings already re-render from it, so reach for this callback only for logging or for pushing the state somewhere else.

Changed while a session runs: takes effect immediately — the latest function is the one called.

onEvent

Called for every status event Speechineer emits for this session — progress, warnings and failures alike, each with a type, a level and a payload. Use it for logging or a live status display; nothing you must react to arrives only here.

Changed while a session runs: takes effect immediately — the latest function is the one called.

onError

Called when something fails in a way you should handle — a rejected request, a denied microphone permission, a lost connection, a quota that is reached. Read error.code to branch and error.recoverable to decide whether to offer a retry; the same error also sits in the session state as error.

Changed while a session runs: takes effect immediately — the latest function is the one called.

Additional information in

onSessionStart

Called once, as soon as the session exists and work can begin, with the session id — keep it to correlate the session with your own logs.

Changed while a session runs: takes effect immediately — the latest function is the one called, though it fires again only when the next session starts.

Results

What the call hands back — the session, its controls and its observable state:

start

Asks for the microphone the first time, connects, and listens. Safe to call again after stop() — it continues the same session.

stop

Pauses listening. Values still being recognized keep arriving, so expect late onFieldValue calls after it and keep applying them.

end

Finishes the session and releases everything. start() afterwards begins a new session.

dispose

Releases the microphone and the connections without finishing the session — for leaving the page. The React and Angular bindings call it for you when the component goes away; in JavaScript, call it yourself.

getState

The session reports one observable state, identical everywhere — React hands it to you as plain values, Angular as signals, JavaScript reads it with getState() or through subscribe. Render from it; never keep a copy.

FieldWhat it tells you
valuesThe latest recognized value of every field, keyed by field id. Starts from initialValues; a refined value replaces the earlier one.
transcriptThe spoken text so far — filled when the session streams it (transcript: true).
isListeningThe microphone is on and audio is being sent. Drives your talk button.
isConnectingThe session or one of its connections is being established. Show a busy state.
isEndingend() is finishing the session, or the microphone is winding down after stop().
errorThe last SpeechineerError, or null; cleared when the next start() succeeds.
sessionIdThe id of the running session, or null — join your own logs on it.
statusThe lifecycle: idle · starting · active · recovering · ending · failed.
connectionsPer-connection status (session, audio, results, transcript): closed · connecting · open · closing.

subscribe

Be told about every state change; returns the unsubscribe function. The framework bindings subscribe for you — in plain JavaScript it is how your UI follows the session.

setOptions

Swaps the options the session reads from — how you change form, language or callbacks between sessions. In React and Angular you never call it: changing what you pass to the hook does the same.

Example

The same wiring once more, this time using every option the session takes — including the per-session account and all the callbacks:

With Speechineer
tsx
import { useForm } from "react-hook-form";
import { useSpeechToForm } from "@speechineer/react";
import { intakeForm } from "./intake-form";
const savedDraft = { insurance: "Public" }; // the draft your form holds — not a submitted record
export function IntakeForm() {
const { register, handleSubmit, setValue, getValues } = useForm<Intake>({ defaultValues: savedDraft });
const voice = useSpeechToForm({
form: intakeForm,
spokenLanguage: "en", // the language the patient will speak
transcript: true, // also stream the spoken text
initialValues: getValues(), // what the form holds now — resent to Speechineer on every start
account: { key: patientId }, // this session's end user, when it differs from the client's default
onTranscript: (text) => console.log("transcript", text),
onSessionStart: (sessionId) => console.log("session", sessionId),
onStateChange: (state) => console.log("state", state.status),
onEvent: (event) => console.log(event.type, event.level),
onFieldValue: (id, value) => value != null && setValue(id as keyof Intake, value as never, { shouldDirty: true }),
onError: (error) => console.warn(error.code, error.message),
});
return (
<form onSubmit={handleSubmit(save)}>
<label>Full name <input {...register("full_name")} /></label>
<label>Date of birth <input type="date" {...register("date_of_birth")} /></label>
<label>Insurance
<select {...register("insurance")}>
<option>None</option><option>Public</option><option>Private</option>
</select>
</label>
<label>Reason for visit <textarea {...register("reason")} /></label>
<button type="submit">Save</button>
{/* isListening & co. live directly on voice — the hook re-renders on every state change, no getState() needed */}
<button type="button" onClick={voice.isListening ? voice.stop : () => void voice.start()} disabled={voice.isConnecting}>
{voice.isConnecting ? "Connecting…" : voice.isListening ? "Stop" : "Speak"}
</button>
{voice.error && <p role="alert">{voice.error.message}</p>}
<label>Transcript {/* the live transcription, streaming while the user speaks */}
<textarea rows={4} readOnly aria-live="polite" value={voice.transcript} />
</label>
</form>
);
}

added · changed — everything else is untouched.