Web call widget

Let a visitor talk to your agent from your website: one script tag, a publishable key limited to one agent and to your domains, and nothing to host.

The widget adds a button to your page. When a visitor clicks it, the browser asks for the microphone, starts a web call with your agent and shows the call state. The script is a single file with no dependency: it is served by Infynia, and so is the LiveKit client it loads on the first click. The only third party is Google reCAPTCHA v3, which protects the publishable key.

Add the widget to a page

  1. In the dashboard, open Developers, then Keys, and create a publishable key: choose the agent, list the websites allowed to use it (HTTPS addresses, no wildcard) and set a monthly minute cap. The key is shown once.
  2. Paste the snippet below where the button should appear, or anywhere in the page for a floating button at the bottom right. The Keys tab shows the same snippet, with your key filled in right after creation.
  3. Open the page from one of the allowed websites and click the button.
<script src="https://infynia.quantylcore.com/widget/v1.js"
        data-key="inf_pub_YOUR_KEY"
        data-recaptcha-site-key="YOUR_RECAPTCHA_SITE_KEY"
        defer></script>

data-recaptcha-site-key is the public reCAPTCHA v3 site key of your Infynia account. It is not secret.

AttributeRole
data-keyThe publishable key (inf_pub_...). Required, unless you use data-session-token.
data-recaptcha-site-keyreCAPTCHA v3 site key. Required with data-key.
data-session-tokenSingle-use session token minted by your server (see below). Replaces data-key and data-recaptcha-site-key.
data-targetCSS selector of the element that receives the button, for example #call. Without it, the button floats at the bottom right.
data-langfr or en. Default: the language of the page (lang attribute).
data-labelText of the button when idle. Default: “Talk to the agent”.
data-api-baseAPI root. Default: the domain that serves the script, followed by /api/v1.

What the visitor sees

StateButtonAnnounced to screen readers
Idle“Talk to the agent”The button label.
Connecting“Connecting…”, inactive“Connecting to the agent. Allow the microphone if your browser asks.”
In call“End the call”, red“Call in progress. Your microphone is on.”
Error“Try again”The reason, as an alert: blocked microphone, network failure, website not allowed, too many attempts, agent unavailable, and so on.
  • The button is a real button element: Tab reaches it, Enter and Space activate it, Escape ends a call in progress, and the focus ring is always visible.
  • Texts are in French or English, with no technical error code shown to the visitor.
  • The pulse of the call indicator stops when the visitor prefers reduced motion.
  • The widget lives in a shadow DOM: your page styles do not alter it, and it does not alter your page.

JavaScript API

To place the button yourself or follow the call, load the script without attributes and call InfyniaWidget.init. It returns start(), stop(), getState() and destroy().

<script src="https://infynia.quantylcore.com/widget/v1.js" defer></script>
<div id="call-button"></div>
<script>
  window.addEventListener("DOMContentLoaded", function () {
    var widget = InfyniaWidget.init({
      key: "inf_pub_YOUR_KEY",
      recaptchaSiteKey: "YOUR_RECAPTCHA_SITE_KEY",
      target: "#call-button",
      lang: "en",
      onStateChange: function (e) { console.log(e.state, e.error); },
    });
  });
</script>
  • Options: key, recaptchaSiteKey, target, lang, label, apiBase, onStateChange, plus sessionToken and getSessionToken (see below).
  • onStateChange receives { state } with idle, connecting, in-call or error, and { state: "error", error } with a short code. The same data is sent as an infynia:statechange event on the widget element.

What the call does

The widget sends POST /api/v1/web-calls with the key and a reCAPTCHA token, then joins the room it receives. The call is closed by the server after the maximum duration, 10 minutes by default or what remains of the monthly cap, whichever is shorter. Minutes started count against the cap of the key; the unused part of a reservation is given back when the call ends.

Error codeMeaning
origin_not_allowed (403)The website that hosts the widget is not in the allowed list of the key.
invalid_key (401)The key is wrong or revoked.
invalid_recaptcha (400)The reCAPTCHA check failed. The visitor can reload and try again.
rate_limited (429)Too many calls from the same address: 6 per 10 minutes, for the key path and the session token path alike. The address is the one seen by the server, not a header sent by the visitor.
minute_cap_reached (429)The monthly minute cap of the key is used up.
agent_unavailable (503)The agent of the key is not active.
plan_required (403)The organization has no active paid plan.

A publishable key is made to be in a page. It can only start a call to its agent, from the websites you list, within its monthly cap: it cannot read anything. If it leaks, revoke it from the Keys tab and create another one. For integrations that must not put any key in the page, use session tokens.

Session tokens: no key in the page

If you do not want any key in your page, your server can mint a token for each visitor with POST /v1/web-call-sessions and a secret key that has the webcalls:write scope. The token is single-use and valid for 5 minutes; the browser gives it to the widget instead of the publishable key, and there is no reCAPTCHA, no list of websites and no per-address limit, because the token is the proof that your server wanted this call.

curl -X POST https://infynia.quantylcore.com/api/v1/web-call-sessions \
  -H "Authorization: Bearer $INFYNIA_API_KEY" \
  -H "Idempotency-Key: visit-8841" \
  -H "Content-Type: application/json" \
  -d '{"agentId": "AGENT_ID"}'
{
  "token": "inf_wcs_3f9a0c5d7e1b4a68c2d09e7f1a3b5c4d6e8f0a1b2c3d4e5f",
  "expiresAt": "2026-10-16T09:05:00.000Z",
  "livemode": true
}
  • Body: agentId (required) and variables (optional, flat values, kept with the call and never sent to the browser). The key can be restricted to some agents; a live key needs an active paid plan.
  • Send an Idempotency-Key to retry safely: the same key returns the same token, which may have expired or been used by then, so mint a new one with a new key when in doubt.
  • The token is shown once, and only a hash of it is stored. Do not log it or put it in a URL.
  • The call length is limited like for a publishable key (10 minutes by default). Your organization plan minutes are checked when the token is used. The same limit of 6 calls per 10 minutes per visitor address applies as for a publishable key, and rate_limited (429) is returned beyond it.
  • A token that is wrong, expired or already used gets 401 invalid_session_token. A refusal because of the plan, the quota or an unavailable agent does not spend the token; a failure while starting the call does, so mint another one.

Give the token to the widget with data-session-token (one call: the attribute is removed from the page as soon as it is read), or with the JavaScript options. getSessionToken is called before each call, so the visitor can call again.

<script src="https://infynia.quantylcore.com/widget/v1.js"
        data-session-token="SESSION_TOKEN_FROM_YOUR_SERVER"
        defer></script>
<script src="https://infynia.quantylcore.com/widget/v1.js" defer></script>
<script>
  window.addEventListener("DOMContentLoaded", function () {
    InfyniaWidget.init({
      getSessionToken: function () {
        return fetch("/my-server/web-call-session", { method: "POST" })
          .then(function (r) { return r.json(); })
          .then(function (s) { return s.token; });
      },
    });
  });
</script>

You can also send POST /api/v1/web-calls yourself with { "sessionToken": "inf_wcs_..." } and join the room it returns with any LiveKit client.

With a test key (inf_test_), the token starts a simulated call: the widget shows it as in progress for about a minute, nothing is connected, no minute is used and a call with livemode: false is recorded. See the test mode guide.

Keep the key where it belongs

  • Never put a secret key (inf_live_, inf_test_) in a page: it can do much more than start a web call.
  • The widget sends the key only in the body of the call request, to the domain that serves the script. It never writes it into the page, the console or the events.
  • Keep the list of allowed websites as short as possible, and the monthly cap at what you can afford.