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.
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"}
}}
| Field | Type | Meaning |
|---|---|---|
ts | integer | Server unix timestamp at the moment the payload was computed (5-second cache window). |
src.<source>.age_s | number or null | Seconds since the most recent write from this source to our ingestion tables. null means no recent writes seen. |
src.<source>.role | string | One of primary, degraded, offline. Classified from age_s against a per-source freshness budget tuned to observed steady-state. |
Role values
| Role | Meaning | Recommended action |
|---|---|---|
| primary | Last write within 1.5x the source's expected freshness budget. Source is meeting its polling cadence. | Trust normally. |
| degraded | Last 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. |
| offline | Last 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.
| Source | Budget (s) | Primary threshold (1.5x) | Offline threshold (6x) |
|---|---|---|---|
| pinnacle, draftkings, fanduel, betmgm, caesars, bet365 | 30 | 45 s | 180 s |
| espnbet | 45 | 67 s | 270 s |
| fanatics, hardrock, betrivers, betparx, bovada | 45-60 | 67-90 s | 270-360 s |
| underdog, prizepicks, kalshi, polymarket, sleeper | 90 | 135 s | 540 s |
| pickem | 180 | 270 s | 1080 s |
| any source not listed | 90 (default) | 135 s | 540 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
- Smart fallback. If your primary source is offline, your model can branch to a backup source rather than dropping the prediction entirely.
- Confidence-weighted aggregation. Weight rows by role: primary x1.0, degraded x0.5, offline x0.0.
- Arb / sharpness validators. Reject a sharpness check if either side of the pair is from a degraded source.
- Latency-aware retry. If pinnacle.role becomes degraded, sleep for the budget window (30s) before re-querying instead of retrying immediately.
- Drift alerting. Send an internal alert when a source you depend on transitions to offline.
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}
Related
- AsyncAPI spec describes the streaming surface where Provider-State signals would inform per-frame trust decisions.
- WebSocket docs for clients that subscribe to live updates and want freshness-aware filtering.
- OpenAPI spec for the REST surface.
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.