Nimbia Widget Integration Guide

This guide shows how to add the Nimbia widget to your web app and control it from your own code. The widget loads from one script tag.

#Quick Start

Add this snippet to every page where the widget should appear, and fill in the visitor's details:

html
<script>
(function (w, d) {
  if (w.Nimbia) return;
  var q = [];
  var n = { _q: q };
  "identify update shutdown boot startGuide stopGuide pauseGuide resumeGuide startCall endCall sendText continueCall dismissCtaModal off".split(" ").forEach(function (m) {
    n[m] = function () {
      var a = [].slice.call(arguments);
      return new Promise(function (res, rej) { q.push([m, a, res, rej]); });
    };
  });
  ["on", "once"].forEach(function (m) {
    n[m] = function (e, h) {
      q.push([m, [e, h]]);
      return function () { w.Nimbia.off(e, h); };
    };
  });
  n.ready = new Promise(function (r) { n.once("ready", r); });
  w.Nimbia = n;
  var s = d.createElement("script");
  s.type = "module";
  s.async = true;
  s.src = "https://widget.nimbia.ai/assets/widget.js";
  d.head.appendChild(s);
})(window, document);

Nimbia.boot({
  platformId: "your-platform-id", // from the Nimbia dashboard
  user: {
    id: user.id,
    email: user.email,
    name: user.fullName,
    createdAt: user.createdAt, // sign-up date, ISO 8601
    locale: user.locale, // e.g. "en", "nl"
    company: { id: user.companyId, name: user.companyName },
  },
});
</script>

The first part puts a stub on window.Nimbia and loads the widget script without blocking the page. The stub queues every call made before the script has loaded and runs them in order once it has, so the boot() call below it works.

boot() starts the widget. It takes:

Option Type Required Description
platformId string Yes Your platform ID from the Nimbia dashboard
user object No The visitor, as in the table below. Leave it out to boot now and call identify() once you know who the visitor is
settings object No Settings to start with, e.g. { visible: false }
appFrame true or object No Show your app in an iframe on this page, so full page loads don't end a call. See Apps with full page loads

boot() returns a promise that resolves once the visitor's session has started, or at once without user. It also still accepts the older snake_case config (platform_id, user_id, email, full_name, ...), and initialize() is a deprecated name for boot(). window.Hyper is a deprecated alias for window.Nimbia.

#With npm (coming soon)

Coming soon: the @nimbia/js package isn't published yet. Until then, use the script snippet above.

Once it's published, an app with a bundler can install the package instead of pasting the snippet:

bash
npm install @nimbia/js
typescript
import { loadNimbia } from "@nimbia/js";

const nimbia = await loadNimbia({
  platformId: "your-platform-id",
  user: { id: user.id, email: user.email, name: user.fullName },
});
await nimbia.startGuide("your-guide-id");

loadNimbia() adds the same script to the page once and returns the same object as window.Nimbia, with TypeScript types. With user, it resolves once the session has started.

#Who the visitor is

boot({ user }) and identify(user) take the visitor's details:

Field Type Required Description
email string Yes The visitor's email address
id string No Your ID for the visitor. Defaults to email
name string No Full name
createdAt string No When the visitor signed up, as an ISO 8601 date
locale string No Preferred language code, e.g. "en"
company object No { id, name, meta? }
meta object No Anything else to store on the visitor's record

#Logging out

When the visitor logs out, call shutdown(). It ends the session, clears what the widget stored, and removes it from the page, so the next visitor doesn't inherit the session.

javascript
async function handleLogout() {
  await Nimbia.shutdown("logout");
  // ... the rest of your logout logic
}

Pass a reason. A shutdown without one that interrupts a running guide is reported on our side as a possible integration problem.

When the next visitor logs in, call boot() again with their details. If a visitor logs in while the widget runs for an anonymous one, call identify() instead: it switches the running widget to the new visitor, which ends a running call.

#Apps with full page loads

A full page load ends a call: the widget loads again on the new page and the visitor has to start over. If your app loads a new page on most clicks, give it an outer page that only runs the widget, and let the widget show your app in an iframe on that page:

javascript
Nimbia.boot({
  platformId: "your-platform-id",
  user: { id: user.id, email: user.email },
  appFrame: {
    src: "/dashboard", // the first app page; defaults to this page's URL
    onLeaveApp: ({ lastAppUrl }) => {
      // The iframe went to another domain, such as a single sign-on page.
      location.href = lastAppUrl;
    },
  },
});

With appFrame, the widget adds a full-size iframe to the page and guides your app inside it. Page loads in your app reload only the iframe, so the call keeps running. The address bar follows the iframe's page, and the browser's back and forward buttons step through your app's pages.

To try this before changing your app, turn on the Nimbia browser extension for your app and switch on "Keep calls running when pages reload" in its popup. The extension shows your app the same way, in an iframe on your own domain.

If changing your app isn't an option, contact us about a hosted setup.

Option Type Description
src string The app page to open first. Defaults to the outer page's URL
onLeaveApp function Runs when the iframe opens a page on another domain, where Nimbia can't guide it. Gets { lastAppUrl }, the last page of your app the iframe showed

appFrame: true opens the outer page's own URL with no onLeaveApp.

Your side needs:

  • The outer page on the same domain as your app. The browser lets the widget read and guide the iframe only then.
  • A server that answers a top-level load of any app URL with the outer page, and the same URL inside the iframe with the app page. The address bar shows the app's URLs, so a reload or a shared link loads one of them at the top. Requests for the iframe carry Sec-Fetch-Dest: iframe; top-level loads carry Sec-Fetch-Dest: document.
  • X-Frame-Options: SAMEORIGIN or Content-Security-Policy: frame-ancestors 'self' on your app's pages, instead of DENY or 'none', so the browser lets your own outer page show them.
  • The snippet only on the outer page. A widget that loads inside the iframe stays off by itself, so a snippet on your app's pages does no harm.

The outer page can cover only the signed-in part of your app. Serve the sign-in pages as usual, and the outer page once the visitor is signed in.

The iframe may not navigate the outer page, so code that breaks out of frames (top.location = location) and links with target="_top" have no effect. Popups and new tabs work as usual.

#JavaScript API

The JavaScript API page lists every function, setting, action and event on window.Nimbia.

#Browser Compatibility

#Safari with Intelligent Tracking Prevention (ITP)

When your application (that includes the Nimbia widget) is embedded as an iframe on a different domain, Safari users with ITP enabled will experience session persistence issues. Sessions will not persist across page navigations, creating a new session on each page load.

Recommended solutions:

  • Use a Single Page Application (SPA) - no page navigations means no session loss
  • Avoid embedding your application in cross-domain iframes
  • Host your application on the same domain as the parent frame
  • See Safari ITP Limitations for detailed information

#Content security policy

If your site sends a Content-Security-Policy header, it must allow the widget to load and connect. This applies to the snippet and to the Nimbia browser extension. Add these sources:

  • script-src: https://*.nimbia.ai
  • style-src: 'unsafe-inline', the widget injects its own styles into the page
  • connect-src: https://*.nimbia.ai wss://*.nimbia.ai wss://*.livekit.cloud https://o4510939940519936.ingest.de.sentry.io

The LiveKit entry carries the voice and screen-sharing connection during a call. The Sentry entry carries the widget's error reports, so we can see when it breaks on your site. Call audio travels over WebRTC, which a content security policy does not cover, so no media-src entry is needed. A nonce on your scripts does not replace the script-src host entry, because the widget script is loaded from our host.

Without them the browser blocks the widget in stages: first the script, then its styles, then the calls it makes. The console names the blocked URL and the directive, for example Refused to connect ... because it violates the following Content Security Policy directive, and the extension shows "This site's security policy blocked the Nimbia script."

#Permissions policy

A call needs the visitor's microphone. If your HTML pages send a Permissions-Policy header that lists the microphone with an empty allowlist, the browser refuses the microphone without ever asking the visitor:

text
Permissions-Policy: geolocation=(), microphone=(), camera=()

microphone=() means no origin, not even your own page, may open the microphone. Neither the visitor's site settings nor the widget can override it. The widget then reports that the microphone was denied, and the call runs without the visitor's voice.

Allow the microphone for your own origin. The widget runs inside your page, not in an iframe, so self is enough:

text
Permissions-Policy: geolocation=(), microphone=(self), camera=()

To check, open your page, open the browser console and run:

js
document.featurePolicy.allowedFeatures().includes("microphone")

It prints false while the header blocks the microphone and true once it is fixed. The header itself is under Response Headers for the page request in the Network tab of the browser's developer tools.

#Redacting sensitive data

Add the class nim-no-capture to any element whose contents Nimbia should not see or record. It applies to the element and everything inside it. You can combine it with the classes the element already has:

html
<div class="patient-details nim-no-capture">
  <!-- Nimbia does not read or record anything in here -->
</div>

Only the class works. There is no data attribute or setting for this.

What the class does:

  • The page view sent to the AI agent keeps the element, but empties it and removes every attribute except class. The agent cannot read, point at or click anything inside it.
  • Call recordings show the element as an empty box of the same size. Nothing inside it is recorded, canvases included.

Because the agent sees an empty element, it cannot guide users through anything inside it.

What the class does not cover:

  • Text outside blocked elements is captured.
  • Form field values outside blocked elements are captured. Password fields are never captured.
  • The class attribute of a blocked element stays visible, so keep sensitive data out of class names.
  • Anything the user says out loud during a call is recorded as audio.

#Troubleshooting

If the widget doesn't show up:

  • Open the browser console and look for errors that start with [nimbia].
  • Check the platform UUID. Most settings are optional, but the widget won't load without a valid one.
  • Check that your page can load the widget script URL. A content security policy can block it, see Content security policy.
  • Check that you pass a user identifier for the visitor.
  • Check that the browser shows the microphone prompt when a call starts. If it never does, a permissions policy is blocking it, see Permissions policy.