---
title: "Browser SDK"
description: "Optional - the Quickstart is complete on its own. This widget wires the same flow to a button and holds no secret; HMAC verification stays on your server."
source: "/docs/browser-sdk"
updated: "2026-09-24"
---

# Browser SDK

Optional - the Quickstart is complete on its own. This widget wires the same flow to a button and holds no secret; HMAC verification stays on your server.

## 1. Install the widget

Install it from npm, or load the IIFE build from jsDelivr - it self-attaches to `window.Helix`:

*npm*

```bash
npm install @helix-id/browser
```

*CDN*

```html
<script src="https://cdn.jsdelivr.net/npm/@helix-id/browser@0/dist/helix.iife.js"></script>
```

## 2. Open the flow

```js
import { Helix } from "@helix-id/browser";
// Recommended. Full-page navigation - nothing to block.
document.querySelector("#verify").addEventListener("click", () => {
  Helix.open({
    mode: "redirect",
    startUrl: "/helix/start", // your backend - returns { url }
    onError: (err) => console.error(err),
  });
});
```

## Options

| Option        | Type                               | Description                                                                                                                                                                                                                            |
| ------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mode`        | `"popup" \| "embed" \| "redirect"` | Defaults to `popup`; pass `redirect` explicitly. Redirect navigates away, so only `onError` can fire, before the navigation.                                                                                                           |
| `container`   | `string \| Element`                | Embed only, and required there. CSS selector or DOM element the iframe mounts into - give it a height, the iframe is `100%` of it. Embed mode returns a `{ destroy() }` handle so you can tear it down on unmount.                     |
| `startUrl`    | string                             | Your endpoint returning `{ url }`. Defaults to `/helix/start`.                                                                                                                                                                         |
| `callbackUrl` | string                             | Popup and embed. Your endpoint verifying the signed callback. Defaults to `/helix/callback`.                                                                                                                                           |
| `onSuccess`   | `(result) => void`                 | Popup and embed. Called with `{ status, statusCode, logId, enrollmentId? }` on a verified result.                                                                                                                                      |
| `onError`     | `(error) => void`                  | Called with `{ status: "error", message }` when the SDK itself fails - **and with the full callback result** (`{ status: "failed", statusCode, ... }`, no `message`) on any non-verified verdict. Branch on `statusCode`, not `message`. |
| `onExit`      | `() => void`                       | Popup only. Called when the user closes the popup early, and once if the popup is blocked - the SDK then falls back to a full-page redirect.                                                                                           |

There is no `baseUrl` option - the widget derives the Helix origin from the `url` your server returns from `createAuthUrl()`, so it always targets the environment your server is configured for.

> **WARN - Embed mode: three things that break it**
>
> The iframe is sized `100%` of your container - a container with no height renders an invisible widget and no error.
> The parent page must be HTTPS and must not block `microphone` in its own `Permissions-Policy`: the SDK delegates the
> permission via `allow`, but it cannot grant what the top-level document forbids. And an embed that sees no completion
> message within 10 minutes tears its iframe down and calls `onError`.

## Signature check before redirect

The widget checks that the URL from your `startUrl` endpoint carries a `sig` param shaped like an HMAC-SHA256 digest (64 lowercase hex chars). If it is missing or malformed, the widget calls `onError` with `{ status: "error", message }` instead of opening the popup or mounting the iframe.

The widget holds no secret, so this is a shape check, not cryptographic verification - it catches a misconfigured backend or an obviously rewritten URL, and nothing more. The real guarantees stay server-side: `verifyCallback()` on the way back, and `verifyAuthUrlSignature()` if you need to re-check a URL that crossed a service boundary.

> **WARN - Do not rewrite the URL**
>
> The widget no longer overrides `sdk_origin` - your backend now signs it into the URL via the `origin` option. Editing
> any query param client-side invalidates `sig` and the flow is rejected.


## Sitemap

See the full [sitemap](https://helix.id/sitemap.md) for all pages.
