Skip to main content

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.
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 and we’ll raise your key’s limit.

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.
BucketLimitWindowScopePurpose
per_minute12060sper keySmooths spikes
daily25,00024hper keyCost gate
per_ip2060sper (key, client IP)Catches scraped-key abuse from a single source

Development API keys — bs_dev_*

Backend keys for local development, test suites, and the Client SDK Playground. Designed for bursty dev workflow; tight enough on sustained throughput that running a production integration on a dev key is painful.
BucketLimitWindowScope
burst3010sper key
hourly3001hper key
daily2,00024hper key

Production API keys — bs_prod_*

Backend keys for server-to-server calls in production — approvals, presence checks, and other authenticated operations.
BucketLimitWindowScope
per_minute6060sper key
hourly2,0001hper key
daily25,00024hper 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.
{
  "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 }
    }
  }
}
FieldMeaning
scopeWhich tier was applied (dev, bs_prod, pk_*, bss_*)
primaryThe most-constrained bucket — the one you’ll hit first if you keep going
primary.resetInSeconds until this bucket’s window rolls over
bucketsFull per-bucket breakdown

Envelope inclusion policy

Key typeEnvelope included
bs_dev_*Always — dev 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
{
  "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 }
    }
  }
}
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.

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 with:
  • Your organization ID (visible in the Partner Dashboard)
  • 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.