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

# Server API

> Read a Verified Account's status, list your registry, and unlink an account from your server with your API key.

Your server can read and end the Verified Accounts on your platform with its secret API key. Three operations cover it:

| Operation | SDK method (2.2.0 and later) | What it does |
| - | - | - |
| `GET /secured-accounts/status` | `securedAccounts.getStatus` | Is this account linked right now? Answers with no tap. |
| `GET /secured-accounts/list` | `securedAccounts.list` | Your registry: one row per link, by handle. |
| `POST /secured-accounts/revoke` | `securedAccounts.revoke` | Unlink one account. |

The paths and the SDK namespace keep the older name, `secured-accounts` and `securedAccounts`. They are the Verified Accounts operations.

None of these operations returns who the person is: no name, email, device, BotShield ID, or your own reference back. Your reference goes in, a handle comes out.

## Authentication and environment

Send your **server API key** as `Authorization: Bearer <key>`. Create it in the Console under **Settings → Developer Tools → API Keys**.

| Key | Reaches |
| - | - |
| `bs_dev_…` | Development links only |
| `bs_production_…` | Production links only |

The environment always comes from the key. It is never an input, so a development key can never read or unlink a production link. Site keys (`pk_…`), grant tokens (`bss_…`) and agent keys (`bs_agent_…`) are refused with `api_key_required`.

> [!WARNING]
>
> The API key is a secret. Call these operations from your server only. In the browser, the [widget](https://docs.botshield.ai/verified-accounts/widget.md) shows the live state with your public site key.

## Status

`GET /secured-accounts/status` answers whether an account is linked right now. It is the same answer the widget's live connection gives the page, so your server and the page always agree.

| Query parameter | Required | Description |
| - | - | - |
| `platform_user_ref` | Yes | Your stable user ID for the account, the same value you pass to the widget. 1 to 512 characters. An email address is refused with `notarize_ref_must_be_stable`. |

```bash curl theme={null}
curl -s "https://api.botshield.ai/operations/secured-accounts/status?platform_user_ref=usr_8f31c2d9" \
  -H "Authorization: Bearer $BOTSHIELD_API_KEY"
```

```typescript SDK theme={null}
import { BotShield } from "botshield-sdk";

const botshield = new BotShield({ serverURL: "https://api.botshield.ai/operations" });
const key = { apiKeyAuth: `Bearer ${process.env.BOTSHIELD_API_KEY}` };

const res = await botshield.securedAccounts.getStatus(key, { platformUserRef: "usr_8f31c2d9" });
if (res.data.error) throw new Error(res.data.error.message);

const linked = res.data.data?.state !== "none";
```

```json theme={null}
{ "data": { "data": { "state": "fresh", "fresh_until": "2026-11-10T18:04:05.000Z" } } }
```

| `state` | Meaning |
| - | - |
| `none` | Not linked, or no longer linked. `fresh_until` is `null`. |
| `fresh` | Linked, with a recent passkey confirmation. |
| `stale` | Linked. The latest passkey confirmation is older than `fresh_until`. |

`fresh` and `stale` both mean the account is linked. Use `stale` only if you want to ask for a new passkey confirmation, for example with a gate.

If BotShield cannot read the state, the call returns `data.error` with `statusCode` 503 and `code: "presence_unavailable"`. It never guesses a state. Try again shortly.

## List

`GET /secured-accounts/list` returns your registry for the key's environment, newest first.

| Query parameter | Required | Description |
| - | - | - |
| `platform_user_ref` | No | Exact match on one of your user IDs. Use it to find the handle for one user. |
| `status` | No | `active` or `unlinked`. Leave it out for both. |
| `limit` | No | 1 to 100. Default 25. |
| `offset` | No | Default 0. |

```bash curl theme={null}
curl -s "https://api.botshield.ai/operations/secured-accounts/list?status=active&limit=50" \
  -H "Authorization: Bearer $BOTSHIELD_API_KEY"
```

```typescript SDK theme={null}
const res = await botshield.securedAccounts.list(key, { status: "active", limit: 50 });
if (res.data.error) throw new Error(res.data.error.message);

for (const row of res.data.data?.rows ?? []) {
  console.log(row.handle, row.securedAt);
}
```

```json theme={null}
{
  "data": {
    "data": {
      "rows": [
        {
          "handle": "OP_QDhs65484684",
          "status": "active",
          "secured_at": "2026-10-20T17:04:05.000Z",
          "unlinked_at": null,
          "unlinked_by": null,
          "unlink_reason": null,
          "surface_id": "6f1c…"
        }
      ],
      "total": 1,
      "environment": "production",
      "limit": 50,
      "offset": 0
    }
  }
}
```

| Field | Description |
| - | - |
| `handle` | The account's opaque handle, `OP_` followed by 12 letters or digits. The value `account.secured` and `account.unlinked` carry. |
| `status` | `active` or `unlinked`. |
| `secured_at` | When the account was linked. |
| `unlinked_at`, `unlinked_by`, `unlink_reason` | Set when `status` is `unlinked`. `unlinked_by` is `human` or `platform`. `unlink_reason` is `human`, `platform` or `credential_changed`. |
| `surface_id` | The Link with BotShield ID surface the account was linked on. `null` for a link made at a gate. |
| `total` | The number of rows that match, across all pages. |

## Revoke (unlink)

`POST /secured-accounts/revoke` ends one link. It does what **Revoke** does in the Console registry, from code.

| Body field | Required | Description |
| - | - | - |
| `platform_user_ref` | One of the two | Your user ID for the account. |
| `handle` | One of the two | The account's handle. |
| `reason` | No | `platform` (the default) or `credential_changed`. Send `credential_changed` when a sign-in detail changed on your side. |

Send exactly one of `platform_user_ref` and `handle`.

```bash curl theme={null}
curl -s https://api.botshield.ai/operations/secured-accounts/revoke \
  -H "Authorization: Bearer $BOTSHIELD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"platform_user_ref": "usr_8f31c2d9", "reason": "credential_changed"}'
```

```typescript SDK theme={null}
// A user changed their password: end the link. They link again behind your own re-auth.
const res = await botshield.securedAccounts.revoke(key, {
  platformUserRef: user.id,
  reason: "credential_changed",
});
if (res.data.error) throw new Error(res.data.error.message);

console.log(res.data.data?.revoked); // false when there was no active link
```

```json theme={null}
{
  "data": {
    "data": {
      "success": true,
      "revoked": true,
      "handle": "OP_QDhs65484684",
      "unlinked_at": "2026-11-10T09:12:40.000Z",
      "unlink_reason": "credential_changed"
    }
  }
}
```

When `revoked` is `true`, BotShield:

* marks the link **Unlinked · by you** in the registry,
* sends `account.unlinked` to your webhook endpoints with `by: "platform"` and the `reason`,
* removes your company's agents for that person, in the same environment,
* moves any open widget for the account to `none`, which shows the chip again, and&#x20;
* tells the person in the BotShield app.

Revoke is safe to repeat. An account that is not linked, already unlinked, or linked only in the other environment returns `success: true` with `revoked: false` and changes nothing. No passkey step-up applies: the server key acts for your platform. The Console's **Revoke** button asks for a Developer passkey in Production because a person is clicking it.

### When to revoke

| Situation | `reason` |
| - | - |
| The email, password, phone number or MFA on the account changed | `credential_changed` |
| The account was closed or deleted on your side | `platform` |
| The account changed hands | `platform` |
| Your user asks you to remove the link | `platform` |

## Errors

Errors arrive as `data.error` with HTTP 200, like every BotShield operation. Branch on `statusCode` and `code`.

| `statusCode` | `code` | Cause |
| - | - | - |
| 401 | `api_key_required` | No key, or a key that is not a server API key. |
| 401 | `invalid_api_key` | The key is invalid, expired or revoked. |
| 403 | `partner_not_found` | No BotShield company is set up for this key. |
| 400 | `presence_requires_user_ref` | Status: `platform_user_ref` is missing. |
| 400 | `notarize_ref_must_be_stable` | Status: `platform_user_ref` looks like an email address. |
| 400 | `invalid_platform_user_ref` | `platform_user_ref` is empty or longer than 512 characters. |
| 400 | `invalid_status` | List: `status` is not `active` or `unlinked`. |
| 400 | `ref_or_handle_required` | Revoke: send exactly one of `platform_user_ref` and `handle`. |
| 400 | `invalid_handle` | Revoke: the handle is not `OP_` followed by 12 letters or digits. |
| 400 | `invalid_reason` | Revoke: `reason` is not `platform` or `credential_changed`. |
| 503 | `presence_unavailable` | Status: the state could not be read. Retry. |
| 500 | `secured_accounts_unavailable`, `revoke_failed` | A temporary failure. Retry. |

See [Errors](https://docs.botshield.ai/api-reference/errors.md) for the envelope and a typed helper.

## Next steps

> [card] [Account lifecycle](https://docs.botshield.ai/verified-accounts/lifecycle.md)
>
> Every state, and what moves an account between them.

> [card] [Staying in sync](https://docs.botshield.ai/verified-accounts/staying-in-sync.md)
>
> Combine the live widget, webhooks and these calls.

> [card] [API reference](https://docs.botshield.ai/api-reference/overview.md)
>
> The generated reference for every operation.

> [card] [TypeScript SDK](https://docs.botshield.ai/sdk/typescript.md)
>
> `client.securedAccounts.*` and the rest of the client.


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