Voice Agents

Web Voice SDK

Run a voice agent from your own website UI with the headless StrideOps voice SDK.

The headless voice SDK lets a website talk to a StrideOps voice agent with its own buttons, visualizer and transcript, instead of the embedded chat widget. Audio runs over WebRTC directly between the visitor's browser and the agent.

It is a single script with no dependencies and no build step:

<script src="https://app.strideops.ai/widgets/voice-sdk.v1.js"></script>

It also loads as <script type="module">. Either way it defines window.StrideVoice.

Set up the widget

The SDK connects through a Chat Widget that has the Voice AI channel turned on. In Grow > Chat Widgets:

  1. Create a widget of kind Voice AI (or add the Voice AI channel to an all-in-one widget) and pick the voice agent.
  2. Under Installation > Allowed website origins, add every exact origin that will run the SDK, for example https://example.com, https://www.example.com and, for local development, http://localhost:3000. Origins are exact: scheme, host and port must match, with no path.
  3. Optional: raise Maximum simultaneous voice calls (default 2, max 25) if many visitors may call at once.
  4. Publish the widget and copy its ID.

Requests from any origin that is not on the allowed list are refused, both by the browser (CORS) and by the server.

Quick start

<button id="talk">Talk to our AI</button>
<script src="https://app.strideops.ai/widgets/voice-sdk.v1.js"></script>
<script>
  const call = StrideVoice.create({ widgetId: "YOUR-WIDGET-ID" });
  const button = document.getElementById("talk");

  call.on("call-start", () => (button.textContent = "End call"));
  call.on("call-end", () => (button.textContent = "Talk to our AI"));
  call.on("volume-level", (agentLevel) => {
    button.style.transform = `scale(${1 + agentLevel * 0.2})`;
  });
  call.on("message", (m) => {
    if (m.type === "transcript" && m.transcriptType === "final") console.log(m.role, m.transcript);
  });
  call.on("error", (e) => alert(e.message));

  button.onclick = () => (call.isActive ? call.stop() : call.start());
</script>

Start the call from a click or tap so the browser allows microphone access and audio playback.

React

"use client";
import { useEffect, useRef, useState } from "react";

const SDK_URL = "https://app.strideops.ai/widgets/voice-sdk.v1.js";

function loadSdk(): Promise<void> {
  if ((window as any).StrideVoice) return Promise.resolve();
  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = SDK_URL;
    script.onload = () => resolve();
    script.onerror = () => reject(new Error("Voice SDK failed to load"));
    document.head.appendChild(script);
  });
}

export function useStrideVoice(widgetId: string) {
  const callRef = useRef<any>(null);
  const [active, setActive] = useState(false);
  const [level, setLevel] = useState(0);

  useEffect(() => {
    let cancelled = false;
    loadSdk().then(() => {
      if (cancelled) return;
      const call = (window as any).StrideVoice.create({ widgetId });
      call.on("call-start", () => setActive(true));
      call.on("call-end", () => { setActive(false); setLevel(0); });
      call.on("error", () => setActive(false));
      call.on("volume-level", (v: number) => setLevel(v));
      callRef.current = call;
    });
    return () => { cancelled = true; callRef.current?.stop(); };
  }, [widgetId]);

  return {
    active,
    level,
    start: () => callRef.current?.start(),
    stop: () => callRef.current?.stop(),
  };
}

API

StrideVoice.create(options)

OptionTypeDescription
widgetIdstringRequired. The published chat widget ID.
apiBasestringDefaults to https://app.strideops.ai. Use your white-label app domain if you have one.
contact{ fields, consent?, message? }Only needed when the widget requires its contact form before voice. See below.

Returns a call object.

Call object

MemberDescription
start(options?)Requests the microphone, opens a visitor session (reusing it across calls in the same tab), and connects. Resolves when connecting has begun; call-start fires once audio is live. Rejects with { code, message } on failure. options accepts challengeToken and contact.
stop()Ends the call and releases the microphone. Safe to call at any time.
setMuted(boolean)Mutes or unmutes the visitor's microphone.
isMuted()Current mute state.
isActivetrue while a call is connected.
on(event, handler) / off(event, handler?)Subscribe or unsubscribe.

Events

EventPayload
call-startNone. Audio is connected.
call-end{ reason }: stopped, ended, time_limit, connection_lost, page_exit or error. Fires only for calls that connected; failures before that fire error instead.
volume-level(agentLevel, micLevel), both 0..1, about 20 times a second. agentLevel is the agent's voice.
message{ type: "transcript", role: "user" | "assistant", transcript, transcriptType: "partial" | "final" }. Partial user transcripts update as the visitor speaks.
speech-start / speech-endThe agent started or stopped talking.
error{ code, message }.

Error codes

CodeMeaning
origin_not_allowedThe page origin is not in the widget's allowed origins.
network_errorStrideOps could not be reached. A blocked CORS request also shows up this way, so check allowed origins first.
mic_deniedThe visitor blocked the microphone.
unsupportedThe browser lacks WebRTC or microphone support, or the page is not served over HTTPS.
rate_limitedToo many calls in a short time. Limits: 3 voice calls per visitor per hour, and an hourly organization limit of 10x the concurrency setting (minimum 20).
concurrency_limitThe maximum number of simultaneous voice calls is in use.
consent_requiredThe widget requires its contact form first and no contact was provided.
challenge_requiredThe widget has the spam challenge on and no Turnstile token was provided.
session_expiredThe visitor session expired and could not be renewed.
unavailableVoice is unavailable (widget unpublished, out of credits, agent misconfigured, or the call could not connect).

Contact form and spam challenge

If the widget's contact form or required consent is on, voice only starts after the visitor submits it. Collect those details in your own UI and pass them in:

call.start({
  contact: {
    fields: { name: "Ada Lovelace", email: "ada@example.com" },
    consent: true,
    message: "Requested a demo call",
  },
});

The fields keys must match the widget's form fields. For a pure demo, simply turn the contact form off on that widget.

If the widget's spam challenge (Cloudflare Turnstile) is on, render Turnstile with the site key returned by the widget config and pass the token as call.start({ challengeToken }). Most sites leave the challenge off and rely on the built-in rate limits.

Billing and limits

Voice calls draw from the organization's credits exactly like calls through the embedded widget. Each call is capped at the widget's Maximum voice call minutes.

Demo

A minimal working page lives at https://app.strideops.ai/widgets/voice-sdk-demo.html. Copy it to your own site, set the widget ID, and add that origin to the widget.

On this page