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 billing period, also set by your tier.
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

Quota is deducted only after a real data-layer lookup. A request rejected before reaching the data layer (bad token, over quota, rate-limited) does not consume monthly quota.

Response headers

Every billed response — success or 429 — carries the current limit state, so you can implement adaptive backoff without provoking a 429:

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. 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. 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 their own connection and message-rate limits. Contact support@sportquanta.com for streaming access.