> ## Documentation Index
> Fetch the complete documentation index at: https://docs.proofage.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Browser SDK

> Open the verification over your page with a few lines of JavaScript and get the result back.

The browser SDK opens ProofAge's verification widget on your page, in a modal or a new tab, and tells your page when the person is done. The decision itself reaches your backend by [webhook](/integration/webhooks).

## Load it

```html theme={null}
<script src="https://app.proofage.net/sdk-build/kyc-loader.js"></script>
```

The script defines `window.KycService` and nothing else. Load it from this URL: the SDK finds the API from the address it was loaded from. A copy you host yourself must pass `apiUrl` to `init`.

## Initialise

```js theme={null}
window.KycService.init({
  apiKey: 'pk_test_...',
  language: 'de',
  openInNewTab: false,
});
```

| Option | Required | Description |
| - | - | - |
| `apiKey` | Yes | Your workspace's public key (`pk_test_…` or `pk_live_…`). It is safe in the page; the secret key never is. |
| `language` | No | The widget's language: `cs`, `da`, `de`, `en`, `es`, `fr`, `id`, `it`, `lt`, `hu`, `nl`, `no`, `pl`, `pt-BR`, `pt-PT`, `ro`, `fi`, `sv`, `tr`, `el`, `bg`, `ru`, `hi`, `th`, `ja`, `zh-CN`, `zh-TW`, `ko`. Without it the widget uses the person's browser language, then English. |
| `openInNewTab` | No | `true` opens the widget in a new tab, `false` (default) in a modal over your page. |
| `metadata` | No | Sent only with a session the SDK creates itself (see below). It comes from the browser, so never trust it on your backend. |
| `apiUrl` | No | Only when the script is not loaded from the URL above. |

`init` throws synchronously when the configuration is invalid (for example a missing `apiKey`).

## Start

**Recommended: a session your backend created.** Your backend calls `POST /v1/verifications` with a signed request carrying your `external_id` (see the [Quick Start](/getting-started/quick-start)) and returns the session's `url` to the page:

```js theme={null}
window.KycService.start({ verificationUrl: url });
```

**Quick: a session created from the browser.** Without a URL, the SDK creates the session itself with the public key:

```js theme={null}
window.KycService.start();
```

Such a session cannot carry `external_id` or `callback_url`: the API honours those only on a signed request. Use it to try the widget, not to verify your users.

Call `start()` from a click handler. In new-tab mode the SDK opens the tab before anything else so that pop-up blockers let it through, which only works if nothing was awaited before `start()` in that handler: have your backend's session URL ready before the click (Safari in particular blocks a tab opened after an `await`). In modal mode fetching the URL inside the handler is fine. If the browser blocks it anyway, the SDK shows the person a link to open the widget themselves; that tab cannot talk back to your page, so neither `onComplete` nor `onClose` fires for it.

Calling `start()` again, or `close()`, closes whatever the previous call opened, including a session it was still creating.

## Callbacks

```js theme={null}
window.KycService.onComplete(function (result) { /* result.verificationId */ });
window.KycService.onClose(function () { /* the person left early */ });
window.KycService.onError(function (error) { /* error.code, error.message, error.recoverable */ });
```

### onComplete

Fires once, when the person reaches the widget's final screen. `result.verificationId` is the verification's `id`.

It does **not** tell you whether the person was approved or declined: the widget shows the same final screen either way. Ask your backend, which learns the outcome from the webhook or `GET /v1/verifications/{id}`.

It does not fire for:

* a session waiting in manual review: the person sees a processing screen until a decision is made;
* a session the person finished on their phone after scanning the QR code: the modal on the computer fires it when it catches up;
* expired or abandoned sessions.

### onClose

Fires when the person leaves before the final screen: the close button, a click outside the modal, the Escape key, or closing the tab in new-tab mode. It does not fire after `onComplete`, nor when your own code calls `close()`.

### onError

`error.recoverable` is `false` when trying again will not help.

| Code | When |
| - | - |
| `INVALID_CONFIG`, `MISSING_API_KEY`, `INVALID_API_URL` | `init` received an invalid configuration, or no API could be found for a self-hosted copy of the script. |
| `NOT_INITIALIZED` | `start` was called before `init`. |
| `INVALID_VERIFICATION_URL` | `verificationUrl` is not an `http(s)` URL. |
| `INSECURE_CONTEXT` | The page is not served over HTTPS, and `start` would create a session or open the modal. Not recoverable. |
| `NETWORK_ERROR`, `API_TIMEOUT`, `API_UNAVAILABLE` | The API could not be reached while creating a session. |
| `UNAUTHORIZED`, `FORBIDDEN` | The API rejected the key. Not recoverable when the key is invalid. |
| `RATE_LIMITED` | Too many requests; try again later. |
| `WORKSPACE_SUSPENDED`, `PAYMENT_REQUIRED` | The workspace cannot create sessions. Not recoverable. |
| `START_FAILED`, `INVALID_RESPONSE`, `INTERNAL_ERROR`, `UNKNOWN_ERROR` | Anything else went wrong while starting, or your own callback threw. |
| `WIDGET_CREATION_FAILED`, `DOM_MANIPULATION_FAILED` | The modal could not be added to the page. |

`UNSUPPORTED_BROWSER` never reaches `onError`: in a browser without `fetch` or modern JavaScript, the script throws it as it loads, before `init` runs, and `window.KycService` is not defined.

## Closing

`window.KycService.close()` removes the widget from your page (which stops the camera) or closes the tab. The session stays open: a later `start()` with the same `verificationUrl` continues where the person left off.

## After the final screen

About 3 seconds after the final screen, the SDK sends your whole page to the session's redirect address, after calling `onComplete`. That address is the `callback_url` your backend passed when creating the session, or else the workspace's **Redirect URL** (console: Workspace → Integration URLs). A new workspace's Redirect URL points at a ProofAge page. Set it to a page of yours, or clear it to keep the person on your page and handle `onComplete` yourself. Without a redirect address the modal stays on the final screen until the person closes it.

`callback_url` is a browser redirect, not a webhook address. Webhooks go to the workspace's webhook URL.

## Security

* Messages from the widget are accepted only from the widget's own origin.
* `onComplete` runs in the person's browser, where anyone can call it. Never unlock anything from it: wait for your backend to confirm the outcome.
* `external_id` can only be set by your backend, on a signed request. See [Authentication](/getting-started/api-authentication#keys).
* The page must be served over HTTPS to create a session from the browser or to open the modal. Opening a backend-created session in a new tab also works on plain HTTP.

## Test mode

With a `pk_test_` key the submission is not analysed: it waits in `review` and the person sees a processing screen. Set the outcome in the console (or with the MCP tool `set-test-verification-outcome`); the widget then shows the final screen and `onComplete` fires. See [Sandbox and test data](/integration/sandbox-testing).

## Full example

```html theme={null}
<script src="https://app.proofage.net/sdk-build/kyc-loader.js"></script>
<script>
  window.KycService.init({
    apiKey: 'pk_test_...',
    openInNewTab: false,
    language: 'en',
  });

  // The person reached the final screen. This is not the outcome:
  // your backend gets that from the webhook or GET /v1/verifications/{id}.
  window.KycService.onComplete(function (result) {
    console.log('Finished verification', result.verificationId);
  });

  // The person left before finishing.
  window.KycService.onClose(function () {
    console.log('Verification closed');
  });

  window.KycService.onError(function (error) {
    console.error('Verification error', error.code, error.message);
  });

  async function startVerification() {
    // Your backend creates the session with a signed
    // POST /v1/verifications (with external_id) and returns its url.
    const { url } = await fetch('/your-backend/proofage-session', { method: 'POST' }).then((r) => r.json());
    window.KycService.start({ verificationUrl: url });
  }
</script>

<button onclick="startVerification()">Verify</button>
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.