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

# Gates API

> Place, activate, close and cancel gates from your server with your secret API key.

The Gates API lets your server do what the Console's **BotShield Gates** tab does: place a gate with its settings, activate it, read it, extend its window, close it and cancel it. Use it when you open a gate per event, for example one gate for each presale or show. The same API key also places and runs [BotShield Actions](https://docs.botshield.ai/actions/overview.md).

All calls go to `https://api.botshield.ai/operations/gates-api/…`. Like every BotShield operation, they answer HTTP `200` with a `data` envelope. A success is under `data.data`. A refusal is under `data.error`, with `message`, `statusCode` and a machine `code`. Check `data.error` first. See [Errors](https://docs.botshield.ai/api-reference/errors.md).

> [!NOTE]
>
> The [TypeScript SDK](https://docs.botshield.ai/sdk/typescript.md) covers the Gates API from `botshield-sdk` 2.2.1, as `botshield.gates.*` and `botshield.gates.actions.*`. The SDK takes camelCase fields and returns camelCase fields, and it does not unwrap the envelope: read `result.data.data`, and check `result.data.error` first.
>
> ```typescript theme={null}
> import { BotShield } from "botshield-sdk";
>
> // The SDK's first server is localhost: always set serverURL.
> const botshield = new BotShield({ serverURL: "https://api.botshield.ai/operations" });
>
> const result = await botshield.gates.create(
>   { gatesKeyAuth: `Bearer ${process.env.BOTSHIELD_API_KEY}` },
>   { key: "presale-dec-04", name: "Presale · Dec 4", preset: "presale", activate: true },
> );
> if (result.data.error) throw new Error(result.data.error.message);
> console.log(result.data.data?.gate.status);
> ```

## Authentication

Send a secret API key as a Bearer token: `bs_dev_…` or `bs_production_…`. The key must have **Gates API access**.

**1. Open your API keys**

In the [BotShield Console](https://console.botshield.ai), go to **Settings → Developer Tools** and select the **API Keys** tab.

**2. Switch on Gates API access**

On the key's row, check **Gates API access**. The row then shows a **Gates API** badge. On a Production key, you confirm with your Developer passkey before the access is granted.

The key decides two things, and the request body cannot change either of them:

* **Organization.** Every call acts on the key's organization.
* **Environment.** A `bs_dev_` key places and reads Development gates. A `bs_production_` key places and reads Production gates.

Every call is written to your audit log, and a gate placed with the API records which key placed it. In the Console's **Activity Logs** the entries read, for example, "Gate activated with the Gates API".

## Operations

| Operation | Method | What it does |
| - | - | - |
| [`gates-api/create`](#create-a-gate) | `POST` | Place a gate with its settings. Optionally activate it in the same call. |
| [`gates-api/activate`](#activate-a-gate) | `POST` | Make a Draft gate Active. |
| [`gates-api/extend`](#extend-a-gate) | `POST` | Move a gate's closing time later. |
| [`gates-api/close`](#close-a-gate) | `POST` | Stop a gate issuing passes. Its log is kept. |
| [`gates-api/cancel`](#cancel-a-gate) | `POST` | Remove a gate that nobody has passed. |
| [`gates-api/get`](#get-a-gate) | `GET` | Read one gate by key. |
| [`gates-api/list`](#list-gates) | `GET` | List your Draft and Active gates. |
| `gates-api/actions/create`, `actions/list`, `actions/set-state` | `POST`, `GET`, `POST` | Place and run [BotShield Actions](https://docs.botshield.ai/actions/overview.md#place-an-action-with-the-api). |
| `gates-api/actions/request`, `actions/request-status` | `POST`, `GET` | Put one action in front of a linked person on behalf of your agent, and collect the token. See [Ask through an agent](https://docs.botshield.ai/actions/ask-through-an-agent.md). |

Request bodies use camelCase field names (`gateType`, `destinationUrl`). Responses use snake\_case (`gate_type`, `destination_url`), except inside `schedule`, which keeps `opensAt` and `closesAt`.

## Create a gate

`POST /gates-api/create` places a gate as a **Draft**, with its first policy document. Send `activate: true` to make it live in the same call.

```bash curl theme={null}
curl -s https://api.botshield.ai/operations/gates-api/create \
  -H "Authorization: Bearer $BOTSHIELD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "presale-dec-04",
    "name": "Presale · Dec 4",
    "message": "Presale for fans who signed up.",
    "preset": "presale",
    "policy": {
      "schedule": {
        "opensAt": "2026-12-01T10:00:00-08:00",
        "closesAt": "2026-12-02T10:00:00-08:00"
      }
    },
    "destinationUrl": "https://tickets.example.com/presale/dec-04",
    "activate": true
  }'
```

| Request field | Type | Description |
| - | - | - |
| `key` | string | The gate key: what your page sends as `scope` and your server sends as `gate`. Lower-case letters, digits, `_` and `-`, starting with a letter or digit, up to 64 characters. Unique among your Draft and Active gates in the key's environment. |
| `name` | string | A label for your team, 1–120 characters. |
| `message` | string, optional | The gate's line to people, shown when they arrive. Up to 90 characters. |
| `gateType` | `human` or `age` | Default `human`. |
| `ageThreshold` | `13`, `18` or `21` | Required when `gateType` is `age`. |
| `preset` | string, optional | Pre-fills the policy document. See [Presets](#presets). |
| `policy` | object, optional | The [policy document](#the-policy-document). Its fields override the preset's. |
| `destinationUrl` | string, optional | Where the gate sends a person after they pass. `https://` only, up to 2048 characters. |
| `activate` | boolean | Default `false`. `true` activates the gate in the same call. |

```json theme={null}
{
  "data": {
    "data": {
      "gate": {
        "key": "presale-dec-04",
        "name": "Presale · Dec 4",
        "message": "Presale for fans who signed up.",
        "status": "active",
        "state": "scheduled",
        "environment": "production",
        "destination_url": "https://tickets.example.com/presale/dec-04",
        "policy": {
          "preset": "presale",
          "gate_type": "human",
          "age_threshold": null,
          "signed_up": true,
          "link": "required",
          "passes_per_person": { "limit": 2, "window": "ever" },
          "real_days_at_least": 0,
          "schedule": {
            "opensAt": "2026-12-01T10:00:00-08:00",
            "closesAt": "2026-12-02T10:00:00-08:00"
          },
          "line": "2 per person · ever"
        },
        "placed_at": "2026-11-03T18:20:41.512Z",
        "placed_by_api": true
      },
      "activated": true
    }
  }
}
```

If you sent `activate: true` and a [Production requirement](#production-activation) is missing, the gate is still placed, as a Draft, and the response says why it is not live:

```json theme={null}
{
  "data": {
    "data": {
      "gate": { "key": "presale-dec-04", "status": "draft", "state": null, "…": "…" },
      "activated": false,
      "activation_error": {
        "message": "Verify your domain before activating a production gate. One TXT record proves the site is yours.",
        "statusCode": 403,
        "code": "domain_not_verified"
      }
    }
  }
}
```

Fix the requirement, then call [`gates-api/activate`](#activate-a-gate) with the same key. Do not call `create` again: the key is now taken.

### The gate object

`create`, `activate`, `get` and `list` return gates in this shape. It holds your own settings and nothing about people.

| Field | Description |
| - | - |
| `key` | The gate key. |
| `name`, `message` | As you set them. `message` is `null` when unset. |
| `status` | `draft`, `active` or `closed`. |
| `state` | Active gates only: `scheduled` (before `opensAt`), `open`, or `closed` (after `closesAt`). `null` on a Draft. |
| `environment` | `development` or `production`, from the key. |
| `destination_url` | As you set it, or `null`. |
| `policy` | The gate's effective policy: `preset`, `gate_type`, `age_threshold`, `signed_up`, `link`, `passes_per_person`, `real_days_at_least`, `schedule`, and `line`, the one-line summary the Console shows (for example `"1 per person · a day · BotShield ID real 30+ days"`). |
| `placed_at` | When the gate was placed. |
| `placed_by_api` | `true` when a Gates API key placed it. |

### The policy document

The policy document holds a gate's settings. BotShield validates the whole document; a field it does not know is refused with `invalid_policy`.

| Field | Type | Description |
| - | - | - |
| `version` | `1`, `2` or `3` | Optional on `create`. Send `3` if you send it at all. |
| `preset` | string or `null` | The label the gate was placed with. It pre-fills settings and does nothing else. `null` means Custom. |
| `link` | `required`, `optional` or `never` | Whether a pass links the person to your account for them. See [The link setting](https://docs.botshield.ai/gate/link.md). When you leave it out, the preset's value applies. With no preset and no `link`, the gate does not link (`never`). |
| `signedUp` | boolean | Only people who signed up for this gate may pass. Refused people get `signup_required`. |
| `schedule` | object | `opensAt` and `closesAt`, ISO 8601 with a time zone offset. Either can be `null` or left out: no `opensAt` means open from activation, no `closesAt` means no end. `closesAt` must be after `opensAt`. A person who arrives outside the window is refused with `gate_not_open`. |
| `passesPerPerson` | object or `null` | `{ "limit": 1–100, "window": "day" \| "week" \| "30d" \| "ever" }`. `null` means as often as they like. See [Who may pass](https://docs.botshield.ai/gate/who-may-pass.md). |
| `realDaysAtLeast` | integer | `0`–`365`. The person's BotShield ID must have proved a person on at least this many days. `0` means anyone. |
| `asks` | `always` | Optional. A gate always asks; this field only lets a document say so. |

Your organization can also hold an organization-wide document. A gate's document can only tighten what the organization's allows. The `policy` in the response is the effective result.

### Presets

A preset pre-fills four settings. Any field you send in `policy` replaces the preset's value for that field, as a whole: send all of `passesPerPerson` if you change it.

| `preset` | `signedUp` | `link` | `passesPerPerson` | `realDaysAtLeast` |
| - | - | - | - | - |
| `presale` | `true` | `required` | 2, ever | 0 |
| `promo` | `true` | `required` | 1, ever | 0 |
| `drop` | `false` | `optional` | 1, a day | 30 |
| `vote` | `false` | `optional` | 1, ever | 0 |
| `reservation` | `false` | `optional` | 1, a day | 0 |
| `claim` | `false` | `optional` | 1, ever | 30 |

The signed-up presets link every pass, because the people who signed up have accounts with you. The others link only when you send your user ref. Send `link` in `policy` to change it. Age is no preset's: set `gateType` and `ageThreshold` yourself. An Age Gate never links, whatever `link` says.

## Activate a gate

`POST /gates-api/activate` makes a Draft gate Active. Calling it on a gate that is already Active changes nothing and returns the gate.

```bash curl theme={null}
curl -s https://api.botshield.ai/operations/gates-api/activate \
  -H "Authorization: Bearer $BOTSHIELD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"presale-dec-04"}'
```

```json theme={null}
{
  "data": {
    "data": {
      "gate": { "key": "presale-dec-04", "status": "active", "state": "scheduled", "…": "…" }
    }
  }
}
```

### Production activation

A Development gate activates with no checks. A Production gate activates only when your organization has met these requirements, checked in this order:

| Code | Requirement |
| - | - |
| `org_not_verified` | Your business has passed verification. |
| `domain_not_verified` | You have at least one [verified domain](https://docs.botshield.ai/gate/go-to-production.md#1-verify-your-domain). |
| `gate_limit_reached` | Your plan has room for another production gate. The message names your plan and its allowance. |

The API has no passkey step at activation. The passkey you gave when you granted the key **Gates API access** stands in for it. Gates that are already Active keep running if a requirement changes later. See [Go to production](https://docs.botshield.ai/gate/go-to-production.md).

## Extend a gate

`POST /gates-api/extend` moves a gate's closing time later, for example when a show runs long. It changes nothing else: BotShield saves a new version of the gate's policy document with only `schedule.closesAt` moved.

```bash curl theme={null}
curl -s https://api.botshield.ai/operations/gates-api/extend \
  -H "Authorization: Bearer $BOTSHIELD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"presale-dec-04","closesAt":"2026-12-02T14:00:00-08:00"}'
```

| Request field | Type | Description |
| - | - | - |
| `key` | string | The gate key. |
| `closesAt` | string | The new closing time, ISO 8601 with a time zone offset. It must be later than the gate's current closing time. |

```json theme={null}
{
  "data": {
    "data": {
      "gate": { "key": "presale-dec-04", "status": "active", "state": "open", "…": "…" },
      "extended": true,
      "version": 2
    }
  }
}
```

`version` is the number of the policy document the extension saved. The response `schedule.closesAt` is in UTC.

A window only moves later. Extend refuses:

* a `closesAt` that is not later than the current one (`extend_only`);
* a gate that has no closing time (`gate_has_no_end`);
* a gate whose closing time has already passed (`gate_closed`). A closed gate is never reopened: place a new gate.

## Close a gate

`POST /gates-api/close` stops the gate issuing passes. Tokens already issued still verify until they expire, 120 seconds after issue. The gate's Verification Logs and Activity Logs are kept. A closed gate cannot be reopened.

```bash curl theme={null}
curl -s https://api.botshield.ai/operations/gates-api/close \
  -H "Authorization: Bearer $BOTSHIELD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"presale-dec-04"}'
```

```json theme={null}
{ "data": { "data": { "closed": true } } }
```

Close is safe to repeat. When no Draft or Active gate has the key, it answers `{ "closed": true, "already_closed": true }`.

## Cancel a gate

`POST /gates-api/cancel` removes a gate that nobody has passed through, for example one placed by mistake. If any verification has run on the gate, cancel is refused with `gate_in_use`: close it instead, so its log is kept. If BotShield cannot check the gate or cannot remove it, cancel answers `retry` and the gate stays as it was.

```bash curl theme={null}
curl -s https://api.botshield.ai/operations/gates-api/cancel \
  -H "Authorization: Bearer $BOTSHIELD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"presale-dec-04"}'
```

```json theme={null}
{ "data": { "data": { "cancelled": true } } }
```

```json theme={null}
{
  "data": {
    "error": {
      "message": "People already passed through this gate — close it instead; its log is kept.",
      "statusCode": 409,
      "code": "gate_in_use"
    }
  }
}
```

After a gate is closed or cancelled, its key is free, and you can place a new gate with it.

## Get a gate

`GET /gates-api/get` reads one Draft or Active gate by key.

```bash curl theme={null}
curl -s "https://api.botshield.ai/operations/gates-api/get?key=presale-dec-04" \
  -H "Authorization: Bearer $BOTSHIELD_API_KEY"
```

```json theme={null}
{
  "data": {
    "data": {
      "gate": { "key": "presale-dec-04", "status": "active", "state": "open", "…": "…" }
    }
  }
}
```

A closed or cancelled gate answers `gate_not_found`.

## List gates

`GET /gates-api/list` returns your Draft and Active gates in the key's environment, newest first. `limit` is optional: 1–200, default 50. Closed and cancelled gates are not listed.

```bash curl theme={null}
curl -s "https://api.botshield.ai/operations/gates-api/list" \
  -H "Authorization: Bearer $BOTSHIELD_API_KEY"
```

```json theme={null}
{
  "data": {
    "data": {
      "gates": [
        { "key": "presale-dec-04", "status": "active", "state": "open", "…": "…" },
        { "key": "presale-dec-05", "status": "draft", "state": null, "…": "…" }
      ]
    }
  }
}
```

## Rules

* **A key is never renamed for you.** If the key is taken, `create` is refused with `gate_key_taken`. Unlike the Console, the API never adds a suffix such as `-2`, so the key you sent is always the gate's key. If a `create` call timed out, call `get` with the same key before you retry.
* **Cancel only works before anyone has passed.** After the first verification, close the gate instead.
* **Close keeps the log.** A closed gate stays in your Verification Logs and Activity Logs.
* **A window only moves later.** `extend` never moves `closesAt` earlier and never reopens a gate that has closed.
* **Production activation needs** business verification, a verified domain and room in your plan's production gate allowance.
* **The platform rules still hold.** Every pass is a fresh passkey confirmation, and every token is valid for [one check](https://docs.botshield.ai/gate/verify-on-your-server.md#single-use). Neither is a setting, and a policy document can only tighten a gate.

## Error codes

Errors arrive as `data.error` with HTTP `200`. A body that fails input validation, for example a key with capital letters, is rejected with HTTP `400` before the operation runs. See [Errors](https://docs.botshield.ai/api-reference/errors.md).

| `code` | `statusCode` | Operations | Cause |
| - | - | - | - |
| `invalid_key` | 401 | All | The `Authorization` header has no `bs_` key, or the key is invalid or inactive. |
| `gates_scope_required` | 403 | All | The key does not have **Gates API access**. Grant it in **Settings → Developer Tools → API Keys**. |
| `org_not_found` | 404 | All | The key's organization could not be found. |
| `gate_not_found` | 404 | `activate`, `extend`, `cancel`, `get` | No Draft or Active gate has this key in the key's environment. |
| `gate_key_taken` | 409 | `create` | A Draft or Active gate already has this key in this environment. |
| `age_threshold_required` | 400 | `create` | `gateType` is `age` and `ageThreshold` is missing. |
| `invalid_policy` | 400 | `create` | The policy document failed validation. The message lists each problem. |
| `org_not_verified` | 403 | `activate`, `create` with `activate` | Production: business verification is not complete. |
| `domain_not_verified` | 403 | `activate`, `create` with `activate` | Production: no verified domain. |
| `gate_limit_reached` | 403 | `activate`, `create` with `activate` | Production: the plan's production gate allowance is used. |
| `extend_only` | 400 | `extend` | `closesAt` is not later than the gate's current closing time. |
| `gate_has_no_end` | 409 | `extend` | The gate has no closing time, so there is nothing to extend. |
| `gate_closed` | 409 | `extend` | The gate's closing time has passed. A closed gate is never reopened. |
| `conflict` | 409 | `extend` | The gate's settings changed at the same moment. Read the gate with `get` and try again. |
| `gate_in_use` | 409 | `cancel` | Someone has passed through the gate. Close it instead. |
| `retry` | 503 | `cancel` | BotShield could not check whether the gate was used, or could not remove it. Nothing changed. Try again. |
| `internal` | 500 | All | BotShield could not complete the call. Nothing was half-placed: a gate whose settings could not be saved is removed. |

## Next steps

> [card] [The link setting](https://docs.botshield.ai/gate/link.md)
>
> Decide whether a pass links the person to your account for them.

> [card] [Who may pass](https://docs.botshield.ai/gate/who-may-pass.md)
>
> Passes per person and BotShield ID age.

> [card] [Verify on your server](https://docs.botshield.ai/gate/verify-on-your-server.md)
>
> Check each token once, on your server.

> [card] [BotShield Actions](https://docs.botshield.ai/actions/overview.md)
>
> Key any act to a passkey, without a gate.


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