Response headers
Every header the API sets, what it means, and which ones a browser can read.
On every response
| Header | Meaning |
|---|---|
X-Request-Id | A UUID identifying this request. Log it. It's repeated in error.request_id on failures, and it's what turns a support question into one log lookup. |
Content-Type | Always application/json; charset=utf-8. |
On every metered response
Metered means any authenticated /v1/... call — so everything except /health.
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Your token bucket's burst capacity (100 on both paid plans). |
X-RateLimit-Remaining | Whole tokens left after this request. |
X-Quota-Limit | Requests your plan includes in the current window: 50,000/month on Standard, 150,000/month on Pro, 5,000 in total on a legacy trial, 2,000 in total on a sandbox key, 500 a UTC day for each network address on the public sample key. |
X-Quota-Remaining | Requests left in the window, across every key on the account. |
X-Quota-Reset | When the window ends — RFC 3339, UTC. Start of next month, or a legacy trial's end. Not sent for a sandbox key, which never resets. |
On a 429
Retry-After, in seconds. A rate-limit 429 gives a second or two; a quota 429 can give days.
A sandbox_quota_exceeded 429 has none: a sandbox key's 2,000 requests never come back.
Which one you got is in error.code — see errors.
A sample_quota_exceeded 429 carries Retry-After too: the seconds until 00:00 UTC, when the public sample key's daily counts
reset. On that key X-Quota-Limit is 500 and X-Quota-Reset is the next 00:00 UTC.
Always honour Retry-After over your own backoff curve: we know when the bucket refills, your curve
is a guess.
On a per-endpoint 429
Two cases carry the per-endpoint headers, and in both your overall allowance still has requests left, so another endpoint keeps working.
- A legacy trial has its own ceiling on each endpoint (1,000
requests; 20 on
/matches/bulk). Subscriptions no longer start with a trial. The429has the codetrial_endpoint_quota_exceeded, andRetry-Afterruns to the end of the trial. - A Standard account that first subscribed on or after 30 October 2026 may make 250 bulk-export
requests a month. The
429has the codebulk_quota_exceeded, andRetry-Afterruns to the start of next month. Pro, and a Standard account that first subscribed before that date, have no such cap. See bulk export.
Either 429 carries two more headers:
| Header | Meaning |
|---|---|
X-Quota-Endpoint-Limit | The ceiling for the endpoint you called (the trial's, or 250 for the bulk export). |
X-Quota-Endpoint-Remaining | Always 0. The header is only sent once the ceiling is reached. |
They appear on those responses only, not on every metered response.
Reading them from a browser
Custom headers are hidden from cross-origin fetch unless the server names them. The API sets:
Access-Control-Expose-Headers: X-Request-Id, X-RateLimit-Limit, X-RateLimit-Remaining,
X-Quota-Limit, X-Quota-Remaining, X-Quota-Reset, X-Quota-Endpoint-Limit,
X-Quota-Endpoint-Remaining, Retry-Afterso all nine are readable where CORS applies — including X-Request-Id, which is the one you want
on a failure, since the rate-limit and quota headers are absent on a 401. See
CORS.
Checking your position
curl -sS -D - -o /dev/null \
-H "Authorization: Bearer $ESPORTSODDS_API_KEY" \
"https://api.esportsodds.gg/v1/cs2/matches?limit=1" \
| grep -iE "x-quota|x-ratelimit|x-request-id"X-Quota-Remaining is the one to alarm on in your own monitoring.
What isn't set
No ETag, Last-Modified or conditional-request support, and no Cache-Control. Responses are not
cacheable by an intermediary — cache in your own layer with a TTL you choose, informed by
how often data actually changes.
Compression is negotiated on /v1/{game}/matches/bulk only (Accept-Encoding: gzip), where a page
is large enough for it to matter. Every other response is uncompressed JSON.
There is no X-RateLimit-Reset; the bucket refills continuously rather than at a boundary, so
Retry-After on a 429 is the meaningful signal.