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

# Verified Accounts quickstart

> Turn on Link with BotShield ID, add the widget after your sign-in, and verify the linked result on your server.

This guide takes you from a site key to a Verified Account that your server has verified. Work in **Development** first, with a `pk_test_...` site key.

## Before you start

Verified Accounts is part of a paid plan. See [pricing](https://botshield.ai/pricing).

You need:

* A site key whose allowed origins include your page. See [Keys and environments](https://docs.botshield.ai/console/keys-and-environments.md).
* A signed-in user on your side, and a **stable ID** for that user's account.

> [!WARNING]
>
> `platform-user-ref` must be an ID that never changes for the account, such as your internal user ID. It must not be an email address. BotShield refuses a value that contains `@` with `notarize_ref_must_be_stable`, because an email address can change and it identifies a person. BotShield stores the value only as a one-way hash.

## Steps

**1. Turn on Link with BotShield ID**

In the [BotShield Console](https://console.botshield.ai), open **Verified Accounts** and select **Set up Link with BotShield ID** in the page header. Add a surface in three steps:

1. **The surface.** Name the page where the chip goes, for example `Sign-in · app.example.com`, and pick the site key for it. One surface has one site key.
2. **The tag.** Copy the tag the Console shows.
3. **Turn it on.** In Production, the Console asks you to confirm with your Developer passkey.

The switch is the authority. The chip appears only while it is on. Your page markup cannot turn it on.

> [!NOTE]
>
> A gate can also link the person as they pass, through its **link** setting (`required`, `optional` or `never`). See [Linking at a gate](https://docs.botshield.ai/gate/link.md).

**2. Add the widget after your sign-in**

Show the chip only to a user you have just signed in or re-authenticated. The link says that the human confirming is the one using this account, so your own sign-in has to come first.

```html theme={null}
<script src="https://cdn.botshield.ai/sdk.js"></script>

<botshield-verify
  site-key="pk_test_..."
  notarize
  platform-user-ref="usr_8f31c2d9"
></botshield-verify>
```

| Attribute | Value |
| - | - |
| `site-key` | Your public site key, the one you picked for the surface. |
| `notarize` | Asks for the chip. It cannot turn the chip on while the Console switch is off. |
| `platform-user-ref` | A stable ID for the account on your platform. Never an email address. |

**3. The person links the account**

The widget shows the **Link with BotShield ID** chip, with **Tap to secure this account** under the title.

| Device | What happens on tap |
| - | - |
| Phone | BotShield opens and asks the person to confirm. |
| Desktop | BotShield opens in a new tab. The person can choose **Use your phone instead** to see a QR code and a 6-character code. |

The code lasts 5 minutes and works once. The person can scan the QR code, or type the code at `app.botshield.ai/link`.

In BotShield, the person sees your platform's name and confirms with a passkey. The chip on your page then reads **Account secured**.

**4. Handle the result on your page**

Listen for `botshield:success` and send the token to your server. `trusted` is `true` when the account is linked.

```html theme={null}
<script>
  const widget = document.querySelector('botshield-verify');

  widget.addEventListener('botshield:success', async (e) => {
    const { token, request_id, trusted } = e.detail;
    await fetch('/api/botshield/linked', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token, request_id }),
    });
  });

  widget.addEventListener('botshield:failure', (e) => {
    console.warn('BotShield:', e.detail.reason);
  });
</script>
```

Treat `trusted` in the browser as a hint. The decision belongs on your server.

**5. Verify on your server**

Send the token to `POST /sdk/verify-token`. The token lives 120 seconds and is good for one check. You can call without a key and send your organization ID as `audience`, or call with your secret API key as `Authorization: Bearer bs_…`, and BotShield then uses your key's organization as the audience.

```bash theme={null}
curl -s https://api.botshield.ai/operations/sdk/verify-token \
  -H "Content-Type: application/json" \
  -d '{"token":"eyJhbGciOiJFUzI1NiIsImtpZCI6...","audience":"org_..."}'
```

```json theme={null}
{
  "data": {
    "data": {
      "valid": true,
      "verdict": "pass",
      "replayed": false,
      "claims": {
        "request_id": "req_4b1f0c6e2a9d4e7f8a3b5c6d7e8f9a0b",
        "verified": true,
        "organization_id": "org_...",
        "audience": "org_...",
        "timestamp": "2026-10-20T17:04:05.000Z",
        "trusted": true,
        "trusted_since": "2026-10-20T17:04:05.000Z",
        "last_pass_at": "2026-10-20T17:04:05.000Z"
      }
    }
  }
}
```

Check all of these before you mark the account as linked:

1. `data.error` is absent and `valid` is `true`.
2. `organization_id` is your organization. Pass your organization ID as `audience`, or call with your API key, and BotShield checks this for you.
3. `request_id` is the one your page sent.
4. `trusted` is `true`.

Then store `trusted_since` against **your** user record. See [Token and webhooks](https://docs.botshield.ai/verified-accounts/token-and-webhooks.md) for local verification and for the events your endpoint receives.

**6. Store the handle**

Your webhook endpoint receives `account.secured` with the account's `handle`, an opaque value that starts with `OP_`. Store it beside your user. Every later account event carries it. See [Staying in sync](https://docs.botshield.ai/verified-accounts/staying-in-sync.md).

**7. See it in the Console**

Open **Verified Accounts**, then the **Registry** tab, with the **Development** toggle selected. The account appears by its handle, with the date it was linked.

The **Try it** tab runs the same chip against your Development key, with you as the customer. Use it to see each state before you ship.

## Server-to-server

If you create verification requests from your server instead of using the widget, pass `partner_user_ref` to `POST /sdk/create-verification-link` for a gate whose link setting is `optional` or `required`. The same rules apply: a stable ID that is not an email address. On an `optional` gate, `notarize: false` skips the offer for one request. `notarize: true` cannot turn the offer on while the gate's setting is `never`; the request is then an ordinary verification. A `required` gate refuses a request without `partner_user_ref` with `link_requires_user_ref`.

## Test the other paths

| To see | Do this |
| - | - |
| A Verified Account on return | Load the page again as the same user. The chip reads **Account secured**. |
| `already_trusted` | With the same BotShield ID, try to link a second account that has a different `platform-user-ref`. |
| `link_not_enabled` | Turn the surface's switch off, reload and tap the chip. The widget fires one `botshield:failure` with `link_not_enabled`, then shows the normal verify button. |
| The chip again | Unlink the account in the BotShield app, or select **Revoke** in the Registry. With the 3.3 widget the chip comes back without a reload. See [Staying in sync](https://docs.botshield.ai/verified-accounts/staying-in-sync.md). |
| A declined request | Select **Cancel** in BotShield when it asks. Your page receives `botshield:failure` with `reason: "declined"`. |

## Next steps

> [card] [Account lifecycle](https://docs.botshield.ai/verified-accounts/lifecycle.md)
>
> What happens after the link: status, unlink and link again.

> [card] [Server API](https://docs.botshield.ai/verified-accounts/server-api.md)
>
> Read the status and unlink from your server.


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