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

# BotShield Actions

> Key any act on your platform to a passkey tap, and get a signed token that names the act.

A **BotShield Action** asks for a passkey tap on one act on your platform: accepting a trade, claiming an offer, posting a listing. The person confirms on their phone, and your server receives a signed token that carries your own string for the act, as `claims.act`.

An action is not a gate. It has no schedule, no queue and no [Who may pass](https://docs.botshield.ai/gate/who-may-pass.md) levers. It always stands on an account: every tap links the person to the account you name, in the same confirmation, like a [`link: required`](https://docs.botshield.ai/gate/link) gate. So every action request needs your user ref.

There are two ways to ask for a tap:

* **On your page or from your server**, while the person is with you: the widget or `create-verification-link` with an `act`. This page covers it.
* **Through your agent**, when the person is elsewhere: your server puts the action in front of a person already linked to you, as a card in their BotShield app. See [Ask through an agent](https://docs.botshield.ai/actions/ask-through-an-agent.md).

Both give you the same token, with `claims.act`.

## Place an action

You place an action once, under a short **act key** such as `trade`. An action is live as soon as it is placed, and you can turn it off at any time.

> [tab] Console
>
> In the [BotShield Console](https://console.botshield.ai), select **Verified Accounts** in the sidebar, open the **BotShield Actions** tab and select **Place an action**. The list shows the actions placed in the environment selected in the header.
>
> | Field | What to enter |
> | - | - |
> | **Name** | What the person sees as the title of the tap, for example "Confirm trade". |
> | **Act key** | The key your acts start with, for example `trade`. Lower-case letters, digits, `_` and `-`, up to 40 characters. |
> | **Where it lives** (optional) | An `https://` link to the page where the act happens. |
>
> Select **Place action**. In Production, placing an action is confirmed with your Developer passkey.
>
> To stop an action, select **Turn off** on its row. Turning off takes effect at once and never asks for a passkey. **Turn on** in Production is confirmed with your Developer passkey. Only organization admins place and turn actions on or off.

> [tab] Gates API
>
> Use an API key with **Gates API access**. See [Gates API](https://docs.botshield.ai/gate/gates-api.md#authentication). The environment comes from the key.
>
> ```bash curl theme={null}
> curl -s https://api.botshield.ai/operations/gates-api/actions/create \
>   -H "Authorization: Bearer $BOTSHIELD_API_KEY" \
>   -H "Content-Type: application/json" \
>   -d '{"name":"Confirm trade","actKey":"trade","whereUrl":"https://tickets.example.com/trades"}'
> ```
>
> ```json theme={null}
> {
>   "data": {
>     "data": {
>       "action": {
>         "id": "6b0d2f4e-8a51-4c3e-9f7a-2d1c5e8b9a04",
>         "name": "Confirm trade",
>         "act_key": "trade",
>         "where_url": "https://tickets.example.com/trades",
>         "state": "live",
>         "environment": "production",
>         "placed_at": "2026-11-03T18:42:10.204Z",
>         "turned_off_at": null
>       }
>     }
>   }
> }
> ```

### Place an action with the API

| Operation | Request | Response |
| - | - | - |
| `POST /gates-api/actions/create` | `name` (1–60 characters), `actKey` (lower-case letters, digits, `_`, `-`, up to 40), `whereUrl` (optional, `https://`) | `{ action }` |
| `GET /gates-api/actions/list` | none | `{ actions: [...] }`, newest first, in the key's environment |
| `POST /gates-api/actions/set-state` | `actKey`, `state` (`live` or `off`) | `{ action }` |

An act key is unique within one environment. Turning an action off takes effect at once and is safe to repeat:

```bash curl theme={null}
curl -s https://api.botshield.ai/operations/gates-api/actions/set-state \
  -H "Authorization: Bearer $BOTSHIELD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"actKey":"trade","state":"off"}'
```

## The act string

Each request names the act with an **act string**. The part before the first `:` is the act key of a placed, live action. Everything after it is yours. BotShield never reads it. It signs the whole string into the token, and `verify-token` returns it exactly as you sent it, as `claims.act`.

```text theme={null}
trade:trd_7f3a:dbd67e121d8d1bf5f004d9d550fcb02945cea0b07542ba3aa10291b722b6e6a8
└─┬─┘ └──────────────────────────────┬──────────────────────────────────────┘
act key                     yours, returned verbatim
```

| Rule | Value |
| - | - |
| Characters | Letters, digits, `_`, `-`, `.` and `:`. No spaces. |
| Start | A letter or digit. |
| Length | Up to 128 characters. |
| Act key | The part before the first `:`, matched lower-case against your placed actions. A string with no `:` is the act key alone, for example `watch`. |

### Bind the terms

The token proves that a person confirmed this act string. Make the string specific enough that it can only mean one thing. When the person is agreeing to terms, put a hash of the terms in the act:

1. Your server builds the terms from its own records: who, what, how much.
2. It hashes them, for example SHA-256 over a fixed serialization, and puts the hash in the act.
3. When the token comes back, your server rebuilds the terms from its records, hashes them again, and compares the result with `claims.act`.

If the terms changed between the request and the tap, the hashes differ and you refuse the act.

> [!WARNING]
>
> Never trust a client-side event. The widget's `botshield:success` event and anything your page sends are claims made in the browser. Decide on your server, from the token, after `verify-token` answers valid (see [Server flow](#server-flow)).

## The label

`act_label` is what the person reads on the card in the BotShield app before they confirm, for example "Buy 2 GA for Dec 4 from sam\_k for \$180". The action's **Name** is the card's title.

* 1 to 90 characters.
* Sent only with an act. A label without an act is refused with `act_label_without_act`.
* Display only. It is **not** in the token. Bind the terms through the act, never through the label.

## Server flow

**1. Create a session**

Call `POST /sdk/create-session` with your API key, as for a gate. See [Verify on your server](https://docs.botshield.ai/gate/verify-on-your-server.md#pattern-b-server-to-server).

```bash curl theme={null}
curl -s https://api.botshield.ai/operations/sdk/create-session \
  -H "Authorization: Bearer $BOTSHIELD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"partner_user_id":"usr_48121"}'
```

**2. Create the request with the act**

Call `POST /sdk/create-verification-link` with the `bss_` grant token. Send `act`, `act_label` and `partner_user_ref`. Do not send `gate` or `scope`: an action is not a gate.

```bash curl theme={null}
curl -s https://api.botshield.ai/operations/sdk/create-verification-link \
  -H "Authorization: Bearer bss_5e1c…" \
  -H "Content-Type: application/json" \
  -d '{
    "act": "trade:trd_7f3a:dbd67e121d8d1bf5f004d9d550fcb02945cea0b07542ba3aa10291b722b6e6a8",
    "act_label": "Buy 2 GA for Dec 4 from sam_k for $180",
    "partner_user_ref": "usr_48121",
    "mode": "private"
  }'
```

```typescript TypeScript SDK theme={null}
// botshield-sdk 2.2.1 or later. grantToken is anchorGrantToken from census.createSession.
const link = await botshield.census.createVerificationLink(
  { grantTokenAuth: `Bearer ${grantToken}` },
  {
    act: "trade:trd_7f3a:dbd67e121d8d1bf5f004d9d550fcb02945cea0b07542ba3aa10291b722b6e6a8",
    actLabel: "Buy 2 GA for Dec 4 from sam_k for $180",
    partnerUserRef: "usr_48121",
    mode: "private",
  },
);
if (link.data.error) throw new Error(link.data.error.message);
```

| Request field | Description |
| - | - |
| `act` | The act string. |
| `act_label` | Optional. What the person reads. Up to 90 characters. |
| `partner_user_ref` | Your user ref for the person. Required, here or as `partner_user_id` on `create-session`. |
| `mode` | Send `private`, as for a gate. |

```json theme={null}
{
  "data": {
    "data": {
      "request_id": "req_8c2e5a1f9b3d4c6e7a8b9c0d1e2f3a4b",
      "web_url": "https://app.botshield.ai/verify?request_id=req_8c2e5a1f9b3d4c6e7a8b9c0d1e2f3a4b&mode=private",
      "deep_link": "botshield://verify?request_id=req_8c2e5a1f9b3d4c6e7a8b9c0d1e2f3a4b&mode=private",
      "expires_at": "2026-11-03T19:05:00.000Z",
      "gate_type": "human",
      "pushed_to_devices": 1,
      "trusted": true,
      "notarize": true,
      "organization": { "id": "org_…" }
    }
  }
}
```

`notarize: true` says the tap runs the linking confirmation. `trusted: true` says the user ref is already linked. When `pushed_to_devices` is greater than `0`, the request went to the person's phone as a push notification.

**3. Show the link and wait**

Show `web_url` as a QR code or a link, as for a gate. A request lives 5 minutes. When the person confirms, you get the token either way: the `return_url` receives `?token=…&request_id=…`, or poll `GET /verification/status?request_id=…` until `status` is `completed` and read `verification_token`. The `gate.human_verified` webhook also fires for an action tap; it carries `request_id`, `act` (exactly as you sent it) and, once linked, the person's `handle`. The webhook tells you it happened — still check the token with `verify-token` before you act on it.

**4. Verify the token once**

Call `POST /sdk/verify-token`. It answers `valid: true` once per token. See [Single use](https://docs.botshield.ai/gate/verify-on-your-server.md#single-use).

```bash curl theme={null}
curl -s https://api.botshield.ai/operations/sdk/verify-token \
  -H "Authorization: Bearer $BOTSHIELD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"token":"eyJhbGciOiJFUzI1NiIsImtpZCI6…"}'
```

```json theme={null}
{
  "data": {
    "data": {
      "valid": true,
      "verdict": "pass",
      "replayed": false,
      "claims": {
        "request_id": "req_8c2e5a1f9b3d4c6e7a8b9c0d1e2f3a4b",
        "verified": true,
        "organization_id": "org_…",
        "audience": "org_…",
        "timestamp": "2026-11-03T19:01:12.000Z",
        "nonce": "req_8c2e5a1f9b3d4c6e7a8b9c0d1e2f3a4b_1793732472000",
        "issued_at": 1793732472,
        "expires_at": 1793732592,
        "act": "trade:trd_7f3a:dbd67e121d8d1bf5f004d9d550fcb02945cea0b07542ba3aa10291b722b6e6a8",
        "trusted": true,
        "trusted_since": "2026-10-20T17:04:05.000Z",
        "last_pass_at": "2026-11-03T19:01:12.000Z"
      }
    }
  }
}
```

**5. Compare the act**

Accept the act only when all of these hold:

1. `valid` is `true`.
2. `claims.request_id` is the request you created for this person and this act.
3. `claims.act` is exactly the act string you expect, rebuilt from your own records.

## Widget

Put the act on `<botshield-verify>` instead of a gate. The widget sends `act` and `act-label` in place of `scope`, so leave `scope` off.

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

<botshield-verify
  site-key="pk_live_…"
  act="trade:trd_7f3a:dbd67e121d8d1bf5f004d9d550fcb02945cea0b07542ba3aa10291b722b6e6a8"
  act-label="Buy 2 GA for Dec 4 from sam_k for $180"
  platform-user-ref="usr_48121"
></botshield-verify>
```

| Attribute | Description |
| - | - |
| `act` | The act string. Up to 128 characters. |
| `act-label` | What the person reads. Up to 90 characters. Sent only with `act`. |
| `platform-user-ref` | Your user ref for the signed-in person. Required for an action. |

Load widget 3.0.15 or later (`/sdk.js` serves it). On success, send `detail.token` to your server and run steps 4 and 5 above. When BotShield refuses the request, `botshield:failure` carries the reason, the code and the message:

```json theme={null}
{
  "reason": "modal_create_failed",
  "code": "action_requires_user_ref",
  "message": "An action stands on a verified account — send your own user ref (platform-user-ref) with the request."
}
```

`reason` is `mobile_create_failed` on a phone. The `act` and `act-label` attributes, and `code` and `message` on failures, need widget 3.0.15 or later. Earlier versions ignore `act` and send the request without it. See [Widget versions](https://docs.botshield.ai/gate/widget-versions.md).

## Error codes

When you create the request, errors arrive as `data.error` with HTTP `200`:

| `code` | `statusCode` | Cause |
| - | - | - |
| `action_invalid` | 400 | The act string breaks the [rules](#the-act-string). |
| `action_not_found` | 400 | No action with this act key is placed in the key's environment. |
| `action_off` | 409 | The action is turned off. |
| `action_requires_user_ref` | 400 | No user ref was sent. |
| `action_not_a_gate` | 400 | The request sent both a gate (`gate` or `scope`) and an act. Send one. |
| `act_label_without_act` | 400 | `act_label` was sent without `act`. |

An `act` longer than 128 characters, or an `act_label` that is empty or longer than 90, fails input validation with HTTP `400` before the operation runs.

BotShield checks the action again when the person confirms. If the action was turned off during the tap, the request ends `failed` with `reason: "action_off"`, and no token is issued. The `gate.unavailable` webhook then carries `failure_code: "action_off"` and the `act`.

The Gates API action operations return `invalid_key`, `gates_scope_required`, `org_not_found` and `internal` as described in [Gates API](https://docs.botshield.ai/gate/gates-api.md#error-codes), plus `action_key_taken` (409) from `actions/create` and `action_not_found` (404) from `actions/set-state`.

## Example: a two-party trade

A ticket marketplace lets fans trade tickets. sam\_k (`usr_20417`) offers 2 GA tickets for the December 4 show to river\_j (`usr_48121`) for \$180. The trade goes through only when both people confirm the same terms with a passkey.

**1. Place the action once**

Place an action named "Confirm trade" with the act key `trade`.

**2. Build the act from the terms**

Your server records the trade as `trd_7f3a` and hashes its terms:

```typescript theme={null}
import { createHash } from "node:crypto";

type Trade = {
  trade_id: string; seller: string; buyer: string;
  event: string; items: string; price: number; currency: string;
};

// Build the act from your own record. Fixed key order: the same terms always give the same hash.
export function tradeAct(t: Trade): string {
  const terms = JSON.stringify({
    trade_id: t.trade_id, seller: t.seller, buyer: t.buyer,
    event: t.event, items: t.items, price: t.price, currency: t.currency,
  });
  const hash = createHash("sha256").update(terms).digest("hex");
  return `trade:${t.trade_id}:${hash}`;
}

const trade: Trade = {
  trade_id: "trd_7f3a", seller: "usr_20417", buyer: "usr_48121",
  event: "dec-04", items: "2 GA", price: 18000, currency: "USD",
};

tradeAct(trade);
// "trade:trd_7f3a:dbd67e121d8d1bf5f004d9d550fcb02945cea0b07542ba3aa10291b722b6e6a8"
```

**3. Ask each party**

Create one request per person, with the same act, each person's own user ref, and a label written for them:

| Party | `partner_user_ref` | `act_label` |
| - | - | - |
| Seller | `usr_20417` | "Sell 2 GA for Dec 4 to river\_j for \$180" |
| Buyer | `usr_48121` | "Buy 2 GA for Dec 4 from sam\_k for \$180" |

Store each `request_id` against the party it was created for.

**4. Check each token once**

For each completed request, call `verify-token` and keep the result:

```typescript theme={null}
async function confirmParty(tradeId: string, party: "seller" | "buyer", token: string) {
  const trade = await db.trades.get(tradeId);
  const res = await fetch("https://api.botshield.ai/operations/sdk/verify-token", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.BOTSHIELD_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ token }),
  });
  const body = await res.json();
  if (body.data?.error) throw new Error(body.data.error.message);

  const { valid, reason, claims } = body.data.data;
  if (!valid) return { ok: false, reason };

  if (claims.request_id !== trade.requests[party]) return { ok: false, reason: "wrong request" };
  if (claims.act !== tradeAct(trade)) return { ok: false, reason: "terms changed" };

  await db.trades.markConfirmed(tradeId, party, claims.request_id);
  return { ok: true };
}
```

**5. Settle**

Settle the trade when both the seller and the buyer are confirmed. If either party's token fails a check, or the terms changed after the request, refuse the trade and ask both again with a new act.

## Next steps

> [card] [Verify on your server](https://docs.botshield.ai/gate/verify-on-your-server.md)
>
> Token claims and single use.

> [card] [Secured Accounts](https://docs.botshield.ai/secured-accounts/overview.md)
>
> What the account link gives you.

> [card] [Gates API](https://docs.botshield.ai/gate/gates-api.md)
>
> Place gates and actions from your server.

> [card] [Web component](https://docs.botshield.ai/gate/web-component.md)
>
> Every widget attribute and event.


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