Headers, CORS & policy

Response headers

Every header the API sets, what it means, and which ones a browser can read.

On every response

HeaderMeaning
X-Request-IdA 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-TypeAlways application/json; charset=utf-8.

On every metered response

Metered means any authenticated /v1/... call — so everything except /health.

HeaderMeaning
X-RateLimit-LimitYour token bucket's burst capacity (100 on both paid plans).
X-RateLimit-RemainingWhole tokens left after this request.
X-Quota-LimitRequests 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-RemainingRequests left in the window, across every key on the account.
X-Quota-ResetWhen 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. The 429 has the code trial_endpoint_quota_exceeded, and Retry-After runs 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 429 has the code bulk_quota_exceeded, and Retry-After runs 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:

HeaderMeaning
X-Quota-Endpoint-LimitThe ceiling for the endpoint you called (the trial's, or 250 for the bulk export).
X-Quota-Endpoint-RemainingAlways 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-After

so 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.

On this page