> ## 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.

# Secured Accounts quickstart

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

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

## Before you start

Secured 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](/console/keys-and-environments).
* 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.
</Warning>

## Steps

<Steps>
  <Step title="Turn on Link with BotShield ID">
    In the [BotShield Console](https://console.botshield.ai), open **Secured Accounts**. On the **Link with BotShield ID** tab, select **Set up Link with BotShield ID** and follow the 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 Console passkey.

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

    <Note>
      You can also offer the chip on a Human Gate. Open **BotShield Gate**, select the gate, and turn on **Link with BotShield ID** in its **Configuration** card. Then place the widget with `scope` set to the gate's key, as on the [Widget reference](/secured-accounts/widget).
    </Note>
  </Step>

  <Step title="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. |
  </Step>

  <Step title="The person secures 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**.
  </Step>

  <Step title="Handle the result on your page">
    Listen for `botshield:success` and send the token to your server. `trusted` is `true` when the account is secured.

    ```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/secured', {
          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.
  </Step>

  <Step title="Verify on your server">
    Send the token to `POST /sdk/verify-token`. The call needs no API key. The token lives 120 seconds.

    ```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 secured:

    1. `data.error` is absent and `valid` is `true`.
    2. `organization_id` is your organization. Pass your organization ID as `audience` 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](/secured-accounts/token-and-webhooks) for local verification and for the events your endpoint receives.
  </Step>

  <Step title="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](/secured-accounts/staying-in-sync).
  </Step>

  <Step title="See it in the Console">
    Open **Secured Accounts**, then the **Registry** tab, with the **Development** toggle selected. The account appears by its handle, with the date it was secured.

    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.
  </Step>
</Steps>

## 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 Human Gate whose **Link with BotShield ID** switch is on. The same rules apply: a stable ID that is not an email address. `notarize: false` skips the offer for one request. `notarize: true` cannot turn the offer on while the gate's switch is off. The request is then an ordinary verification.

## Test the other paths

| To see | Do this |
| - | - |
| A secured 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 secure 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](/secured-accounts/staying-in-sync). |
| A declined request | Select **Cancel** in BotShield when it asks. Your page receives `botshield:failure` with `reason: "declined"`. |


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