API ReferenceRate Limits

Rate Limits

The limits actually enforced today, the response headers, and recommended client behavior under throttling.

Brainerce enforces two layers of rate limiting: a global per-IP throttle that applies to every route, and, on top of it, a per-credential quota for admin API keys and marketplace-app installations. A request must pass both.

Global — per IP, every route

WindowLimitApplies to
60 seconds60 requests per IPEvery route, every auth mode, including unauthenticated ones.
1 hour1000 requests per IPSame. Runs alongside the 60-second window, not instead of it.

Both tiers are keyed on the client IP, not on your credential. Two integrations running from the same host share these buckets.

Individual routes set their own, tighter limits. Anything that costs money to abuse — customer registration, login, forgot-password, resend-verification, coupon application, review submission — carries a per-route override, some as low as 3 requests per minute per IP. There is no single number that covers the storefront surface; treat 60/min as a ceiling, not an allowance, and read the 429 body's details.limit for the limit that actually tripped. A per-route override replaces the 60-second window for that route; the 1000-per-hour window still applies on top. Some read-heavy storefront routes are set above 60/min (see the table below).

The global defaults are the values of the THROTTLE_GLOBAL_LIMIT / THROTTLE_GLOBAL_TTL and THROTTLE_LONG_LIMIT / THROTTLE_LONG_TTL settings of the deployment; the numbers above are what the hosted API runs with.

Storefront routes with their own limit (per IP, per minute)

These are the /api/vc/{salesChannelId}/… routes of the storefront API, plus the storefront bot routes (/api/storefront-bot/…). The legacy /api/stores/{storeId}/… mirror carries the same limits except where noted and except that GET events, GET events/{idOrSlug} and GET events/{idOrSlug}/availability are all 30 per minute there. A storefront route that is not listed here has only the global 60-second and 1000-per-hour limits.

Limit / minRoutes
3POST customers/forgot-password, POST customers/resend-verification, POST products/{id}/reviews, POST inquiries, POST marketing/subscribe, POST events/registrations/view-link, POST donor-manage/request-link. On the legacy /stores/{storeId} mirror POST customers/register is also 3
5POST customers/register, POST customers/login, POST customers/reset-password, POST checkout/{id}/gift-card, POST gift-cards/balance, POST donations, GET donations/{id}, POST events/{idOrSlug}/registrations, POST orders/guest/downloads, POST payment/sdk-confirm, PATCH and DELETE products/{id}/reviews/me, POST stock-alerts, POST loyalty/membership/subscribe, GET loyalty/rewards/recommended, POST orders/lookup (legacy mirror), POST storefront-bot/{id}/escalate
10POST cart/{id}/coupon, POST checkout/{id}/coupon, POST checkout/{id}/complete, POST orders, POST payment/intent, POST reservation/extend, POST customers/verify-email, POST cart/recover, POST customization-upload, POST products/{id}/review-photo, GET loyalty/referral-info, DELETE storefront-bot/{id}/memory/items
20POST guest-checkout, POST storefront-bot/{id}/chat, PUT storefront-bot/{id}/memory/consent, PUT storefront-bot/{id}/memory/switches
30GET checkout/address-details, GET oauth/{provider}/callback, GET events/registrations/by-token/{token}, GET events/registrations/{id}/status, GET newsletter-benefit, POST storefront-bot/attribution/click, GET storefront-bot/{id}/conversations/{id}, PATCH and DELETE storefront-bot/{id}/memory/items/{itemId}, GET cart/{id} and GET products/{id}/reviews (legacy mirror)
60POST inventory/check-availability, GET checkout/address-suggestions, GET events/{idOrSlug}/availability, GET contact-forms, GET contact-forms/{formKey}, GET storefront-bot/{id}/settings, GET storefront-bot/{id}/memory
120GET donation-campaigns, GET donation-campaigns/{slug}, GET donation-options, GET events, GET events/{idOrSlug}, GET storefront-bot/{id}/customer-session, POST events (pageview beacon, /api/vc/{salesChannelId}/events), GET blog/posts, GET blog/posts/{slug}, GET content, GET content/{type}/{key}, GET content/pages/by-slug/{slug}

Per API key — admin mode

On top of the global tiers, a brainerce_* key gets its own bucket keyed on the key id, so two integrators on the same IP no longer collide:

Key tierLimit
FREE60 requests / minute
PRO500 requests / minute
GROWTH2000 requests / minute
ENTERPRISE10000 requests / minute

A key can use its store's plan tier or a lower one. Creating or updating a key with a higher tier, or a rateLimitOverride above the plan ceiling, is refused with 400 (API_KEY_TIER_EXCEEDS_PLAN or API_KEY_RATE_LIMIT_EXCEEDS_PLAN).

Three things to know about this bucket:

  • It does not lift the global per-IP tiers. The /v1/* controller has no throttle exemption, so a GROWTH key calling from a single IP still meets the 60/min per-IP throttle first. The per-key quota bounds a key; it does not exempt it. To actually use a 2000/min tier you have to spread the traffic across source IPs — a single worker behind one NAT will see 429s long before its tier.
  • There is a per-account aggregate ceiling. All keys on one account share a bucket of min(tier_limit × 5, 20000) requests/minute — so minting more keys does not multiply your throughput. A 429 from this bucket carries details.scope: "account".
  • A few /v1/* routes are capped far lower, regardless of tier: customer login and register and reset-password at 5/min, forgot-password at 3/min, applying a coupon to a cart or checkout at 10/min, applying a gift card to a checkout and checking a gift-card balance at 5/min each (tighter than the coupon route on purpose: a coupon code is meant to be shared, a gift-card code is bearer value and therefore a guessing surface), POST /v1/storefront-bot/conversations/{id}/summarize at 10/min, media ingest-by-URL (POST /v1/media) at 20 per 5 minutes, and the deprecated no-op POST /v1/coupons/{id}/sync at 30/min. These are abuse and cost-amplification guards, not quotas you can raise.

Support can pin an individual key to a custom limit; if that has been done for you, the override replaces the tier value. An override is capped at 5000 requests / minute (a larger stored value is treated as 5000), and the per-account ceiling above is still computed from the tier, not from the override.

Per installation — marketplace apps

App-installation tokens (app_inst_*) are on their own tiers: 1000/min for first-party Brainerce apps, 500/min for ECOMMERCE_PLATFORM connectors, 100/min for everyone else. A per-installation override replaces the tier value and is capped at 5000/min. These do not apply to brainerce_* API keys — if you hold an API key, the table above is yours.

Inbound webhooks

Inbound webhook routes are not unbounded. The marketplace-app webhook receivers (/api/apps/webhooks/* and /api/apps/webhooks-signed/*) are capped at 30,000 requests / minute per installation — a DoS safety valve rather than a plan quota, keyed on the installation id so one busy install cannot starve another behind the same egress IP. The global per-IP tiers above still apply to any inbound route that does not explicitly override them.

Failed-authentication throttling

Separately from the quotas above, an IP that repeatedly sends invalid or expired credentials (a wrong brainerce_* key, a forged token) is temporarily blocked: once an IP has made more than 50 failed-auth attempts within 5 minutes, further requests from it that carry an Authorization or X-API-Key header receive 429 Too Many Requests (message Too many failed authentication attempts. Please try again later.) until the window clears. Requests that authenticate successfully never count toward this limit.

If you hit this, fix the credential — don't retry-storm. A valid key with the correct scopes is never affected.

Customer sign-in failures

The customer login route (POST customers/login, on the /api/vc/{salesChannelId} and legacy /api/stores/{storeId} surfaces) has two failure limits on top of its 5 requests a minute per IP. Both count failed sign-ins only, and a successful sign-in never counts against either.

LimitCounted perThresholdWhat a client sees
Locked emailone email address on one store, across all IPs10 failures within 10 minutesThe email is locked for 10 minutes. Every attempt answers 401 Invalid email or password, identical to a wrong password (same status, same body, same response time), even with the right password. Attempts while locked are not counted, so they cannot extend the lock. A password reset ends it at once.
Failures from one addressone IP address on one store100 failures within an hour429 for that address only, until the hour is over. Other addresses are unaffected.

The /v1 API-key login route has the locked-email limit but not the per-address one: its caller is your server, not the shopper, so an address bucket there would pool every shopper of the store. Register, forgot-password and verify-email keep their own limits listed above and in Customer accounts.

Response headers

HeaderDescription
X-RateLimit-LimitRequests allowed in the current window
X-RateLimit-RemainingRequests left in the current window
X-RateLimit-ResetSeconds until the window resets — not a Unix timestamp. Add it to "now" yourself
Retry-AfterSeconds to wait before retrying (only on 429 responses)

The same values are also sent under the standard, un-prefixed names:

HeaderDescription
RateLimit-LimitSame meaning as X-RateLimit-Limit
RateLimit-RemainingSame meaning as X-RateLimit-Remaining
RateLimit-ResetSeconds until the window resets. On a 429 it is the seconds until the block lifts
RateLimit-PolicyThe quota and window, for example "default";q=60;w=60. Every response carries the two global policies ("default" 60 per 60 s and "long" 1000 per 3600 s) before any limit has been counted, without a RateLimit-Remaining

When several buckets apply to one request (global per IP, per API key, per account), the un-suffixed RateLimit-* headers describe the bucket closest to exhaustion, the one that will reject you first. Browser code can read all of these headers cross-origin except the -long copies. X-Brainerce-*-prefixed rate-limit headers do not exist.

Because two windows run at once, the hourly tier emits its own suffixed copies — X-RateLimit-Limit-long, X-RateLimit-Remaining-long, X-RateLimit-Reset-long. The unsuffixed headers describe the 60-second window.

When throttled

A throttled request returns 429 Too Many Requests with the standard error envelope:

{
  "statusCode": 429,
  "code": "RATE_LIMITED",
  "message": "Rate limit exceeded — retry in 12 seconds.",
  "details": { "retryAfterSeconds": 12, "limit": 60, "ttlMs": 60000 },
  "timestamp": "2026-08-23T10:15:00.000Z",
  "path": "/api/v1/products"
}

Switch on code === 'RATE_LIMITED' — it is the same code whichever bucket tripped, and details tells you which one (scope: "account" for the per-account ceiling, tier for the per-key bucket). The failed-authentication block above also answers with code: "RATE_LIMITED", but without a details object.

  1. Honor Retry-After — Don't retry sooner than the header tells you.
  2. Use exponential backoff for transient errors (429, 503, network failures). Start at 1s, double up to 60s.
  3. Batch where possible — e.g. POST /v1/products/bulk creates up to 1000 products in one queued call instead of N individual POST /v1/products requests.
  4. Cache idempotent reads — Storefront catalog data changes slowly; cache product lists for 30-60 seconds on the edge.
  5. Use webhooks instead of polling — Webhooks push order/inventory/customer changes to you in real time.

No burst allowance

There is none. Each bucket is a fixed-window counter that rejects at exactly the limit — request number 61 in a 60-request window is a 429, with no grace, no sliding average, and no borrowing against the next window. Size your client for the limit itself.

If your integration needs a higher limit, contact [email protected] with your expected traffic pattern.