ParlayAPI · Docs · WebSocket · Streaming

X-Provider-State header

Every response on the /v1/* surface carries a machine-readable freshness map for every data source we ingest from. Your client parses one header per request to populate its own provider-health view, decide whether to trust the body, and route around degraded sources without making a separate health-probe call.

Why this exists. Sophisticated callers (latency-sensitive arb operations, real-time props consumers, model validators) need to disqualify stale data on a per-request basis. The alternative is inferring source health from telemetry, which is slow and noisy. This header is the explicit signal.

Payload shape

Compact JSON, capped at 2 KB so it fits HTTP header limits:

X-Provider-State: {"ts":1778739733,"src":{
  "pinnacle":   {"age_s": 1.2,  "role": "primary"},
  "draftkings": {"age_s": 3.8,  "role": "primary"},
  "fanduel":    {"age_s": 12.8, "role": "primary"},
  "caesars":    {"age_s": 103.4,"role": "degraded"},
  "bet365":     {"age_s": null, "role": "offline"}
}}
FieldTypeMeaning
tsintegerServer unix timestamp at the moment the payload was computed (5-second cache window).
src.<source>.age_snumber or nullSeconds since the most recent write from this source to our ingestion tables. null means no recent writes seen.
src.<source>.rolestringOne of primary, degraded, offline. Classified from age_s against a per-source freshness budget tuned to observed steady-state.

Role values

RoleMeaningRecommended action
primaryLast write within 1.5x the source's expected freshness budget. Source is meeting its polling cadence.Trust normally.
degradedLast write within 6x budget. Source is writing but slower than expected. Often a burst-and-pause writer mid-pause, not a true failure.Use the body but discount the data if your model is latency-sensitive.
offlineLast write past 6x budget, or no recent writes seen. Source is genuinely silent.Do not trust this source's rows for time-sensitive decisions. Pick a different source for the same market.

Per-source freshness budgets

Budgets are tuned to observed p95 write age under normal operation, not theoretical polling intervals. Burst-and-pause writers (Pinnacle, DraftKings) have larger budgets than steady-stream writers because their natural pause windows are real.

SourceBudget (s)Primary threshold (1.5x)Offline threshold (6x)
pinnacle, draftkings, fanduel, betmgm, caesars, bet3653045 s180 s
espnbet4567 s270 s
fanatics, hardrock, betrivers, betparx, bovada45-6067-90 s270-360 s
underdog, prizepicks, kalshi, polymarket, sleeper90135 s540 s
pickem180270 s1080 s
any source not listed90 (default)135 s540 s

Dedicated endpoint (no header limit)

The X-Provider-State header truncates at 2 KB. If you want the full untruncated payload, hit the dedicated endpoint directly. It returns the same shape with no size cap:

GET https://parlay-api.com/v1/meta/provider-state

Response:
{
  "ts": 1778739733,
  "src": {
    "pinnacle":   {"age_s": 1.2,  "role": "primary"},
    "draftkings": {"age_s": 3.8,  "role": "primary"},
    ...
  }
}

No API key required. No credits charged. 5-second server-side cache means polling this every 5 s is the natural maximum useful frequency. Polling faster returns the same payload from cache.

Example: Python client using the header

import httpx, json

async def fetch_odds(sport, markets):
    async with httpx.AsyncClient() as c:
        r = await c.get(
            f"https://parlay-api.com/v1/sports/{sport}/odds",
            params={"regions": "us", "markets": markets},
            headers={"X-API-Key": YOUR_KEY},
        )
        # Parse provider state from header
        state = json.loads(r.headers.get("X-Provider-State", "{}"))
        sources = state.get("src", {})

        # Strip out rows from offline sources before the body is consumed
        body = r.json()
        offline = {k for k, v in sources.items() if v.get("role") == "offline"}
        for event in body:
            event["bookmakers"] = [
                bk for bk in event["bookmakers"]
                if bk["key"] not in offline
            ]
        return body, sources

Example: JavaScript client

const r = await fetch(
  "https://parlay-api.com/v1/sports/baseball_mlb/odds?regions=us&markets=h2h",
  { headers: { "X-API-Key": YOUR_KEY } }
);
const state = JSON.parse(r.headers.get("X-Provider-State") || "{}");
const data = await r.json();

// Filter to primary sources only for time-sensitive decisions
const primaryOnly = data.map(ev => ({
  ...ev,
  bookmakers: ev.bookmakers.filter(bk =>
    state.src?.[bk.key]?.role === "primary"
  )
}));

Use cases

When the header is truncated

If the per-source map would exceed 2 KB (rare; happens during high-coverage windows with 25+ active sources), the encoder drops the worst-freshness sources first and adds a "truncated": true field. To get the full map in those windows, call /v1/meta/provider-state directly.

X-Provider-State: {"ts":1778739733,"src":{"pinnacle":{"age_s":1.2,"role":"primary"}, ...},"truncated":true}

This header was shipped 2026-05-14 in response to customer requests for machine-readable provider state. Per-source freshness budgets are tuned against observed steady-state and revisited as ingestion patterns evolve.