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
| Window | Limit | Applies to |
|---|---|---|
| 60 seconds | 60 requests per IP | Every route, every auth mode, including unauthenticated ones. |
| 1 hour | 1000 requests per IP | Same. 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 / min | Routes |
|---|---|
| 3 | POST 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 |
| 5 | POST 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 |
| 10 | POST 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 |
| 20 | POST guest-checkout, POST storefront-bot/{id}/chat, PUT storefront-bot/{id}/memory/consent, PUT storefront-bot/{id}/memory/switches |
| 30 | GET 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) |
| 60 | POST 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 |
| 120 | GET 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 tier | Limit |
|---|---|
FREE | 60 requests / minute |
PRO | 500 requests / minute |
GROWTH | 2000 requests / minute |
ENTERPRISE | 10000 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 aGROWTHkey 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 carriesdetails.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}/summarizeat 10/min, media ingest-by-URL (POST /v1/media) at 20 per 5 minutes, and the deprecated no-opPOST /v1/coupons/{id}/syncat 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.
| Limit | Counted per | Threshold | What a client sees |
|---|---|---|---|
| Locked email | one email address on one store, across all IPs | 10 failures within 10 minutes | The 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 address | one IP address on one store | 100 failures within an hour | 429 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
| Header | Description |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window |
X-RateLimit-Remaining | Requests left in the current window |
X-RateLimit-Reset | Seconds until the window resets — not a Unix timestamp. Add it to "now" yourself |
Retry-After | Seconds to wait before retrying (only on 429 responses) |
The same values are also sent under the standard, un-prefixed names:
| Header | Description |
|---|---|
RateLimit-Limit | Same meaning as X-RateLimit-Limit |
RateLimit-Remaining | Same meaning as X-RateLimit-Remaining |
RateLimit-Reset | Seconds until the window resets. On a 429 it is the seconds until the block lifts |
RateLimit-Policy | The 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.
Recommended client behavior
- Honor
Retry-After— Don't retry sooner than the header tells you. - Use exponential backoff for transient errors (
429,503, network failures). Start at 1s, double up to 60s. - Batch where possible — e.g.
POST /v1/products/bulkcreates up to 1000 products in one queued call instead of N individualPOST /v1/productsrequests. - Cache idempotent reads — Storefront catalog data changes slowly; cache product lists for 30-60 seconds on the edge.
- 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.