Two independent limits apply to every billed request:
  1. A rolling-hour rate limit — requests per hour, set by your tier.
  2. A monthly quota — requests per UTC calendar month, set by your tier. Trial allowances use a lifetime budget instead of a monthly refill.
A request that violates either returns 429 with a code that tells you which limit was hit. Both limits are keyed on your organization, not your token — rotating or adding tokens doesn’t reset or multiply them.

What counts

The gateway counts accepted metered reads, including response-cache hits and data-layer failures. It excludes 401/403/429 responses and requests explicitly marked non-billable, such as pre-read validation or route misses. Authentication failures do not enter the rate limiter; later validation/plan failures may have already spent an hourly token.

Response headers

Responses that pass through the metering chain expose limit headers when the limiter/quota state is available. Catalog responses do not use that chain. Use the returned headers to implement adaptive backoff:

Hourly rate limit

The hourly limit is a token bucket: capacity equals your tier’s hourly cap, refilling smoothly. This tolerates bursts — a quiet client can briefly exceed the steady-state rate (useful for backfills and overnight batch pulls). When the bucket is empty:
Retry-After is the whole number of seconds until the bucket has a token again. Honor it before retrying.

Monthly quota

The monthly quota is a hard counter — there’s no overage. When it’s exhausted:
Retry-After is not set on quota_exceeded — the wait is typically days. Either upgrade your plan or wait for resets_at. Trial budgets do not refill. An exhausted trial uses the same quota_exceeded code, but omits details.resets_at and the monthly reset header. Waiting for a calendar rollover will not restore the allowance. An upgrade takes effect within about a minute: the API caches the plan attached to each token for up to 60 seconds, so a request issued immediately after checkout can still be rejected against the old quota. Retry rather than treating that as a failed upgrade.

When a plan does not cover an endpoint

Quota and rate limits answer “how much”; your plan also decides “what”. An endpoint your plan does not include returns 403 with error.code = "feature_not_in_plan":
Note it is a 403 and not a 401: the token is valid, so reissuing it changes nothing. details.feature names the capability and details.required_tier names the plan that sells it. These refusals do not consume monthly quota, though they do draw down the hourly bucket — the plan check runs after the rate limiter. Core, Sharp, and Max include odds, stats, and sharp-book prices. Sharp and Max include WebSocket access. Max includes signals for settlement grades, durable closing lines, closing-anchored CLV, and divergence. Active trial grants and legacy subscriptions can differ; capability checks and returned headers are authoritative for the token in use. One case is deliberately not an error. Odds responses omit bookmakers your plan does not cover and return 200; the response is correct, just narrower. Asking for only books you do not have returns 403 feature_not_in_plan rather than an empty result, because an empty market set is indistinguishable from an outage. Check class on /v1/bookmakers before concluding a book is missing.

Handling 429s

1

Distinguish the two codes

Branch on error.code: rate_limited is short-lived; quota_exceeded needs an upgrade or a wait until resets_at.
2

Respect Retry-After

On rate_limited, wait Retry-After seconds before retrying. Don’t retry quota_exceeded in a tight loop.
3

Back off proactively

Watch X-RateLimit-Remaining and X-Requests-Remaining-Month on successful responses and slow down before you hit zero.

Streaming is separate

WebSocket connections are not counted against the REST hourly bucket or monthly quota. They have separate connection and message limits and require streaming access. See WebSocket Streaming for subscriptions, heartbeats, connection replacement, and gap recovery.