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

# Rate Limits

> Per-key rate limit tiers, response envelope, and block behavior

# Rate Limits

Every authenticated API request is rate-limited based on the **type of key** in the `Authorization` header. Limits protect against credential abuse and runaway cost without disrupting legitimate integrations.

<Info>
  BotShield is in a free **Pilot Access Program**. The limits below are pilot defaults, chosen to be generous for real dev + production traffic. Need more headroom — for a launch window, a traffic spike, or sustained higher volume? [Contact us](mailto:support@botshield.ai) and we'll raise your key's limit.
</Info>

## Limits by key type

All four bearer token types are limited. Each type has multiple buckets that run in parallel — a request is allowed only if **every** bucket has capacity. Whichever bucket is closest to its cap is what surfaces as `primary` in the response envelope.

### Site keys — `pk_test_*` / `pk_live_*`

Public keys used by the client-side Web Component (Turnstile-style CAPTCHA replacement). Exposable in browser JavaScript.

| Bucket       | Limit  | Window | Scope                | Purpose                                        |
| ------------ | ------ | ------ | -------------------- | ---------------------------------------------- |
| `per_minute` | 120    | 60s    | per key              | Smooths spikes                                 |
| `daily`      | 25,000 | 24h    | per key              | Cost gate                                      |
| `per_ip`     | 20     | 60s    | per (key, client IP) | Catches scraped-key abuse from a single source |

### Test API keys — `bs_test_*`

Backend keys for local development, test suites, and the [Client SDK Playground](https://console.botshield.ai). Designed for bursty dev workflow; tight enough on sustained throughput that running a production integration on a test key is painful.

| Bucket   | Limit | Window | Scope   |
| -------- | ----- | ------ | ------- |
| `burst`  | 30    | 10s    | per key |
| `hourly` | 300   | 1h     | per key |
| `daily`  | 2,000 | 24h    | per key |

### Production API keys — `bs_prod_*`

Backend keys for server-to-server calls in production — approvals, presence checks, and other authenticated operations.

| Bucket       | Limit  | Window | Scope   |
| ------------ | ------ | ------ | ------- |
| `per_minute` | 60     | 60s    | per key |
| `hourly`     | 2,000  | 1h     | per key |
| `daily`      | 25,000 | 24h    | per key |

### Anchor grant tokens — `bss_*`

Short-lived (5-minute) session tokens issued by `POST /sdk/create-session`. They share the production tier defaults above. Because they expire quickly and are single-issue per session, the `daily` cap is effectively unreachable in normal use.

## The `_rateLimit` response envelope

Every response includes a `_rateLimit` envelope so you can observe your current bucket state without guessing. The envelope is a sibling of `data` (or `errors`) at the top of the response body.

```json theme={null}
{
  "data": { /* your operation's response */ },
  "_rateLimit": {
    "scope": "bs_prod",
    "primary": {
      "bucket": "per_minute",
      "limit": 60,
      "remaining": 47,
      "resetIn": 42
    },
    "buckets": {
      "per_minute": { "limit": 60,    "remaining": 47,    "resetIn": 42 },
      "hourly":     { "limit": 2000,  "remaining": 1893,  "resetIn": 2841 },
      "daily":      { "limit": 25000, "remaining": 24891, "resetIn": 71203 }
    }
  }
}
```

| Field             | Meaning                                                                  |
| ----------------- | ------------------------------------------------------------------------ |
| `scope`           | Which tier was applied (`bs_test`, `bs_prod`, `pk_*`, `bss_*`)           |
| `primary`         | The most-constrained bucket — the one you'll hit first if you keep going |
| `primary.resetIn` | Seconds until this bucket's window rolls over                            |
| `buckets`         | Full per-bucket breakdown                                                |

### Envelope inclusion policy

| Key type                  | Envelope included                                                                                                                |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `bs_test_*`               | **Always** — test keys are a developer surface                                                                                   |
| `bs_prod_*` / `bss_*`     | **Always**                                                                                                                       |
| `pk_test_*` / `pk_live_*` | **Opt-in** — send `X-BotShield-Include-RateLimit: true` to include it. Kept off by default so checkout-path responses stay lean. |

## Blocked responses

When any bucket for your key is exhausted, the request is **not** handled by the operation — instead the API returns an immediate block response.

* **HTTP status:** `403`
* **`errors[0].code`:** `RATE_LIMITED`
* **Body:** the same `_rateLimit` envelope you get on success, plus an `errors` array describing which bucket tripped

```json theme={null}
{
  "errors": [{
    "message": "Rate limit exceeded. Bucket \"per_ip\" hit its cap; retry in 57s.",
    "code": "RATE_LIMITED"
  }],
  "_rateLimit": {
    "scope": "pk_live",
    "primary": {
      "bucket": "per_ip",
      "limit": 20,
      "remaining": 0,
      "resetIn": 57
    },
    "buckets": {
      "per_minute": { "limit": 120, "remaining": 99, "resetIn": 57 },
      "daily":      { "limit": 25000, "remaining": 24213, "resetIn": 85649 },
      "per_ip":     { "limit": 20, "remaining": 0, "resetIn": 57 }
    }
  }
}
```

<Warning>
  **Check `errors[0].code`, not the status code.** Clients should detect rate-limit blocks by reading `errors[0].code === 'RATE_LIMITED'`. The HTTP status is `403` (rather than the conventional `429`) for compatibility with our API gateway's retry behavior — the body is the authoritative source of truth.
</Warning>

### Retry strategy

* Read `_rateLimit.primary.resetIn` from the blocked response — that's the seconds until capacity returns.
* Don't retry immediately on block. Exponential backoff is overkill: just wait until `resetIn` seconds have passed, then try again.
* The backend SDKs do not retry `RATE_LIMITED` responses automatically — you decide whether to wait, fall back, or surface the error to the user.

## Common integration patterns

**Pre-flight check before a batch:** read `_rateLimit.primary.remaining` from your last successful response. If it's lower than the batch size you're about to send, pace the batch.

**User-facing error handling:** treat `RATE_LIMITED` as retryable. Show a "this is taking longer than usual" message and retry after `resetIn`. Don't expose the raw bucket state to end users.

**Dashboard observability:** for high-traffic integrations, log `_rateLimit.primary.remaining` alongside your request metrics. A sustained downward trend is an early signal you're approaching a limit.

## Requesting a higher limit

If you need more headroom — for a launch window, a migration, or sustained higher traffic — [email support](mailto:support@botshield.ai) with:

* Your organization ID (visible in the [Partner Dashboard](https://console.botshield.ai))
* The key ID(s) that need the bump
* Which bucket is the bottleneck and your target limit
* Whether the increase should be permanent or have an end date

Limit changes take effect within about a minute.
