{"openapi":"3.1.0","info":{"title":"ParlayAPI","description":"Real-time sports odds aggregation from **33 books and data sources** updated every 2-120 seconds depending on source cadence.\n\n**92 sport keys** including MLB, NFL, NBA, NHL, MMA/UFC, esports, volleyball, and 55 soccer competitions.\n\n**Sportsbooks:** DraftKings, FanDuel, Caesars, Bovada, Pinnacle, Fliff.\n\n**DFS/Props:** PrizePicks, Underdog, Betr, Pick6 (DraftKings), Sleeper.\n\n**Exchanges:** Novig, ProphetX.\n\n**Prediction markets:** Kalshi, Polymarket.\n\n**Features:**\n- Player props with American odds from 21 sources\n- Exchange market liquidity (volume, bid/ask)\n- Arbitrage detection across all books\n- +EV finder (sharp vs soft book comparison)\n- Cross-book odds comparison\n- Pinnacle closing lines for CLV analysis\n- Historical odds archive with per-tier depth (see /v1/pricing)\n- Live/in-play odds\n- WebSocket and SSE streaming (Business tier and up)\n\n**Authentication:** Pass `apiKey` as query parameter, `X-API-Key` header, or `Authorization: Bearer `.\n\n**Free tier:** 1,000 credits/month, no credit card. Get a key at [parlay-api.com/signup](https://parlay-api.com/signup). Docs and a browseable request explorer at [parlay-api.com/docs](https://parlay-api.com/docs). Ready-to-import Postman collection at [parlay-api.com/postman](https://parlay-api.com/postman).","version":"3.2.0","x-credit-currency":"credits","x-credit-cost-catalogue-url":"/v1/meta/credit-costs","x-pricing-url":"/v1/pricing","x-usage-url":"/v1/usage","contact":{"name":"ParlayAPI support","url":"https://parlay-api.com/support","email":"support@parlay-api.com"},"license":{"name":"ParlayAPI Terms of Service","url":"https://parlay-api.com/terms"},"termsOfService":"https://parlay-api.com/terms"},"paths":{"/v1/health":{"get":{"summary":"Health Check","description":"Health check: verifies DB connectivity and data freshness.\n\nAliased at /healthz for the failover Cloudflare Worker (k8s\nconvention) and at /v1/health for symmetry with the rest of the\nversioned API surface (#043).","operationId":"health_check_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Health"]},"head":{"summary":"Health Check","description":"Health check: verifies DB connectivity and data freshness.\n\nAliased at /healthz for the failover Cloudflare Worker (k8s\nconvention) and at /v1/health for symmetry with the rest of the\nversioned API surface (#043).","operationId":"health_check_v1_health_head","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Health"]}},"/healthz":{"get":{"summary":"Health Check","description":"Health check: verifies DB connectivity and data freshness.\n\nAliased at /healthz for the failover Cloudflare Worker (k8s\nconvention) and at /v1/health for symmetry with the rest of the\nversioned API surface (#043).","operationId":"health_check_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Health"]},"head":{"summary":"Health Check","description":"Health check: verifies DB connectivity and data freshness.\n\nAliased at /healthz for the failover Cloudflare Worker (k8s\nconvention) and at /v1/health for symmetry with the rest of the\nversioned API surface (#043).","operationId":"health_check_healthz_head","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Health"]}},"/health":{"get":{"summary":"Health Check","description":"Health check: verifies DB connectivity and data freshness.\n\nAliased at /healthz for the failover Cloudflare Worker (k8s\nconvention) and at /v1/health for symmetry with the rest of the\nversioned API surface (#043).","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Health"]}},"/billing/checkout":{"post":{"tags":["billing"],"summary":"Create Checkout","description":"Create a Stripe Checkout Session. Returns checkout URL.","operationId":"create_checkout_billing_checkout_post","parameters":[{"name":"tier","in":"query","required":true,"schema":{"type":"string","title":"Tier"}},{"name":"promo","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Promo"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/portal":{"post":{"tags":["billing"],"summary":"Create Portal","description":"Create Stripe Customer Portal session for self-service management.","operationId":"create_portal_billing_portal_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"get":{"tags":["billing"],"summary":"Open Portal Via Token","description":"Durable billing-portal entry point (the emailed / dashboard link).\n\n`token` is a long-lived, single-purpose, customer-scoped opaque handle\n(billing.get_or_create_portal_token). It is NOT a login or session\ntoken: it can ONLY open this one customer's Stripe billing portal\n(change / cancel plan, update card, view invoices). Because it never\nauthenticates the user into their account and grants no API access, a\nlong TTL is acceptable here in a way it is not for password-reset or\nmagic-login links. It remains revocable and customer-scoped.\n\nOn every visit we mint a FRESH Stripe portal session server-side and\n302 into it, so this link never goes stale even though the underlying\nStripe session URL expires within minutes.","operationId":"open_portal_via_token_billing_portal_get","parameters":[{"name":"token","in":"query","required":false,"schema":{"type":"string","default":"","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/subscription/cancel":{"post":{"tags":["billing"],"summary":"Cancel Subscription Route","description":"Cancel the caller's subscription, self-serve.\n\nDeliberately NOT named /billing/cancel: that path is already the\nCheckout-abandoned landing page, and one word meaning two opposite\nthings is how a customer ends up on the wrong one.\n\nThe mode is chosen from the subscription's own status rather than asked\nof the customer, because only one answer is ever right:\n\n active / trialing -> cancel at period end. They paid for this period,\n they keep it, the key drops to Free when it runs out.\n past_due / unpaid / incomplete -> cancel now. There is no paid period\n left to keep, and at_period_end would leave Stripe retrying the\n failed payment, which is the thing ticket #271 asked us to stop.\n\nAn immediate cancel files a support ticket for the open invoice. We do\nnot void it here on purpose: that is a money decision.","operationId":"cancel_subscription_route_billing_subscription_cancel_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/billing/portal/send-link":{"post":{"tags":["billing"],"summary":"Admin Send Portal Link","description":"Admin-only: email a customer a DURABLE billing-management link.\n\nThis is the sanctioned replacement for pasting a raw Stripe portal\nsession URL into a support reply (which expires in minutes). Give it\neither the account `email` or a `customer_id`.","operationId":"admin_send_portal_link_billing_portal_send_link_post","parameters":[{"name":"email","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"}},{"name":"customer_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/portal/revoke":{"post":{"tags":["billing"],"summary":"Admin Revoke Portal Link","description":"Admin-only: revoke a customer's durable portal token. Any link\nalready emailed stops working immediately.","operationId":"admin_revoke_portal_link_billing_portal_revoke_post","parameters":[{"name":"email","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"}},{"name":"customer_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/success":{"get":{"tags":["billing"],"summary":"Checkout Success","operationId":"checkout_success_billing_success_get","parameters":[{"name":"session_id","in":"query","required":false,"schema":{"type":"string","default":"","title":"Session Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/success-page":{"get":{"tags":["billing"],"summary":"Checkout Success Page Alias","operationId":"checkout_success_page_alias_billing_success_page_get","parameters":[{"name":"session_id","in":"query","required":false,"schema":{"type":"string","default":"","title":"Session Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/billing/cancel":{"get":{"tags":["billing"],"summary":"Checkout Cancel","operationId":"checkout_cancel_billing_cancel_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/billing/cancel-page":{"get":{"tags":["billing"],"summary":"Checkout Cancel Page Alias","operationId":"checkout_cancel_page_alias_billing_cancel_page_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/signup":{"get":{"tags":["auth"],"summary":"Signup Page","operationId":"signup_page_signup_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"tags":["auth"],"summary":"Signup Submit","operationId":"signup_submit_signup_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_signup_submit_signup_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/login":{"get":{"tags":["auth"],"summary":"Login Page","operationId":"login_page_login_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"tags":["auth"],"summary":"Login Submit","operationId":"login_submit_login_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_login_submit_login_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/logout":{"get":{"tags":["auth"],"summary":"Logout","operationId":"logout_logout_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/auth/magic":{"get":{"tags":["auth"],"summary":"Magic Link Login","description":"Land here from a magic-link URL. Sets the session cookie from the\nsid query param and redirects to the dashboard.\n\nUsed by the agent-signup flow: a session is created server-side at\nsignup time and embedded in the URL; visiting the URL claims it.","operationId":"magic_link_login_auth_magic_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/forgot-password":{"get":{"tags":["auth"],"summary":"Forgot Password Page","operationId":"forgot_password_page_forgot_password_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"tags":["auth"],"summary":"Forgot Password Submit","description":"Accept any email, generate a reset token, email the link.\nAlways returns success to avoid enumerating which emails exist.","operationId":"forgot_password_submit_forgot_password_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_forgot_password_submit_forgot_password_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/reset-password":{"get":{"tags":["auth"],"summary":"Reset Password Page","operationId":"reset_password_page_reset_password_get","parameters":[{"name":"token","in":"query","required":false,"schema":{"type":"string","default":"","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["auth"],"summary":"Reset Password Submit","operationId":"reset_password_submit_reset_password_post","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_reset_password_submit_reset_password_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/agent/signup":{"post":{"tags":["agent"],"summary":"Agent Signup","description":"Create a free-tier account for an email and return an API key\nplus a magic-link claim URL the user clicks to verify and access\nthe dashboard.\n\nIdempotent on email: if an account already exists, the API key is\nNOT returned (security), only a login URL the user can use to recover.","operationId":"agent_signup_v1_agent_signup_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/agent/magic-link":{"post":{"tags":["agent"],"summary":"Agent Magic Link","description":"Send a magic login link to an email. Always returns 200 so callers\ncannot enumerate which emails have accounts.","operationId":"agent_magic_link_v1_agent_magic_link_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/agent/checkout-link":{"post":{"tags":["agent"],"summary":"Agent Checkout Link","description":"Generate a Stripe Checkout URL for a tier upgrade.\n\nTwo modes, picked by whether the caller can prove they control the\ntarget email:\n\n 1. **Authenticated agent flow.** Caller sends X-API-Key or\n ?apiKey= and the key's email matches body[\"email\"]. We\n return the Stripe Checkout URL synchronously so the agent\n can paste it to the user. This is the documented happy path\n for an MCP/agent helping the key's owner upgrade.\n\n 2. **Unauthenticated request.** No API key, or the key's email\n doesn't match. We DON'T return a checkout URL. Instead we\n email a one-shot signed link to body[\"email\"] that lands on\n /dashboard?checkout_tier= after magic-link login. The\n recipient has to actually open their inbox and click; an\n attacker can't generate a Stripe-Checkout-bound-to-victim\n URL without controlling the victim's email.\n\nThis closes the phishing vector flagged in the 2026-05-20 audit\nP0 1.12: previously the route returned a Stripe URL for ANY email\nafter passing only the per-IP signup throttle.","operationId":"agent_checkout_link_v1_agent_checkout_link_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard":{"get":{"tags":["dashboard"],"summary":"Dashboard Page","operationId":"dashboard_page_dashboard_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/dashboard/billing":{"get":{"tags":["dashboard"],"summary":"Billing Page","description":"Self-serve tier upgrade / downgrade page.\n\nThe legacy /dashboard/billing link from the dashboard upgrade\nbanner used to 404 (the page was referenced but not implemented).\nCustomers who'd hit their credit cap saw the 'Upgrade tier' button,\nclicked it, and landed on a dead page, then went hunting through\nthe Stripe portal where the only visible action was 'Cancel'. One\nconfirmed support ticket (Bryce, 2026-05-11) from this exact path.\n\nThis page shows the customer's current tier, recent usage, and\none-click upgrade buttons that POST directly to /billing/checkout\nfor each higher tier. Downgrade routes through the Stripe Customer\nPortal which they can also reach from here.","operationId":"billing_page_dashboard_billing_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/dashboard/billing/cancel":{"get":{"tags":["dashboard"],"summary":"Billing Cancel Page","description":"Confirmation step for a self-serve cancellation.\n\nA cancel that happens on one click is a cancel people undo by email, so\nthis page states what will actually happen before the POST: which plan,\nwhich date access runs to, that the key keeps working on Free\nafterwards, and what the refund policy is. The POST target is\n/billing/subscription/cancel, which picks cancel-at-period-end or\ncancel-now from the subscription's Stripe status.\n\nStripe is read here only to fill in the date and the status. If that\nread fails we still render the page with the button, because the one\noutcome we must never produce is a customer who came here to stop\npaying and found nothing to click.","operationId":"billing_cancel_page_dashboard_billing_cancel_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/support":{"get":{"tags":["public_support"],"summary":"Support Page","description":"Public contact form. No auth.","operationId":"support_page_support_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"post":{"tags":["public_support"],"summary":"Support Submit","operationId":"support_submit_support_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_support_submit_support_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/webhooks":{"get":{"tags":["webhooks"],"summary":"List Webhooks","operationId":"list_webhooks_v1_webhooks_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["webhooks"],"summary":"Create Webhook","operationId":"create_webhook_v1_webhooks_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/webhooks/{webhook_id}":{"patch":{"tags":["webhooks"],"summary":"Update Webhook","operationId":"update_webhook_v1_webhooks__webhook_id__patch","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"integer","title":"Webhook Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["webhooks"],"summary":"Get Webhook","description":"Look up a single webhook config by id. iter_054 #446: was 405.\nCustomer who lost the create-time secret can at least re-fetch the\nrest of their config. (Secret itself is NEVER returned again; ours\nis a one-time-show secret per Stripe convention.)","operationId":"get_webhook_v1_webhooks__webhook_id__get","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"integer","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["webhooks"],"summary":"Delete Webhook","operationId":"delete_webhook_v1_webhooks__webhook_id__delete","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"integer","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/webhooks/{webhook_id}/rotate-secret":{"post":{"tags":["webhooks"],"summary":"Rotate Webhook Secret","description":"Generate a new HMAC secret for this webhook. Returns the new\nsecret exactly once. Use this when a secret has leaked or on a\nperiodic rotation schedule; for config edits without rotation, use\nPATCH instead.","operationId":"rotate_webhook_secret_v1_webhooks__webhook_id__rotate_secret_post","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"integer","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/webhooks/{webhook_id}/deliveries":{"get":{"tags":["webhooks"],"summary":"List Webhook Deliveries","description":"List recent delivery attempts for a webhook. iter_054 #446:\nreliability monitoring is the #2 most-requested webhook feature.\nStores up to ~30 days of delivery history per webhook in the\nwebhook_deliveries table; older rows are pruned by the retention\ncleanup.","operationId":"list_webhook_deliveries_v1_webhooks__webhook_id__deliveries_get","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"integer","title":"Webhook Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter: 'success', 'failed', or omit for all","title":"Status"},"description":"Filter: 'success', 'failed', or omit for all"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/webhooks/{webhook_id}/test":{"post":{"tags":["webhooks"],"summary":"Test Webhook","description":"Fire a `test` event so the customer can verify their endpoint.","operationId":"test_webhook_v1_webhooks__webhook_id__test_post","parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"integer","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/live":{"get":{"tags":["live"],"summary":"Live Page","description":"Serve the live odds dashboard.","operationId":"live_page_live_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/live/api/data_flow":{"get":{"tags":["live"],"summary":"Live Data Flow","description":"Per-source freshness for the /live page status strip.\n\nFor each active sportsbook reports the last time we POLLED the\nupstream, not the last time we wrote a price-change row. The two\ndiffer: change-detection suppresses no-change writes, so\nMAX(timestamp_ms) on prop_snapshots reports the last price MOVE,\nwhich for many books is much older than the last poll cycle. Using\npoll_pulses (Redis-backed heartbeats, written every poll cycle\nregardless of change-detection) gives the true \"last poll\" age.\n\nFalls back to MAX(timestamp_ms) for sources that don't write pulses\nyet, so we keep the chip visible (with an honest 'fallback: true'\ntag so we can audit which sources still need pulse coverage).\n\nCached 5s, public, no auth.","operationId":"live_data_flow_live_api_data_flow_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/live/api/sports":{"get":{"tags":["live"],"summary":"Live Sports","description":"Active sports with event counts. No auth required. Cached 60s.","operationId":"live_sports_live_api_sports_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/live/api/games":{"get":{"tags":["live"],"summary":"Live Games","description":"Games for a sport with odds preview. Anonymous users get limited books.","operationId":"live_games_live_api_games_get","parameters":[{"name":"sport","in":"query","required":true,"schema":{"type":"string","title":"Sport"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/live/api/game/{event_id}":{"get":{"tags":["live"],"summary":"Live Game Detail","description":"Full game detail with all books + props.\n\nCustomer report 2026-05-21: a Scale-tier account hit\n\"credits_exhausted\" clicking a game card. Two bugs were stacked:\n\n1) _get_user_api_key (since removed) returned api_key_display, the\n masked 8-char prefix like 'sk_live_...', NOT the full key. The\n hash of that masked string never matched any stored hash, so\n _get_tier returned None and charge_credits returned False —\n surfaced to the user as \"credits_exhausted\" but the real failure\n was key lookup.\n\n2) Charging a credit for the LIVE DASHBOARD didn't match policy.\n The new /live/api/game/{event_id}/view (the dedicated single-\n game page) is free; the customer dashboard is free to browse.\n Credits are for programmatic API egress, not dashboard reads.\n\nSo this endpoint is now free for any logged-in user, matching the\nnew view endpoint's policy.","operationId":"live_game_detail_live_api_game__event_id__get","parameters":[{"name":"event_id","in":"path","required":true,"schema":{"type":"string","title":"Event Id"}},{"name":"sport","in":"query","required":true,"schema":{"type":"string","title":"Sport"}},{"name":"home","in":"query","required":false,"schema":{"type":"string","title":"Home"}},{"name":"away","in":"query","required":false,"schema":{"type":"string","title":"Away"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/live/game/{event_id}":{"get":{"tags":["live"],"summary":"Live Game Page","description":"Single-game live view for a retail bettor watching the game.\n\nServer renders just the shell; the page calls\n/live/api/game/{event_id}/view to populate, then opens a WebSocket\nfor live updates.","operationId":"live_game_page_live_game__event_id__get","parameters":[{"name":"event_id","in":"path","required":true,"schema":{"type":"string","title":"Event Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/live/api/game/{event_id}/view":{"get":{"tags":["live"],"summary":"Live Game View Json","description":"JSON for the single-game retail live view.\n\nAnonymous: returns top-3-books table with sharp anchor visible.\nLogged in: full table + props (no credit charge — this is the\ncustomer-facing pricing view; we charge for API egress, not for\nlooking at the dashboard).","operationId":"live_game_view_json_live_api_game__event_id__view_get","parameters":[{"name":"event_id","in":"path","required":true,"schema":{"type":"string","title":"Event Id"}},{"name":"sport","in":"query","required":true,"schema":{"type":"string","title":"Sport"}},{"name":"home","in":"query","required":false,"schema":{"type":"string","title":"Home"}},{"name":"away","in":"query","required":false,"schema":{"type":"string","title":"Away"}},{"name":"history_window_s","in":"query","required":false,"schema":{"type":"integer","maximum":1800,"minimum":30,"description":"History window for movement calc (default 5 min).","default":300,"title":"History Window S"},"description":"History window for movement calc (default 5 min)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/live/api/search":{"get":{"tags":["live"],"summary":"Live Search","description":"Search teams/players/tournaments across all sports. No auth required.\n\nSport-key matching uses the user's literal query plus a space-to-\nunderscore-normalized variant, so 'ITF Kurume' matches\n'tennis_itf_women_kurume' (the sport_key the FD in-play source\nemits for that tournament).\n\niter_049 #423: capped at `?limit=` (default 25) so broad-substring\nqueries like 'mlb' don't blow up trigram-index work. Was previously\nunbounded; high-cardinality queries timed out at 20s.","operationId":"live_search_live_api_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Max results to return. iter_049 #423: previously unbounded; broad queries like 'mlb' could time out. Default 25.","default":25,"title":"Limit"},"description":"Max results to return. iter_049 #423: previously unbounded; broad queries like 'mlb' could time out. Default 25."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/live/api/command_center":{"get":{"tags":["live"],"summary":"Live Command Center","description":"Best-line and book-breadth snapshot for the /live dashboard.","operationId":"live_command_center_live_api_command_center_get","parameters":[{"name":"sport","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional sport key","title":"Sport"},"description":"Optional sport key"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":12,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/live/api/pbp":{"get":{"tags":["live"],"summary":"Live Pbp","description":"Recent play-by-play events for a sport (or specific game). Free, no auth.\nPowers the 'What's happening' marquee on the /live page.","operationId":"live_pbp_live_api_pbp_get","parameters":[{"name":"sport","in":"query","required":true,"schema":{"type":"string","description":"Sport key, e.g. basketball_nba","title":"Sport"},"description":"Sport key, e.g. basketball_nba"},{"name":"event_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Match id; omit for sport-wide","title":"Event Id"},"description":"Match id; omit for sport-wide"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":20,"minimum":1,"default":5,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/live/api/disagreement":{"get":{"tags":["live"],"summary":"Live Disagreement","description":"Top cross-book line disagreements right now, across all sports.\nFree + no auth (showcase teaser). Updates every ~15s.\n\nPowers the 'Books disagree right now' sidebar on /live.\nFull per-event detail with all books still requires the Pro+\n/v1/sports/{sport_key}/live/disagreement endpoint.\n\nUses singleflight via cached_call so concurrent misses do not fan\nout to N DB queries (the thundering-herd that pinned Postgres\nearlier tonight).","operationId":"live_disagreement_live_api_disagreement_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":25,"minimum":1,"description":"Top-N disagreements across all sports","default":8,"title":"Limit"},"description":"Top-N disagreements across all sports"},{"name":"min_deviation_pct","in":"query","required":false,"schema":{"type":"number","maximum":1,"minimum":0,"description":"Min cross-book deviation to surface","default":0.025,"title":"Min Deviation Pct"},"description":"Min cross-book deviation to surface"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/live/api/sparkline":{"get":{"tags":["live"],"summary":"Live Sparkline","description":"Recent line history for a single game, suitable for a tiny\ninline sparkline on the live game card. Returns one point per minute\nof moneyline (or h2h) movement across all books, averaged.\nFree + no auth. Updates every 30s.","operationId":"live_sparkline_live_api_sparkline_get","parameters":[{"name":"sport","in":"query","required":true,"schema":{"type":"string","title":"Sport"}},{"name":"home_team","in":"query","required":true,"schema":{"type":"string","title":"Home Team"}},{"name":"away_team","in":"query","required":true,"schema":{"type":"string","title":"Away Team"}},{"name":"minutes","in":"query","required":false,"schema":{"type":"integer","maximum":180,"minimum":5,"default":30,"title":"Minutes"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/live/api/best-books":{"get":{"tags":["live"],"summary":"Live Best Books","description":"Rank sportsbooks by how good their prices have actually been.\n\nOver the look-back window we sample each book's price per game at 15-minute\nbuckets. At every (game, side, bucket) the book(s) holding the best\navailable American price are credited a win, and each book's price is\nscored against the field's median (consensus). The response is a\nleaderboard so a bettor can see, for this sport and window, which book to\nline-shop first.\n\nv1 covers h2h (moneyline) from odds_snapshots, which carries ~24 books\ndirectly (DraftKings, FanDuel, Pinnacle, BetRivers, ...). Spreads, totals,\nprops, and per-game / per-book-type slices are the next increments.","operationId":"live_best_books_live_api_best_books_get","parameters":[{"name":"sport","in":"query","required":true,"schema":{"type":"string","description":"Sport key, e.g. baseball_mlb (family umbrellas like soccer are expanded).","title":"Sport"},"description":"Sport key, e.g. baseball_mlb (family umbrellas like soccer are expanded)."},{"name":"market","in":"query","required":false,"schema":{"type":"string","description":"Bet type. v1 supports h2h (moneyline); spreads/totals/props coming.","default":"h2h","title":"Market"},"description":"Bet type. v1 supports h2h (moneyline); spreads/totals/props coming."},{"name":"window_hours","in":"query","required":false,"schema":{"type":"integer","maximum":336,"minimum":1,"description":"Look-back window in hours (1..336).","default":24,"title":"Window Hours"},"description":"Look-back window in hours (1..336)."},{"name":"home","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional: restrict to one game by home team name (case-insensitive substring; orientation-agnostic).","title":"Home"},"description":"Optional: restrict to one game by home team name (case-insensitive substring; orientation-agnostic)."},{"name":"away","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional: restrict to one game by away team name.","title":"Away"},"description":"Optional: restrict to one game by away team name."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/best-books":{"get":{"tags":["live"],"summary":"Best Books Page","description":"The line-shopping board: which sportsbook prices best, with sport /\nwindow / bet-type toggles. Consumes /live/api/best-books.","operationId":"best_books_page_best_books_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/moves.json":{"get":{"summary":"Moves Json","description":"**Free line-movement feed (JSON).** The biggest pre-game moneyline\nmoves across major sports: cross-book consensus with the vig removed,\nrefreshed every 15 minutes. No auth, no credits. Free to republish\nwith a link back to https://parlay-api.com/moves. Also served as HTML\n(/moves) and RSS 2.0 (/moves.rss).","operationId":"moves_json_moves_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/divergence.json":{"get":{"summary":"Divergence Json","description":"**Free prediction-market divergence board (JSON).** Where Kalshi\nand Polymarket prices disagree with the vig-free sportsbook\nconsensus (median across 3+ books) for the same pre-game events,\nranked by absolute gap, refreshed every 15 minutes. No auth, no\ncredits. Free to republish with a link back to\nhttps://parlay-api.com/divergence. Also served as HTML\n(/divergence) and RSS 2.0 (/divergence.rss).","operationId":"divergence_json_divergence_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/sports/{sport_key}/live/sse":{"get":{"summary":"Sports Live Sse","description":"Server-Sent Events stream of live state changes.\n\nTier-gated: paid tier (starter+). 5 credits at connection time.","operationId":"sports_live_sse_v1_sports__sport_key__live_sse_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"match_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Match ID to subscribe to. Omit or use '*' for all live matches in this sport.","title":"Match Id"},"description":"Match ID to subscribe to. Omit or use '*' for all live matches in this sport."},{"name":"apiKey","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs.","title":"Apikey"},"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":5,"x-credit-cost-type":"fixed","x-credit-cost-description":"Live play-by-play SSE stream connection","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/live/points":{"get":{"summary":"Sports Live Points","description":"One-shot snapshot of live state. Free tier OK. 1 credit per call.","operationId":"sports_live_points_v1_sports__sport_key__live_points_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"match_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Match ID. Omit for all currently in-play matches in this sport.","title":"Match Id"},"description":"Match ID. Omit for all currently in-play matches in this sport."},{"name":"apiKey","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs.","title":"Apikey"},"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":1,"x-credit-cost-type":"fixed","x-credit-cost-description":"Live play-by-play state snapshot","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/live/book_latency":{"get":{"summary":"Sports Live Book Latency","description":"Per-book latency for live games in this sport.\n\nFor each book (DK / FD / Caesars / BetMGM / Pinnacle / etc.) we\ncompute lag_seconds = primary_PBP_age - book_odds_last_update.\nPositive lag means the book is behind reality (their lines might be\nstale; exploit before rebalance).\n\nUse case: arb scanner customers query this every few seconds and\nsurface \"FD has 8s lag on Yankees-Rangers right now\" alerts.\n\nPro+ tier. 5 credits per call (rich derived data).","operationId":"sports_live_book_latency_v1_sports__sport_key__live_book_latency_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"apiKey","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs.","title":"Apikey"},"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":5,"x-credit-cost-type":"fixed","x-credit-cost-description":"Per-book lag relative to live game state","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/live/source-health":{"get":{"summary":"Sports Live Source Health","description":"Customer-facing source-freshness diagnostic.\n\nReturns per-source freshness for the requested sport: which feeds\nare emitting events, when they last did, and how many events landed\nin the last 5 minutes. Use this to detect when a source goes stale\nso your bot doesn't trade on dead data.\n\nResponse shape (one row per source actively emitting for this sport):\n [\n {\n \"sport_key\": \"basketball_nba\",\n \"source\": \"nba_live\",\n \"role\": \"primary\",\n \"events_last_5min\": 142,\n \"seconds_since_last_event\": 3.2,\n \"latest_capture_ms\": 1778214028111\n },\n ...\n ]\n\nFree tier OK. 1 credit per call. Recommended polling cadence: every\n30 seconds. If a source you care about shows seconds_since_last_event\n> 60 during a known-live game, that source has gone stale; failover\nto another source or the league-API path.\n\n**Empty-when-healthy semantics** (iter_064 #479): the pbp_source_health\ntable is populated by the source-health monitor only when a feed\ntransitions into a degraded state (slow / silent / errored). A\nresponse of `{count: 0, results: []}` means every tracked source\nfor the sport is firing within its expected cadence. Treat empty\nas a positive signal, not a missing-data signal. To verify the\nunderlying source is actually live, cross-reference\n`/live/api/data_flow` which always returns the per-source pulse\nsnapshot.","operationId":"sports_live_source_health_v1_sports__sport_key__live_source_health_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"apiKey","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs.","title":"Apikey"},"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":1,"x-credit-cost-type":"fixed","x-credit-cost-description":"Per-sport live source freshness diagnostic","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/live/period_markets":{"get":{"summary":"Sports Live Period Markets","description":"Return latest period market lines for a sport.\n\nOpen to all tiers. 2 credits per call.\n\nThe apiKey query param above is declared OPTIONAL on purpose. It used\nto be `Query(...)`, which made FastAPI 422 \"Field required\" before\nthis body ever ran whenever a customer sent the key the way docs.html\nrecommends (X-API-Key header) or as Authorization: Bearer. Leaving it\noptional lets require_api_key below resolve all three forms, with\nheaders beating query so an appended &apiKey= cannot clobber a key\nsent securely.","operationId":"sports_live_period_markets_v1_sports__sport_key__live_period_markets_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"period","in":"query","required":false,"schema":{"type":"string","description":"FT / 1H / 2H / Q1 / Q2 / Q3 / Q4 / OT or 'all'","default":"all","title":"Period"},"description":"FT / 1H / 2H / Q1 / Q2 / Q3 / Q4 / OT or 'all'"},{"name":"match_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional source-native match id","title":"Match Id"},"description":"Optional source-native match id"},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional book filter","title":"Source"},"description":"Optional book filter"},{"name":"market","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional market filter (spread/total/h2h)","title":"Market"},"description":"Optional market filter (spread/total/h2h)"},{"name":"apiKey","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs.","title":"Apikey"},"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":2,"x-credit-cost-type":"fixed","x-credit-cost-description":"In-game period spreads, totals, and h2h","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/live/disagreement":{"get":{"summary":"Sports Live Disagreement","description":"Cross-book disagreement diagnostic for in-play markets.\n\nReturns one row per (match, period, market, side) with each book's\nlatest line + price plus deviation from a chosen anchor (median or\nPinnacle). Use this to find +EV opportunities during fast scoring\nruns where soft books haven't caught up to sharp lines yet.\n\nTier gate: Pro+. 5 credits per call.\n\nExample response (basketball_nba Q3 total):\n {\n \"sport_key\": \"basketball_nba\",\n \"period\": \"Q3\",\n \"market\": \"total\",\n \"anchor\": \"median\",\n \"as_of_ms\": 1778342828915,\n \"results\": [\n {\n \"match_id\": \"0042500301\",\n \"home_team\": \"Lakers\", \"away_team\": \"Thunder\",\n \"side\": \"over\",\n \"anchor_line\": 51.5,\n \"books\": [\n {\"source\": \"pinnacle\", \"line\": 51.5, \"price\": -110,\n \"deviation_abs\": 0.0, \"deviation_pct\": 0.0,\n \"age_seconds\": 2.1},\n {\"source\": \"dk_web\", \"line\": 50.5, \"price\": -115,\n \"deviation_abs\": 1.0, \"deviation_pct\": 0.0194,\n \"age_seconds\": 1.4}\n ]\n }\n ]\n }\n\nFilter on `deviation_pct > 0.02` client-side to surface the actually\nactionable disagreements (anything within median noise gets dropped).\n\napiKey is optional as a query param because the key may equally be\nsent as X-API-Key or Authorization: Bearer; see the module docstring.","operationId":"sports_live_disagreement_v1_sports__sport_key__live_disagreement_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"period","in":"query","required":false,"schema":{"type":"string","description":"FT / 1H / 2H / Q1-Q4 / OT / P1-P3 / F5 / F7 or 'all'","default":"all","title":"Period"},"description":"FT / 1H / 2H / Q1-Q4 / OT / P1-P3 / F5 / F7 or 'all'"},{"name":"market","in":"query","required":false,"schema":{"type":"string","description":"spread / total / h2h","default":"total","title":"Market"},"description":"spread / total / h2h"},{"name":"side","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional side filter (over/under for total, home/away for spread/h2h). Omit to get all sides per book.","title":"Side"},"description":"Optional side filter (over/under for total, home/away for spread/h2h). Omit to get all sides per book."},{"name":"anchor","in":"query","required":false,"schema":{"type":"string","description":"Reference for deviation: 'median' (across all books) or 'pinnacle'","default":"median","title":"Anchor"},"description":"Reference for deviation: 'median' (across all books) or 'pinnacle'"},{"name":"max_age_s","in":"query","required":false,"schema":{"type":"integer","maximum":300,"minimum":1,"description":"Drop books whose latest observation is older than this many seconds. Default 90s catches Bovada's slower polling cycle alongside Pinnacle's tight one. Lower it for stricter freshness, raise it for sparse periods (deep Q4, OT).","default":90,"title":"Max Age S"},"description":"Drop books whose latest observation is older than this many seconds. Default 90s catches Bovada's slower polling cycle alongside Pinnacle's tight one. Lower it for stricter freshness, raise it for sparse periods (deep Q4, OT)."},{"name":"min_books","in":"query","required":false,"schema":{"type":"integer","maximum":10,"minimum":1,"description":"Minimum number of books required to emit a row. Default 2 because rows with n_books=1 can't be disagreeing with anyone (max_deviation_pct=0 always). Pass 1 to also see markets where only one book has a recent quote (useful for diagnosing which books are slow to refresh).","default":2,"title":"Min Books"},"description":"Minimum number of books required to emit a row. Default 2 because rows with n_books=1 can't be disagreeing with anyone (max_deviation_pct=0 always). Pass 1 to also see markets where only one book has a recent quote (useful for diagnosing which books are slow to refresh)."},{"name":"apiKey","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs.","title":"Apikey"},"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":5,"x-credit-cost-type":"fixed","x-credit-cost-description":"Cross-book in-play line disagreement","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/live/period_markets/sources":{"get":{"summary":"Sports Live Period Sources","description":"List which books currently have period markets for this sport.\n\nOpen to all tiers. 1 credit per call.\n\napiKey is optional as a query param because the key may equally be\nsent as X-API-Key or Authorization: Bearer; see the module docstring.","operationId":"sports_live_period_sources_v1_sports__sport_key__live_period_markets_sources_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"apiKey","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs.","title":"Apikey"},"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":1,"x-credit-cost-type":"fixed","x-credit-cost-description":"List books with period data for a sport","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/historical/sports/{sport_key}/period_markets":{"get":{"summary":"Historical Period Markets","description":"Durable per-distinct-state archive of period market line movement.\n\nEach row is one (match_id, source, period_key, market, side, line,\nprice) state with `first_seen_ms` (when the book first wrote that\nstate) and `last_seen_ms` (the latest poll that re-confirmed it).\n\nReplaying movement: order by first_seen_ms within a (match, period,\nmarket, side) group; consecutive distinct line / price values are\nthe moves. The same archive feeds both pre-game and in-play movement,\nso a Q3 line that opens at 55.5, drops to 54.5 mid-game, and closes\nat 53.5 produces three rows.\n\nTier gating: 5 credits per call (mirrors /historical/closing-odds).\n\napiKey is optional as a query param because the key may equally be\nsent as X-API-Key or Authorization: Bearer; see the module docstring.","operationId":"historical_period_markets_v1_historical_sports__sport_key__period_markets_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"period","in":"query","required":false,"schema":{"type":"string","description":"FT / 1H / 2H / Q1-Q4 / OT / P1-P3 or 'all'","default":"all","title":"Period"},"description":"FT / 1H / 2H / Q1-Q4 / OT / P1-P3 or 'all'"},{"name":"match_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional source-native match id","title":"Match Id"},"description":"Optional source-native match id"},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional book filter","title":"Source"},"description":"Optional book filter"},{"name":"market","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional market filter (spread/total/h2h)","title":"Market"},"description":"Optional market filter (spread/total/h2h)"},{"name":"home_team","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by home team substring","title":"Home Team"},"description":"Filter by home team substring"},{"name":"away_team","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by away team substring","title":"Away Team"},"description":"Filter by away team substring"},{"name":"date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Specific date YYYY-MM-DD (shortcut for dateFrom=dateTo=date)","title":"Date"},"description":"Specific date YYYY-MM-DD (shortcut for dateFrom=dateTo=date)"},{"name":"dateFrom","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"YYYY-MM-DD inclusive","title":"Datefrom"},"description":"YYYY-MM-DD inclusive"},{"name":"dateTo","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"YYYY-MM-DD inclusive","title":"Dateto"},"description":"YYYY-MM-DD inclusive"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":20000,"minimum":1,"default":5000,"title":"Limit"}},{"name":"apiKey","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs.","title":"Apikey"},"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":5,"x-credit-cost-type":"fixed","x-credit-cost-description":"Historical period odds archive","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Historical"]}},"/v1/sports/{sport_key}/injuries":{"get":{"summary":"Sports Injuries","description":"Return current injury records for a sport.\n\nData source: ESPN's public core API, refreshed by the collector\nevery ~10 minutes. Includes status, IL category, body part, side,\nexpected return date, and short comment.\n\nSports supported: baseball_mlb, basketball_nba, basketball_wnba,\nicehockey_nhl, americanfootball_nfl. Other sport_keys return 400.\n\nOpen to all tiers. 1 credit per call.","operationId":"sports_injuries_v1_sports__sport_key__injuries_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"athlete","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional case-insensitive substring match on athlete name","title":"Athlete"},"description":"Optional case-insensitive substring match on athlete name"},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional filter on ESPN status string (e.g. '15-Day-IL')","title":"Status"},"description":"Optional filter on ESPN status string (e.g. '15-Day-IL')"},{"name":"apiKey","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs.","title":"Apikey"},"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":1,"x-credit-cost-type":"fixed","x-credit-cost-description":"ESPN injury records for a sport","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/injuries/{athlete_name}":{"get":{"summary":"Injury Lookup","description":"Single-athlete injury lookup. Exact name match (case-insensitive\non the input; the cache key is ESPN's `fullName`).\n\nReturns 404 if athlete is not in the injury cache (which usually\nmeans they are NOT injured, but could also mean they're not yet\nrostered or the collector hasn't seen them).\n\n1 credit per call.","operationId":"injury_lookup_v1_sports__sport_key__injuries__athlete_name__get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"athlete_name","in":"path","required":true,"schema":{"type":"string","title":"Athlete Name"}},{"name":"apiKey","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs.","title":"Apikey"},"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":1,"x-credit-cost-type":"fixed","x-credit-cost-description":"Single-athlete injury lookup","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/news":{"get":{"summary":"Sports News","description":"Recent ESPN news headlines for a sport.\n\nUseful for tagging prop_snapshots with game-state context: rain\ndelays, lineup changes, scratched players. The collector refreshes\nevery 5 min from ESPN's public news feed.\n\nOpen to all tiers. 1 credit per call.","operationId":"sports_news_v1_sports__sport_key__news_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"since_hours","in":"query","required":false,"schema":{"type":"integer","maximum":168,"minimum":1,"description":"Look-back window in hours","default":24,"title":"Since Hours"},"description":"Look-back window in hours"},{"name":"keyword","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional case-insensitive headline substring match","title":"Keyword"},"description":"Optional case-insensitive headline substring match"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Max articles to return","default":50,"title":"Limit"},"description":"Max articles to return"},{"name":"apiKey","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs.","title":"Apikey"},"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":1,"x-credit-cost-type":"fixed","x-credit-cost-description":"ESPN news headlines for a sport","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/baseball_mlb/probable-pitchers":{"get":{"summary":"Mlb Probable Pitchers","description":"List today's (and next 2 days') MLB probable starting pitchers.\n\nEach game returns home/away probable pitcher names and IDs, the\nvenue, scheduled game time (UTC), and game status. Pitcher IDs\nare MLB's canonical Person IDs from Stats API.\n\nSourced from MLB Stats API (statsapi.mlb.com) — official, free,\nno auth. Refreshed hourly.\n\nOpen to all tiers. 1 credit per call.","operationId":"mlb_probable_pitchers_v1_sports_baseball_mlb_probable_pitchers_get","parameters":[{"name":"date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"YYYY-MM-DD. Default: all upcoming dates in cache (~3 days)","title":"Date"},"description":"YYYY-MM-DD. Default: all upcoming dates in cache (~3 days)"},{"name":"apiKey","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs.","title":"Apikey"},"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":1,"x-credit-cost-type":"fixed","x-credit-cost-description":"Today's + next 2 days MLB probable starting pitchers","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/alerts/prop-line":{"post":{"tags":["alerts"],"summary":"Create Prop Line Alert","description":"Register a new prop-line threshold alert.\n\nCost: free (configuration only). Tier gate: Pro+.","operationId":"create_prop_line_alert_v1_alerts_prop_line_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["alerts"],"summary":"List Prop Line Alerts","description":"List alerts owned by the calling user.","operationId":"list_prop_line_alerts_v1_alerts_prop_line_get","parameters":[{"name":"enabled_only","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Enabled Only"}},{"name":"sport_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sport Key"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/alerts/prop-line/{alert_id}":{"get":{"tags":["alerts"],"summary":"Get Prop Line Alert","description":"Single-alert detail.","operationId":"get_prop_line_alert_v1_alerts_prop_line__alert_id__get","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"integer","title":"Alert Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["alerts"],"summary":"Patch Prop Line Alert","description":"Toggle enabled / update cooldown / change threshold or\ndirection / set expiry. NEVER changes the underlying webhook\nor the sport/player/market identity; for those, delete and\nre-register.","operationId":"patch_prop_line_alert_v1_alerts_prop_line__alert_id__patch","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"integer","title":"Alert Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertPatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["alerts"],"summary":"Delete Prop Line Alert","operationId":"delete_prop_line_alert_v1_alerts_prop_line__alert_id__delete","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"integer","title":"Alert Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/clv/import-screenshot":{"post":{"tags":["clv"],"summary":"Import Bet Screenshot","description":"Extract structured bets from a sportsbook screenshot.\n\nDefault off via BET_IMPORT_ENABLED=0. When enabled, calls Claude's\nvision API to parse the image. Returns {bets, model, warnings}.\n\nCost: 20 + 5*N credits where N = recognized bets.","operationId":"import_bet_screenshot_v1_clv_import_screenshot_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_import_bet_screenshot_v1_clv_import_screenshot_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost-formula":"20 + 5 x recognized_bets","x-credit-cost-floor":20,"x-credit-cost-type":"variable","x-credit-cost-example":"4 recognized bets = 20 + 5x4 = 40 credits","x-credit-cost-description":"OCR a bet-slip screenshot into a structured leg list","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}]}},"/v1/affiliates":{"get":{"summary":"List Affiliates","description":"List all active affiliate links for a country.\n\nNo API key required (public endpoint). Returns book_key, name, bonus\ntext, and a relative redirect URL the client should use (so we get\nthe click attribution).","operationId":"list_affiliates_v1_affiliates_get","parameters":[{"name":"country","in":"query","required":false,"schema":{"type":"string","default":"US","title":"Country"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/go/{book_key}":{"get":{"summary":"Affiliate Redirect","description":"Redirect to the affiliate signup URL, log the click.","operationId":"affiliate_redirect_go__book_key__get","parameters":[{"name":"book_key","in":"path","required":true,"schema":{"type":"string","title":"Book Key"}},{"name":"utm_source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Where the click came from: ai-claude, ai-chatgpt, partners-page, response-inline, etc","title":"Utm Source"},"description":"Where the click came from: ai-claude, ai-chatgpt, partners-page, response-inline, etc"},{"name":"utm_campaign","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Utm Campaign"}},{"name":"country","in":"query","required":false,"schema":{"type":"string","default":"US","title":"Country"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/partners":{"get":{"summary":"Partners Page","description":"Public partners page. Lists every active affiliate book with a\nbonus tile users can click. Uses /go/{book_key} for redirect so we\nget attribution.","operationId":"partners_page_partners_get","parameters":[{"name":"country","in":"query","required":false,"schema":{"type":"string","default":"US","title":"Country"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/terms":{"get":{"summary":"Terms Of Service","operationId":"terms_of_service_terms_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/legal/tos":{"get":{"summary":"Terms Of Service","operationId":"terms_of_service_legal_tos_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/legal/terms":{"get":{"summary":"Terms Of Service","operationId":"terms_of_service_legal_terms_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/privacy":{"get":{"summary":"Privacy Policy","operationId":"privacy_policy_privacy_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/legal/privacy":{"get":{"summary":"Privacy Policy","operationId":"privacy_policy_legal_privacy_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/acceptable-use":{"get":{"summary":"Acceptable Use Policy","operationId":"acceptable_use_policy_acceptable_use_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/legal/aup":{"get":{"summary":"Acceptable Use Policy","operationId":"acceptable_use_policy_legal_aup_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/legal/acceptable-use":{"get":{"summary":"Acceptable Use Policy","operationId":"acceptable_use_policy_legal_acceptable_use_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/dmca":{"get":{"summary":"Dmca Policy","operationId":"dmca_policy_dmca_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/legal/dmca":{"get":{"summary":"Dmca Policy","operationId":"dmca_policy_legal_dmca_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/legal":{"get":{"summary":"Legal Index","operationId":"legal_index_legal_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/collections/ParlayAPI.postman_collection.json":{"get":{"summary":"Postman Collection Curated","description":"Curated Postman 2.1 collection: 30 requests across six folders.\n\nImport into Postman, Insomnia, Bruno or HTTPie with Import > Link and\nthis URL. The first folder is keyless: seven requests that run against\nlive production with no API key and no signup.\n\nPublic, no auth, no credits. The auto-generated collection covering\nevery endpoint is at /collections/postman.json.","operationId":"postman_collection_curated_collections_ParlayAPI_postman_collection_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/datasets/nfl-week-1-opening-lines-2026.csv":{"get":{"summary":"Week1 Opening Lines Csv","description":"NFL Week 1 2026 opening line vs current line, one row per\ngame / book / market / side.\n\nFree, no auth, no credits, free to redistribute with attribution and\na link back to /nfl/week-1-opening-lines. Column reference and the\nexact definition of \"opening line\" are on that page.","operationId":"week1_opening_lines_csv_datasets_nfl_week_1_opening_lines_2026_csv_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/datasets/nfl-week-1-opening-lines-2026.json":{"get":{"summary":"Week1 Opening Lines Json","description":"Machine-readable summary of the Week 1 opening-lines dataset:\ncounts, the as-of timestamp, the game and book lists, what was\nexcluded and why, and the biggest opener-to-now consensus moves.\n\nFree, no auth, no credits.","operationId":"week1_opening_lines_json_datasets_nfl_week_1_opening_lines_2026_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/sandbox/sports":{"get":{"summary":"Sandbox Sports","description":"Fake /v1/sports response. Same shape as real endpoint, deterministic.\nNo auth, IP rate-limited.","operationId":"sandbox_sports_v1_sandbox_sports_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Sandbox"]}},"/v1/sandbox/sports/{sport_key}/odds":{"get":{"summary":"Sandbox Odds","description":"Fake /v1/sports/{sport_key}/odds response. Returns 3 synthetic\ngames for the requested sport. Real /v1/odds will return the actual\ngames for that sport, including 0 if none are scheduled.","operationId":"sandbox_odds_v1_sandbox_sports__sport_key__odds_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"regions","in":"query","required":false,"schema":{"type":"string","default":"us","title":"Regions"},"style":"form","explode":false,"description":"Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once."},{"name":"markets","in":"query","required":false,"schema":{"type":"string","default":"h2h","title":"Markets"},"style":"form","explode":false,"description":"Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once."},{"name":"oddsFormat","in":"query","required":false,"schema":{"type":"string","default":"american","title":"Oddsformat"}},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bookmakers"},"style":"form","explode":false,"description":"Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sandbox"]}},"/v1/sandbox/sports/{sport_key}/props":{"get":{"summary":"Sandbox Props","description":"Sandbox player-prop response. Returns deterministic synthetic\nprop rows for the requested sport so integrators can verify parsing\nof the real /v1/sports/{sport_key}/props shape without spending\ncredits.\n\nNo auth. IP rate-limited at 60 req/min. iter_042 #354.","operationId":"sandbox_props_v1_sandbox_sports__sport_key__props_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"markets","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"CSV of prop market keys","title":"Markets"},"description":"CSV of prop market keys Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"CSV of book keys (filter applied to output)","title":"Bookmakers"},"description":"CSV of book keys (filter applied to output) Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sandbox"]}},"/v1/sandbox/sports/{sport_key}/live/period_markets":{"get":{"summary":"Sandbox Period Markets","description":"Fake period markets response. Useful for verifying the period\nmarkets integration shape.","operationId":"sandbox_period_markets_v1_sandbox_sports__sport_key__live_period_markets_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"period","in":"query","required":false,"schema":{"type":"string","default":"all","title":"Period"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sandbox"]}},"/v1/sandbox/sports/{sport_key}/live/sse":{"get":{"summary":"Sandbox Sse","description":"Fake SSE stream. Pushes a synthetic state-change event every 3\nseconds so prospects can verify SSE integration before paying.\nSelf-terminates after 30 events to bound resource use.","operationId":"sandbox_sse_v1_sandbox_sports__sport_key__live_sse_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sandbox"]}},"/v1/sandbox/sports/{sport_key}/arbitrage":{"get":{"summary":"Sandbox Arbitrage","description":"Sandbox arbitrage response. Deterministic synthetic 2-side\ncross-book arbs. iter_068 #491.","operationId":"sandbox_arbitrage_v1_sandbox_sports__sport_key__arbitrage_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sandbox"]}},"/v1/sandbox/sports/{sport_key}/ev":{"get":{"summary":"Sandbox Ev","description":"Sandbox +EV response. iter_068 #491.","operationId":"sandbox_ev_v1_sandbox_sports__sport_key__ev_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sandbox"]}},"/v1/sandbox/sports/{sport_key}/consensus":{"get":{"summary":"Sandbox Consensus","description":"Sandbox consensus response. iter_068 #491.","operationId":"sandbox_consensus_v1_sandbox_sports__sport_key__consensus_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sandbox"]}},"/v1/sandbox/sports/{sport_key}/futures":{"get":{"summary":"Sandbox Futures","description":"Sandbox futures response. iter_068 #491.","operationId":"sandbox_futures_v1_sandbox_sports__sport_key__futures_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sandbox"]}},"/v1/sandbox/sports/{sport_key}/scores":{"get":{"summary":"Sandbox Scores","description":"Sandbox scores response. iter_068 #491.","operationId":"sandbox_scores_v1_sandbox_sports__sport_key__scores_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sandbox"]}},"/v1/sandbox/sports/{sport_key}/events":{"get":{"summary":"Sandbox Events","description":"Sandbox events response. iter_068 #491.","operationId":"sandbox_events_v1_sandbox_sports__sport_key__events_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sandbox"]}},"/v1/event-markets/examples":{"get":{"tags":["event_markets"],"summary":"Event Market Examples","description":"Return copy-paste example searches for event-market discovery.","operationId":"event_market_examples_v1_event_markets_examples_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/prediction-markets/search":{"get":{"tags":["event_markets"],"summary":"Search Event Markets","description":"Search non-standard event markets across public/free source surfaces.\n\nThis is a beta discovery endpoint. It does not assert arbitrage-ready\nequivalence across venues, it returns candidate matches with source-native\nprices so customers can inspect the exact live markets.","operationId":"search_event_markets_v1_prediction_markets_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":120,"description":"Free-text market search","title":"Q"},"description":"Free-text market search"},{"name":"sources","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated sources","default":"kalshi,polymarket,novig","title":"Sources"},"description":"Comma-separated sources"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":25,"title":"Limit"}},{"name":"min_volume","in":"query","required":false,"schema":{"type":"number","minimum":0,"description":"Drop markets below this source-native volume","default":0,"title":"Min Volume"},"description":"Drop markets below this source-native volume"},{"name":"min_confidence","in":"query","required":false,"schema":{"type":"number","maximum":1,"minimum":0,"description":"Drop weak text matches","default":0,"title":"Min Confidence"},"description":"Drop weak text matches"},{"name":"sort","in":"query","required":false,"schema":{"type":"string","description":"balanced or match","default":"balanced","title":"Sort"},"description":"balanced or match"},{"name":"include_raw","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Raw"}},{"name":"include_closed","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Closed"}},{"name":"include_unpriced","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Unpriced"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/event-markets/search":{"get":{"tags":["event_markets"],"summary":"Search Event Markets","description":"Search non-standard event markets across public/free source surfaces.\n\nThis is a beta discovery endpoint. It does not assert arbitrage-ready\nequivalence across venues, it returns candidate matches with source-native\nprices so customers can inspect the exact live markets.","operationId":"search_event_markets_v1_event_markets_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":120,"description":"Free-text market search","title":"Q"},"description":"Free-text market search"},{"name":"sources","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated sources","default":"kalshi,polymarket,novig","title":"Sources"},"description":"Comma-separated sources"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"default":25,"title":"Limit"}},{"name":"min_volume","in":"query","required":false,"schema":{"type":"number","minimum":0,"description":"Drop markets below this source-native volume","default":0,"title":"Min Volume"},"description":"Drop markets below this source-native volume"},{"name":"min_confidence","in":"query","required":false,"schema":{"type":"number","maximum":1,"minimum":0,"description":"Drop weak text matches","default":0,"title":"Min Confidence"},"description":"Drop weak text matches"},{"name":"sort","in":"query","required":false,"schema":{"type":"string","description":"balanced or match","default":"balanced","title":"Sort"},"description":"balanced or match"},{"name":"include_raw","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Raw"}},{"name":"include_closed","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Closed"}},{"name":"include_unpriced","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Unpriced"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/calc/kelly":{"get":{"summary":"Calc Kelly","description":"Fractional Kelly stake. FREE. iter-75 deploy / #456.\n\nReturns the optimal stake size given a bankroll, the price you can\nbet at, your estimated win probability, and a fraction-of-Kelly\nmultiplier (most bettors use 0.25-0.5 Kelly to avoid overbetting\non noisy win-prob estimates).\n\n`expected_value_usd` is the +EV in dollars (positive means the bet\nis +EV at your win_prob; negative means -EV). If the Kelly stake\nis negative, the math says don't bet.","operationId":"calc_kelly_v1_calc_kelly_get","parameters":[{"name":"bankroll","in":"query","required":true,"schema":{"type":"number","exclusiveMinimum":0,"description":"Total bankroll in USD","title":"Bankroll"},"description":"Total bankroll in USD"},{"name":"odds","in":"query","required":true,"schema":{"type":"string","description":"Bet price (American or decimal, e.g. '-110' or '1.91')","title":"Odds"},"description":"Bet price (American or decimal, e.g. '-110' or '1.91')"},{"name":"win_prob","in":"query","required":true,"schema":{"type":"number","exclusiveMaximum":1,"exclusiveMinimum":0,"description":"Your estimated win probability (0 < p < 1)","title":"Win Prob"},"description":"Your estimated win probability (0 < p < 1)"},{"name":"fraction","in":"query","required":false,"schema":{"type":"number","maximum":1.0,"exclusiveMinimum":0,"description":"Kelly fraction multiplier (0.25 = quarter Kelly)","default":0.25,"title":"Fraction"},"description":"Kelly fraction multiplier (0.25 = quarter Kelly)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Calculators"]}},"/v1/calc/hedge":{"get":{"summary":"Calc Hedge","description":"Compute the hedge stake. FREE. iter-75 deploy / #456.\n\nTwo-outcome hedge against an existing position. `original_stake` is\nwhat you placed on side A at `original_odds`. `hedge_odds` is what\nthe OTHER side is currently quoted at. Returns the hedge stake +\nprofit-if-A-wins and profit-if-B-wins.","operationId":"calc_hedge_v1_calc_hedge_get","parameters":[{"name":"original_stake","in":"query","required":true,"schema":{"type":"number","exclusiveMinimum":0,"description":"Stake you already placed","title":"Original Stake"},"description":"Stake you already placed"},{"name":"original_odds","in":"query","required":true,"schema":{"type":"string","description":"Odds you took (American or decimal)","title":"Original Odds"},"description":"Odds you took (American or decimal)"},{"name":"hedge_odds","in":"query","required":true,"schema":{"type":"string","description":"Current available odds on the other side","title":"Hedge Odds"},"description":"Current available odds on the other side"},{"name":"target","in":"query","required":false,"schema":{"type":"string","description":"One of: equal_profit (lock in identical profit either side), guaranteed_minimum (max guaranteed return), free_roll (bet just enough to recover original_stake)","default":"equal_profit","title":"Target"},"description":"One of: equal_profit (lock in identical profit either side), guaranteed_minimum (max guaranteed return), free_roll (bet just enough to recover original_stake)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Calculators"]}},"/v1/calc/edge":{"get":{"summary":"Calc Edge","description":"+EV / fair-line / no-vig calculator. FREE. iter-75 deploy / #456.\n\nTwo modes:\n 1. Pass `true_prob` directly: returns EV vs your stated win prob.\n 2. Pass `sharp_over_odds` + `sharp_under_odds`: derives the no-vig\n fair probability from the two-sided sharp market, then computes\n EV against that fair line. This is the canonical EV-scan formula.\n\n`edge_pct` is in percent (positive = +EV).","operationId":"calc_edge_v1_calc_edge_get","parameters":[{"name":"odds","in":"query","required":true,"schema":{"type":"string","description":"The price you can bet at (American or decimal)","title":"Odds"},"description":"The price you can bet at (American or decimal)"},{"name":"true_prob","in":"query","required":false,"schema":{"anyOf":[{"type":"number","exclusiveMaximum":1,"exclusiveMinimum":0},{"type":"null"}],"description":"Your estimated true win probability. Either this OR (sharp_over_odds + sharp_under_odds) required.","title":"True Prob"},"description":"Your estimated true win probability. Either this OR (sharp_over_odds + sharp_under_odds) required."},{"name":"sharp_over_odds","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Sharp book's price on the SAME side as `odds`. Used with sharp_under_odds for no-vig fair-line derivation.","title":"Sharp Over Odds"},"description":"Sharp book's price on the SAME side as `odds`. Used with sharp_under_odds for no-vig fair-line derivation."},{"name":"sharp_under_odds","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Sharp book's price on the OPPOSITE side. Used with sharp_over_odds for no-vig.","title":"Sharp Under Odds"},"description":"Sharp book's price on the OPPOSITE side. Used with sharp_over_odds for no-vig."},{"name":"stake","in":"query","required":false,"schema":{"type":"number","exclusiveMinimum":0,"description":"Stake to compute EV-in-dollars (default $100)","default":100,"title":"Stake"},"description":"Stake to compute EV-in-dollars (default $100)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Calculators"]}},"/v1/calc/free-bet":{"get":{"summary":"Calc Free Bet","description":"Convert a sportsbook free-bet promo to guaranteed cash. FREE.\niter-75 deploy / #456.\n\nThe free-bet pays out as STAKE-FREE-WINNINGS (the original stake is\nNOT returned). So on a $50 free bet at +200, win → $100 returned\n(not $150). Hedge math accounts for this: hedge_stake = free_bet * b\nwhere b = bet_odds_decimal - 1.","operationId":"calc_free_bet_v1_calc_free_bet_get","parameters":[{"name":"free_bet_usd","in":"query","required":true,"schema":{"type":"number","exclusiveMinimum":0,"description":"Face value of the free bet","title":"Free Bet Usd"},"description":"Face value of the free bet"},{"name":"bet_odds","in":"query","required":true,"schema":{"type":"string","description":"Odds you'd take on side A using the free bet","title":"Bet Odds"},"description":"Odds you'd take on side A using the free bet"},{"name":"hedge_odds","in":"query","required":true,"schema":{"type":"string","description":"Odds on side B at a different book for the hedge","title":"Hedge Odds"},"description":"Odds on side B at a different book for the hedge"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Calculators"]}},"/v1/sports/{sport_key}/middles":{"get":{"summary":"Find Middles","description":"Find middle opportunities across bookmakers. 3 credits.\n\nA *middle* is when you bet the Over at a low line on one book and the Under\nat a higher line on another, so there is a window of whole numbers where\nBOTH bets cash. Example: Over 7.5 runs at DraftKings and Under 9.5 at\nFanDuel; if the game lands on 8 or 9, both tickets win, otherwise you win\none and lose the other for a small net cost (the vig).\n\nUnlike arbitrage (guaranteed profit), a middle is a small known cost for a\nshot at a large payout when the result lands in the window. This scans\nevery over/under market with a numeric line, so it covers game totals,\nspreads, AND player-total props (points, rebounds, strikeouts, ...).\n\nEach result carries the window, the exact numbers that hit, the payout if\nit hits (per $100 on each leg), and the net cost if it misses above or\nbelow the window, so you can size the play yourself. Sorted widest-window\nfirst (best chance the result lands in the middle).\n\n**Example:** `GET /v1/sports/baseball_mlb/middles?min_gap=1`","operationId":"find_middles_v1_sports__sport_key__middles_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"min_gap","in":"query","required":false,"schema":{"type":"number","maximum":50.0,"minimum":0.0,"description":"Minimum middle-window width in points/runs/goals (default 1.0). A middle needs at least one whole number strictly inside the window to cash both sides, so 1.0 is the practical floor.","default":1.0,"title":"Min Gap"},"description":"Minimum middle-window width in points/runs/goals (default 1.0). A middle needs at least one whole number strictly inside the window to cash both sides, so 1.0 is the practical floor."},{"name":"min_books","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Minimum distinct books across the group (default 2).","default":2,"title":"Min Books"},"description":"Minimum distinct books across the group (default 2)."},{"name":"markets","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"CSV of market_keys to limit the scan (e.g. markets=totals for game totals only, or markets=player_points). Omit to scan game totals, spreads, AND player-total props.","title":"Markets"},"description":"CSV of market_keys to limit the scan (e.g. markets=totals for game totals only, or markets=player_points). Omit to scan game totals, spreads, AND player-total props. Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"include_props","in":"query","required":false,"schema":{"type":"boolean","description":"Include player-total props (points, strikeouts, ...) alongside game totals + spreads. Default true.","default":true,"title":"Include Props"},"description":"Include player-total props (points, strikeouts, ...) alongside game totals + spreads. Default true."},{"name":"max_width","in":"query","required":false,"schema":{"type":"number","maximum":50.0,"minimum":0.0,"description":"Optional cap on window width. 0 (default) = no cap. Useful to hide implausibly wide 'middles' that pair a main line with a deep, stale alternate line.","default":0.0,"title":"Max Width"},"description":"Optional cap on window width. 0 (default) = no cap. Useful to hide implausibly wide 'middles' that pair a main line with a deep, stale alternate line."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":3,"x-credit-cost-type":"fixed","x-credit-cost-description":"Cross-book middle opportunities","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/verdict":{"get":{"summary":"Verdict","description":"One-call bet verdict: fair price vs the market + a plain-English call. 5 credits.\n\nGive it a specific bet and it returns the no-vig fair price, how the best\navailable (and your) price compares as EV, which book has the best number,\nand a verdict: BET / LEAN / FAIR / PASS (or NO_DATA). Reuses the same sharp\nno-vig math as /ev and /consensus.\n\nExamples:\n /v1/verdict?sport=baseball_mlb&team=Yankees&market=h2h&side=Yankees&book=draftkings\n /v1/verdict?sport=baseball_mlb&home=Toronto Blue Jays&away=Tampa Bay Rays&market=totals&side=over&line=8.5\n /v1/verdict?sport=baseball_mlb&team=Braves&market=player_hits&player=Austin Riley&side=over&line=1.5&price=-115","operationId":"verdict_v1_verdict_get","parameters":[{"name":"sport","in":"query","required":true,"schema":{"type":"string","description":"sport_key, e.g. baseball_mlb","title":"Sport"},"description":"sport_key, e.g. baseball_mlb"},{"name":"market","in":"query","required":false,"schema":{"type":"string","description":"h2h | spreads | totals | a player prop key (player_points, ...)","default":"h2h","title":"Market"},"description":"h2h | spreads | totals | a player prop key (player_points, ...)"},{"name":"side","in":"query","required":true,"schema":{"type":"string","description":"Team name or home/away (h2h/spreads); over/under (totals/props)","title":"Side"},"description":"Team name or home/away (h2h/spreads); over/under (totals/props)"},{"name":"home","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Home team (with `away`, to identify the game)","title":"Home"},"description":"Home team (with `away`, to identify the game)"},{"name":"away","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Away team","title":"Away"},"description":"Away team"},{"name":"event","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Alternative to home/away: 'Away @ Home'","title":"Event"},"description":"Alternative to home/away: 'Away @ Home'"},{"name":"team","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Alternative: one team name to find the game","title":"Team"},"description":"Alternative: one team name to find the game"},{"name":"player","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Player name (required for player-prop markets)","title":"Player"},"description":"Player name (required for player-prop markets)"},{"name":"line","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Line/point for spreads, totals, props","title":"Line"},"description":"Line/point for spreads, totals, props"},{"name":"book","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The book you'd bet at (grades that book's price)","title":"Book"},"description":"The book you'd bet at (grades that book's price)"},{"name":"price","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The price you're offered (American or decimal). Overrides `book` price.","title":"Price"},"description":"The price you're offered (American or decimal). Overrides `book` price."},{"name":"region","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Where you can bet: us (default) | eu | uk | au | ca. Scopes the best-price/shop recommendation to books you can actually use. Omit to use your saved preference or US.","title":"Region"},"description":"Where you can bet: us (default) | eu | uk | au | ca. Scopes the best-price/shop recommendation to books you can actually use. Omit to use your saved preference or US."},{"name":"books","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact CSV of books you can bet at (e.g. draftkings,fanduel,novig). Overrides region; use this for state geo-blocks. Omit to use your saved preference.","title":"Books"},"description":"Exact CSV of books you can bet at (e.g. draftkings,fanduel,novig). Overrides region; use this for state geo-blocks. Omit to use your saved preference."},{"name":"bankroll","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Your bankroll. When the bet is +EV, returns a suggested Kelly stake amount.","title":"Bankroll"},"description":"Your bankroll. When the bet is +EV, returns a suggested Kelly stake amount."},{"name":"kelly","in":"query","required":false,"schema":{"type":"number","maximum":1.0,"minimum":0.0,"description":"Kelly fraction for staking (default 0.5 = half-Kelly, the bankroll-safe standard).","default":0.5,"title":"Kelly"},"description":"Kelly fraction for staking (default 0.5 = half-Kelly, the bankroll-safe standard)."},{"name":"sharpBook","in":"query","required":false,"schema":{"type":"string","description":"Sharp book to anchor the fair line","default":"pinnacle","title":"Sharpbook"},"description":"Sharp book to anchor the fair line"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/verdict/prefs":{"get":{"summary":"Get Verdict Prefs","description":"Read the saved book preference for the calling key. No credits.","operationId":"get_verdict_prefs_v1_verdict_prefs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Set Verdict Prefs","description":"Remember which books you can bet at so /v1/verdict scopes best-price and\nshop recommendations to them without repeating region/books every call. Pass\n`region` OR an exact `books` list. No credits.","operationId":"set_verdict_prefs_v1_verdict_prefs_post","parameters":[{"name":"region","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Where you bet: us | eu | uk | au | ca","title":"Region"},"description":"Where you bet: us | eu | uk | au | ca"},{"name":"books","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact CSV of book keys you can bet at (overrides region)","title":"Books"},"description":"Exact CSV of book keys you can bet at (overrides region)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/parlay/verdict":{"post":{"summary":"Parlay Verdict","description":"Grade a multi-leg parlay in one call. 10 credits.\n\nPOST body:\n {\"legs\": [ {sport, market, side, home, away | team, player, line}, ... ],\n \"region\"?, \"books\"?, \"book\"?, \"stake\"?, \"sharpBook\"?}\n\nReturns each leg's fair-vs-best, the combined no-vig fair price, the single\nBEST BOOK to place the whole parlay at (a real parlay is one slip at one\nbook, not best-of-each), the parlay's EV, the weakest leg, same-game\ncorrelation warnings, and (with `stake`) the payout. Reuses the /v1/verdict\nengine so single-bet and parlay verdicts agree, and scopes to the books you\ncan bet at (region/books, same as /v1/verdict).","operationId":"parlay_verdict_v1_parlay_verdict_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Calculators"]}},"/v1/sports/{sport_key}/best-bets":{"get":{"summary":"Best Bets","description":"The bets worth making right now, ranked. 10 credits.\n\nThe discovery half of the verdict: instead of grading a bet you name, this\nscans the sport's board, grades every candidate with the same no-vig engine\nas /v1/verdict, keeps only bets that are +EV at a book YOU can bet at, and\nranks them by edge. Also returns `edge_alerts` (books showing a price far\noff the market, to grab fast or verify). Region/books/prefs aware.","operationId":"best_bets_v1_sports__sport_key__best_bets_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"region","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Where you can bet: us (default) | eu | uk | au | ca.","title":"Region"},"description":"Where you can bet: us (default) | eu | uk | au | ca."},{"name":"books","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact CSV of books you can bet at (overrides region).","title":"Books"},"description":"Exact CSV of books you can bet at (overrides region)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"description":"Max plays to return.","default":20,"title":"Limit"},"description":"Max plays to return."},{"name":"min_edge","in":"query","required":false,"schema":{"type":"number","maximum":30.0,"minimum":0.0,"description":"Minimum edge %% vs the no-vig fair line.","default":2.0,"title":"Min Edge"},"description":"Minimum edge %% vs the no-vig fair line."},{"name":"min_books","in":"query","required":false,"schema":{"type":"integer","maximum":20,"minimum":2,"description":"Minimum books pricing a play (higher = more reliable).","default":4,"title":"Min Books"},"description":"Minimum books pricing a play (higher = more reliable)."},{"name":"markets","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional CSV of market_keys to restrict the scan.","title":"Markets"},"description":"Optional CSV of market_keys to restrict the scan. Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sports & Odds"]}},"/v1/try/verdict":{"get":{"summary":"Try Verdict","description":"Free, no-auth 'should I bet this?' demo. Rate-limited to 60/hour per IP,\nUS books, no staking/movement (those need a free key). The shareable hook:\npaste a bet, get the fair price, best book, and a plain-English call.","operationId":"try_verdict_v1_try_verdict_get","parameters":[{"name":"sport","in":"query","required":true,"schema":{"type":"string","description":"sport_key, e.g. baseball_mlb","title":"Sport"},"description":"sport_key, e.g. baseball_mlb"},{"name":"side","in":"query","required":true,"schema":{"type":"string","description":"Team (h2h/spreads), or over/under (totals/props)","title":"Side"},"description":"Team (h2h/spreads), or over/under (totals/props)"},{"name":"market","in":"query","required":false,"schema":{"type":"string","description":"h2h | spreads | totals | a player-prop key","default":"h2h","title":"Market"},"description":"h2h | spreads | totals | a player-prop key"},{"name":"home","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Home"}},{"name":"away","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Away"}},{"name":"team","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Team"}},{"name":"player","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Player"}},{"name":"line","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Line"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/widget/odds":{"get":{"summary":"Widget Odds","description":"**Keyless widget feed.** Backing data for the free embeddable odds\nwidget (https://parlay-api.com/widget): moneyline (h2h) for the next\n10 games of a popular US sport from up to 4 major books.\n\nDeliberately shallow and cached server-side for about a minute\n(`cache_age_seconds` in the response is honest). Capped at 60\nrequests/hour per IP. For every sport key, every book, every market,\nand no shared cache: https://parlay-api.com/signup","operationId":"widget_odds_v1_widget_odds_get","parameters":[{"name":"sport","in":"query","required":false,"schema":{"type":"string","default":"americanfootball_nfl","title":"Sport"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/sse/odds/{sport_key}":{"get":{"summary":"Sse Odds","description":"Business+ tier SSE endpoint. Same data feed as /ws/odds, just streamed\nas text/event-stream so corporate proxies / browser EventSource /\nHTTP-only environments work without a WebSocket upgrade.\n\nAuth takes the X-API-Key header, an Authorization: Bearer header, or\n?apiKey= (EventSource in a browser cannot set headers). Headers win when\nboth are present.\n\nEach event is a single JSON line in the standard SSE shape:\n data: {\"type\": \"odds_update\", \"sport_key\": \"...\", ...}\\n\\n\n\nHeartbeat every 5s by default to prove the stream is live through\ncorporate proxies and browser EventSource clients. Initial state and\nsource freshness are sent immediately on connect.","operationId":"sse_odds_v1_sse_odds__sport_key__get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"apiKey","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs; browser EventSource cannot set headers, so this stays supported.","title":"Apikey"},"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs; browser EventSource cannot set headers, so this stays supported."},{"name":"event_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional: filter to one game's event_id","title":"Event Id"},"description":"Optional: filter to one game's event_id"},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated books, e.g. fanduel,pinnacle","title":"Bookmakers"},"description":"Comma-separated books, e.g. fanduel,pinnacle Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"markets","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated prop market keys, e.g. player_points,player_rebounds","title":"Markets"},"description":"Comma-separated prop market keys, e.g. player_points,player_rebounds Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"kinds","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated row kinds: game,prop","title":"Kinds"},"description":"Comma-separated row kinds: game,prop"},{"name":"heartbeat_s","in":"query","required":false,"schema":{"type":"integer","maximum":30,"minimum":1,"description":"SSE heartbeat seconds","default":5,"title":"Heartbeat S"},"description":"SSE heartbeat seconds"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Initial snapshot row limit","default":500,"title":"Limit"},"description":"Initial snapshot row limit"},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Resume from this timestamp_ms. Skips initial replay of rows older than this.","title":"Since"},"description":"Resume from this timestamp_ms. Skips initial replay of rows older than this."},{"name":"diff","in":"query","required":false,"schema":{"type":"boolean","description":"If true, frames after initial_state include only fields that changed since the last frame for the same (event_id, bookmaker, market_key) tuple","default":false,"title":"Diff"},"description":"If true, frames after initial_state include only fields that changed since the last frame for the same (event_id, bookmaker, market_key) tuple"},{"name":"max_age_s","in":"query","required":false,"schema":{"type":"integer","maximum":3600,"minimum":1,"description":"Drop initial_state rows older than this many seconds. Defaults to the same 600s freshness bound /odds enforces.","default":600,"title":"Max Age S"},"description":"Drop initial_state rows older than this many seconds. Defaults to the same 600s freshness bound /odds enforces."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/sse/hot/{sport_key}":{"get":{"summary":"Sse Odds","description":"Business+ tier SSE endpoint. Same data feed as /ws/odds, just streamed\nas text/event-stream so corporate proxies / browser EventSource /\nHTTP-only environments work without a WebSocket upgrade.\n\nAuth takes the X-API-Key header, an Authorization: Bearer header, or\n?apiKey= (EventSource in a browser cannot set headers). Headers win when\nboth are present.\n\nEach event is a single JSON line in the standard SSE shape:\n data: {\"type\": \"odds_update\", \"sport_key\": \"...\", ...}\\n\\n\n\nHeartbeat every 5s by default to prove the stream is live through\ncorporate proxies and browser EventSource clients. Initial state and\nsource freshness are sent immediately on connect.","operationId":"sse_odds_v1_sse_hot__sport_key__get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"apiKey","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs; browser EventSource cannot set headers, so this stays supported.","title":"Apikey"},"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs; browser EventSource cannot set headers, so this stays supported."},{"name":"event_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional: filter to one game's event_id","title":"Event Id"},"description":"Optional: filter to one game's event_id"},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated books, e.g. fanduel,pinnacle","title":"Bookmakers"},"description":"Comma-separated books, e.g. fanduel,pinnacle Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"markets","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated prop market keys, e.g. player_points,player_rebounds","title":"Markets"},"description":"Comma-separated prop market keys, e.g. player_points,player_rebounds Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"kinds","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated row kinds: game,prop","title":"Kinds"},"description":"Comma-separated row kinds: game,prop"},{"name":"heartbeat_s","in":"query","required":false,"schema":{"type":"integer","maximum":30,"minimum":1,"description":"SSE heartbeat seconds","default":5,"title":"Heartbeat S"},"description":"SSE heartbeat seconds"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"Initial snapshot row limit","default":500,"title":"Limit"},"description":"Initial snapshot row limit"},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Resume from this timestamp_ms. Skips initial replay of rows older than this.","title":"Since"},"description":"Resume from this timestamp_ms. Skips initial replay of rows older than this."},{"name":"diff","in":"query","required":false,"schema":{"type":"boolean","description":"If true, frames after initial_state include only fields that changed since the last frame for the same (event_id, bookmaker, market_key) tuple","default":false,"title":"Diff"},"description":"If true, frames after initial_state include only fields that changed since the last frame for the same (event_id, bookmaker, market_key) tuple"},{"name":"max_age_s","in":"query","required":false,"schema":{"type":"integer","maximum":3600,"minimum":1,"description":"Drop initial_state rows older than this many seconds. Defaults to the same 600s freshness bound /odds enforces.","default":600,"title":"Max Age S"},"description":"Drop initial_state rows older than this many seconds. Defaults to the same 600s freshness bound /odds enforces."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/odds-drop/{sport_key}":{"get":{"summary":"Sse Odds Drop","description":"Business+ tier SSE endpoint that filters the broadcast stream to\nline-MOVES only. Customer configures a threshold (in American-odds\ncents) and we push an event the moment a tracked side crosses it.\n\nEach event has shape:\n data: {\"type\":\"odds_drop\",\"event_id\":\"...\",\"bookmaker\":\"...\",\n \"side\":\"h2h_home\",\"prev\":-110,\"new\":-120,\"delta\":-10,\n \"direction\":\"toward_favorite\",\"timestamp\":1747...}\\n\\n\n\nSingle-sport subscription:\n GET /v1/odds-drop/baseball_mlb?apiKey=...\n\nMulti-sport subscription (one connection, many sport_keys):\n GET /v1/odds-drop/all?apiKey=...\n GET /v1/odds-drop/multi?apiKey=...&sports=baseball_mlb,basketball_nba\n\nThe wildcard 'all' value subscribes to every sport_key with broadcast\nactivity in the last 5 minutes (~100-200 sport keys at peak). Drops\ninclude a `sport_key` field so you can route them downstream.","operationId":"sse_odds_drop_v1_odds_drop__sport_key__get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"apiKey","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs; browser EventSource cannot set headers, so this stays supported.","title":"Apikey"},"description":"API key. Prefer the X-API-Key header, which keeps the key out of URLs and logs; browser EventSource cannot set headers, so this stays supported."},{"name":"threshold","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"Minimum American-odds delta to trigger an event. Default 10 (a -110 becoming -120, or +100 becoming +110).","default":10,"title":"Threshold"},"description":"Minimum American-odds delta to trigger an event. Default 10 (a -110 becoming -120, or +100 becoming +110)."},{"name":"event_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional: filter to one game's event_id","title":"Event Id"},"description":"Optional: filter to one game's event_id"},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated books","title":"Bookmakers"},"description":"Comma-separated books Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"markets","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated market keys","title":"Markets"},"description":"Comma-separated market keys Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"sports","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated sport_keys for multi-sport subscription. Overrides path sport_key. Use 'all' as a sentinel to subscribe to every sport_key with recent activity (last 5 min).","title":"Sports"},"description":"Comma-separated sport_keys for multi-sport subscription. Overrides path sport_key. Use 'all' as a sentinel to subscribe to every sport_key with recent activity (last 5 min)."},{"name":"direction","in":"query","required":false,"schema":{"type":"string","description":"both | toward_favorite | toward_dog. Filter by which way the line moved.","default":"both","title":"Direction"},"description":"both | toward_favorite | toward_dog. Filter by which way the line moved."},{"name":"heartbeat_s","in":"query","required":false,"schema":{"type":"integer","maximum":30,"minimum":1,"default":5,"title":"Heartbeat S"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/sports":{"get":{"summary":"List Sports","description":"List available sports. FREE, no credits charged, no API key required.\n\nReturns every sport key we serve, including MLB, NFL, NBA, WNBA, NHL, MLS,\nMMA, Boxing, Cricket, horse racing, disc golf,\nesports, volleyball, table tennis, and the full soccer catalog.\n\nSoccer sport keys follow the pattern: soccer_epl, soccer_germany_bundesliga, etc.\n\nsleep_iter_46 #544: ETag + If-None-Match support. The sport list is\npre-warmed at module import and is bytewise-stable per worker, so\n304 round-trips save the full response body for polling clients.\nCache 1h.","operationId":"list_sports_v1_sports_get","parameters":[{"name":"all","in":"query","required":false,"schema":{"type":"boolean","description":"Include inactive sports","default":false,"title":"All"},"description":"Include inactive sports"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Discovery"]}},"/v1/meta/api-info":{"get":{"summary":"Api Info","description":"Platform metadata in one call. Public, no auth, no credits.\n\nUse this to:\n - Detect deploys: version + worker_started_at change after a\n rolling reload. Trigger reconnect / cache refresh on a change.\n - Health-check externally with a single endpoint that summarizes\n version, source coverage, endpoint count, and links.\n - Programmatically discover docs / OpenAPI / AsyncAPI URLs.\n\nCheap to call. Server-side computation: a few dict literals plus\none `len(app.routes)`. No DB hits.","operationId":"api_info_v1_meta_api_info_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Metadata"]}},"/v1/meta/credit-costs":{"get":{"summary":"Credit Costs","description":"Machine-readable per-endpoint credit cost catalogue. Public,\nno auth, no credits charged.\n\nUse cases:\n- Build a cost calculator in your app (\"this analysis will use\n N credits\").\n- Estimate monthly burn before committing to a tier.\n- Drive a usage dashboard alongside `/v1/usage` (current spend)\n and `/v1/pricing` (tier credit allowance).\n\nResponse shape:\n {\n \"currency\": \"credits\",\n \"version\": \"0.1\",\n \"fixed\": [{method, path, cost, desc}, ...],\n \"variable\": [{method, path, cost_formula, cost_floor, example, desc}, ...],\n \"free_examples\": [path, ...],\n \"notes\": [...]\n }\n\nStability promise: the keys `currency`, `version`, `fixed`,\n`variable`, `free_examples` will not be removed or renamed. Per-\nendpoint costs may change with notice via /changelog. The\n`version` field bumps when a breaking schema change happens.\n\nsleep_iter_21 #520: emits ETag + supports If-None-Match for 304\nNot-Modified responses. SDK clients with conditional-GET wiring\nskip transferring the body when the cost catalogue hasn't\nchanged since the last fetch.","operationId":"credit_costs_v1_meta_credit_costs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Metadata"]}},"/v1/meta/quote":{"post":{"summary":"Quote Credits","description":"Preview the credit cost of a request without executing it.\n\nPublic, no auth, no credits charged.\n\nRequest body:\n```\n{\n \"method\": \"GET\",\n \"path\": \"/v1/sports/baseball_mlb/odds\",\n \"query\": {\"markets\": \"h2h,spreads\", \"regions\": \"us\"}\n}\n```\n\nResponse:\n```\n{\n \"estimated_credits\": 2,\n \"cost_basis\": \"len(markets)=2 x len(regions)=1\",\n \"cost_type\": \"variable\",\n \"matched_template\": \"/v1/sports/{sport_key}/odds\",\n \"description\": \"Multi-market multi-region odds\",\n \"cost_floor\": 1\n}\n```\n\nUse cases:\n- Batch analysis budgeting: estimate cost of running an\n analysis across N events before committing.\n- Tier upgrade planning: see how much a typical workflow\n costs before subscribing.\n- SDK helper: client.estimate_cost(params) before\n client.get_odds(params).\n\nFor fixed-cost endpoints, this just looks up the cost. For\nvariable-cost endpoints, it parses the query and applies the\nformula from _CREDIT_COSTS.\n\nsleep_iter_48 #546.","operationId":"quote_credits_v1_meta_quote_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/meta/batch-quote":{"post":{"summary":"Batch Quote Credits","description":"Batch credit-cost preview. Public, no auth, no credits.\n\nRequest body:\n```\n{\n \"requests\": [\n {\"method\": \"GET\", \"path\": \"/v1/sports/baseball_mlb/odds\", \"query\": {\"markets\": \"h2h\"}},\n {\"method\": \"GET\", \"path\": \"/v1/sports/baseball_mlb/props\", \"query\": {}},\n {\"method\": \"POST\", \"path\": \"/v1/parlay/price\", \"query\": {\"legs_count\": \"3\"}},\n ...\n ]\n}\n```\n\nResponse:\n```\n{\n \"total_estimated_credits\": 42,\n \"request_count\": 3,\n \"per_request\": [\n {\"index\": 0, \"estimated_credits\": 1, \"cost_type\": \"variable\", ...},\n {\"index\": 1, \"estimated_credits\": 3, \"cost_type\": \"fixed\", ...},\n {\"index\": 2, \"estimated_credits\": 6, \"cost_type\": \"variable\", ...}\n ],\n \"errors\": []\n}\n```\n\nUse cases:\n- Planning a batch script: estimate total cost across N\n calls before committing.\n- Tier upgrade calculator: simulate a typical month of\n calls, compare to tier.credits_per_month.\n- SDK helper: client.estimate_batch(requests) before\n asyncio.gather(*[client.request(r) for r in requests]).\n\nCap of 500 requests per batch to prevent abuse.\n\nsleep_iter_52 #550.","operationId":"batch_quote_credits_v1_meta_batch_quote_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/meta/sla":{"get":{"summary":"Get Sla","description":"Machine-readable SLA targets. Public, no auth, no credits.\n\nReturns the operational targets ParlayAPI commits to plus the\nmeasured baselines (latency p50s, support response times,\nmaintenance / data retention policy). Procurement and security\nreview teams ingest this for vendor evaluation.\n\nNumbers reflect operational reality, not aspirational targets.\nLatency p50s are measured from the public URL; uptime\ntargets reflect what the current architecture can sustain.\n\nsleep_iter_28 #527: emits ETag + supports If-None-Match. Changes\nonly when the SLA document itself is edited (a contract event,\nnot a per-request event), so 1h cache + conditional GET is the\nright shape.\n\nStability promise: keys at the top level\n(`version`, `uptime_target_per_month`, `latency_targets_p50_ms`,\n`support_response_time_hours`, `maintenance_window_policy`,\n`data_retention`, `incident_response`) will not be removed or\nrenamed without bumping `version`.","operationId":"get_sla_v1_meta_sla_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Metadata"]}},"/v1/meta/sdks":{"get":{"summary":"Get Sdks","description":"Machine-readable SDK catalogue. Public, no auth, no credits.\n\nReturns:\n - first_party: SDKs we publish ourselves (parlayapi-mcp on PyPI)\n - generators: official OpenAPI/AsyncAPI generator commands\n - integrations: MCP, Postman, Insomnia, etc.\n\nLets customers and AI agents discover the right integration\npath without reading the /docs page. The OpenAPI extensions\nfrom iter_19 (x-credit-cost) and iter_24 (servers,\nsecuritySchemes) make every generated SDK fully-typed,\ncost-aware, and auth-pre-plumbed.\n\nsleep_iter_38 #537: emits ETag + supports If-None-Match.\nChanges only when SDK landscape shifts (a new official SDK,\na new supported generator language).","operationId":"get_sdks_v1_meta_sdks_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Metadata"]}},"/v1/meta/source-capabilities":{"get":{"summary":"Source Capabilities","description":"Machine-readable source capability matrix. Public, no auth,\nno credits.\n\nReturns:\n - `sources[]`: per-source entry with key, title, region, status,\n capabilities (list of supported categories), aliases_to (if\n applicable)\n - `by_capability{}`: inverse index for each capability type,\n listing all sources that support it\n - `capabilities[]`: canonical list of capability category names\n\nExample use cases:\n- Customer building a \"best book for NBA props\" comparison: filter\n by_capability.props ∩ regions==us\n- SDK init: cache the matrix, then locally filter when user picks\n a market type\n- AI agent: answer \"which books carry NHL game-line live odds?\"\n in one fetch\n\nsleep_iter_53 #551: emits ETag + supports If-None-Match.\nSource landscape changes only when we add a new book or flip a\nstatus; ETag round-trips work for SDK boot polling.","operationId":"source_capabilities_v1_meta_source_capabilities_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Metadata"]}},"/v1/meta/endpoints":{"get":{"summary":"List Endpoints","description":"Lean endpoint catalogue. Public, no auth, no credits.\n\nReturns the same path + method + tag + summary + credit-cost info\nas /openapi.json but stripped of request/response schemas. ~10 KB\npayload vs ~194 KB for the full spec. SDK clients fetching this\nat boot for \"what endpoints exist?\" save bandwidth.\n\nEach entry:\n - path: templated URL (\"/v1/sports/{sport_key}/odds\")\n - method: HTTP verb (GET, POST, etc.)\n - tags: list of canonical tags from iter_35\n - summary: short description (when set in route docstring)\n - description: full prose (truncated to 200 chars)\n - deprecated: bool (iter_47 OpenAPI deprecation flag)\n - x-credit-cost: int for fixed-cost endpoints\n - x-credit-cost-type: \"fixed\" | \"variable\" | absent (free)\n - x-credit-cost-description: short capability summary\n\nOptional filters: ?tag=Metadata, ?method=POST.\n\nsleep_iter_55 #552: emits ETag + supports If-None-Match.\nChanges only when OpenAPI schema regenerates (deploy or\nschema-edit).","operationId":"list_endpoints_v1_meta_endpoints_get","parameters":[{"name":"tag","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to endpoints carrying this tag (e.g. 'Metadata', 'Calculators', 'Sports & Odds').","title":"Tag"},"description":"Filter to endpoints carrying this tag (e.g. 'Metadata', 'Calculators', 'Sports & Odds')."},{"name":"method","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a specific HTTP method (GET, POST, etc.).","title":"Method"},"description":"Filter to a specific HTTP method (GET, POST, etc.)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/meta/webhooks":{"get":{"summary":"Webhooks Spec","description":"Machine-readable webhook specification. Public, no auth,\nno credits.\n\nReturns:\n - `events[]`: canonical event type list with descriptions\n - `signing`: HMAC scheme, header format, verification snippet\n - `retry_policy`: attempts + backoff + auto-disable rules\n - `tier_gate`: which tiers can use webhooks\n - `management`: full CRUD endpoint URLs\n - `delivery`: payload shape + headers we send\n\nCustomers building event-driven integrations get the full\nspec in one fetch instead of reading docs HTML.\n\nIndustry pattern: Stripe, Twilio, GitHub all publish their\nwebhook spec at a discoverable URL. We follow the same shape.\n\nsleep_iter_56 #553: emits ETag + supports If-None-Match.","operationId":"webhooks_spec_v1_meta_webhooks_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Metadata"]}},"/v1/meta/provider-state":{"get":{"summary":"Provider State","description":"Per-source provider-state metadata. Machine-readable freshness +\nrole for every data source we currently ingest from. No API key\nrequired, no credits charged. Polls cheap: 5s server-side cache.\n\nResponse shape:\n\n {\n \"ts\": 1778735000,\n \"src\": {\n \"pinnacle\": {\"age_s\": 1.2, \"role\": \"primary\"},\n \"draftkings\": {\"age_s\": 3.8, \"role\": \"primary\"},\n \"fanduel\": {\"age_s\": 47, \"role\": \"degraded\"},\n \"caesars\": {\"age_s\": null, \"role\": \"offline\"}\n }\n }\n\nRole values:\n primary last write within source's expected polling window\n degraded last write within 3x the window; data may be stale\n offline last write past 30x window, or no recent writes seen\n\nEach /v1/* response also includes this in the `X-Provider-State`\nheader, so most clients can avoid this endpoint entirely. Hit this\nendpoint directly only when you want the full untruncated payload\n(the header is capped at ~2 KB to fit HTTP limits).","operationId":"provider_state_v1_meta_provider_state_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Metadata"]}},"/v1/meta/source-quality":{"get":{"summary":"Source Quality","description":"Per-source speed and quality metadata. Public, no auth, no credits.\n\nThis complements /v1/meta/provider-state with an operator-grade\nview over recent write activity across odds, props, and period\nmarkets. It returns SLA state, age, observed row counts, and the\nnext recommended cadence action for each source.\n\nThe endpoint reads metadata only. It does not create, modify, or\ninfer prices.","operationId":"source_quality_v1_meta_source_quality_get","parameters":[{"name":"minutes","in":"query","required":false,"schema":{"type":"integer","maximum":1440,"minimum":1,"default":10,"title":"Minutes"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":40,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/meta/source-health":{"get":{"summary":"Source Health Alias","description":"Compatibility alias for /v1/meta/source-quality.\n\nOperators and older docs often say \"source health\" when referring\nto this payload. Keep the alias live so quick diagnostics do not\n404 during an incident.","operationId":"source_health_alias_v1_meta_source_health_get","parameters":[{"name":"minutes","in":"query","required":false,"schema":{"type":"integer","maximum":1440,"minimum":1,"default":10,"title":"Minutes"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":40,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/meta/collector-flush":{"get":{"summary":"Collector Flush Meta","description":"Latest collector flush telemetry. Public, no auth, no credits.\n\nShows whether the database writer is keeping up with the fetchers:\nelapsed flush time, rows written by kind, remaining queue depth, and\nper-source contributors. This is the first place to look when\nsource pulses are fresh but price-write timestamps lag behind.","operationId":"collector_flush_meta_v1_meta_collector_flush_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Metadata"]}},"/v1/meta/latest-state":{"get":{"summary":"Latest State Meta","description":"Redis latest-state telemetry. Public, no auth, no credits.\n\nThis is the hot lane ahead of Postgres archive flush: latest\naccepted row by source plus collector-lag percentiles. It is\nobservability only and does not derive, modify, or backfill prices.","operationId":"latest_state_meta_v1_meta_latest_state_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"latency_window_s","in":"query","required":false,"schema":{"type":"integer","maximum":3600,"minimum":60,"default":900,"title":"Latency Window S"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/meta/per-book-sla":{"get":{"summary":"Meta Per Book Sla","description":"Per-book freshness SLA thresholds. Public, no auth, no credits.\n\nReturns the (tight_s, slack_s, stale_s) thresholds used to classify\neach source as ok / degraded / breach / stale in the source-quality\npayloads. Customers integrating against /v1/meta/source-quality use\nthis to know what `sla=degraded` actually means for a given book\n(e.g. \"Pinnacle tight=5s, Bovada tight=30s, PrizePicks tight=60s\").\n\nMemory rule compliance: read-only on already-public threshold\nconfig. No prices touched.","operationId":"meta_per_book_sla_v1_meta_per_book_sla_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Metadata"]}},"/v1/meta/parser-coverage":{"get":{"summary":"Meta Parser Coverage","description":"Per-book per-sport market-coverage matrix. Public, no auth, no credits.\n\nAnswers the customer question \"which markets do you actually parse\nfrom Caesars on NBA?\" or \"what markets does Pinnacle expose on\ntable_tennis?\" by aggregating observed market_keys from the\nprop_snapshots and odds_snapshots tables over the requested\nwindow.\n\nReturns:\n {\n \"as_of\": \"...\",\n \"window_hours\": 72,\n \"book_count\": 32, // operators, the keys /odds serves\n \"source_count\": 45, // raw ingest feeds behind them\n \"variants\": {\"unibet\": [\"unibet\", \"unibet_au\", ...], ...},\n \"by_book\": {\n \"draftkings\": {\n \"baseball_mlb\": {\n \"markets\": [\"player_home_runs\", \"player_strikeouts\", ...],\n \"row_count\": 14821,\n \"latest_age_s\": 12.3\n },\n ...\n },\n ...\n }\n }\n\n`book_count` counts operators, not feeds: an operator that publishes\ncountry-licensed feeds off one platform (Kindred runs 14 Unibet feeds)\nis one book, the same one /odds and /props serve. Its markets are the\nunion of its feeds' markets and its row_count their sum. `source_count`\nkeeps the raw feed total visible and `variants` names the members, so\nnothing is hidden by the fold. Naming one feed with ?source= returns it\nunder its own name.\n\nMemory rule compliance: read-only on metadata. No prices touched.","operationId":"meta_parser_coverage_v1_meta_parser_coverage_get","parameters":[{"name":"window_hours","in":"query","required":false,"schema":{"type":"integer","maximum":720,"minimum":1,"default":72,"title":"Window Hours"}},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional filter to one book.","title":"Source"},"description":"Optional filter to one book."},{"name":"sport_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional filter to one sport.","title":"Sport Key"},"description":"Optional filter to one sport."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/meta/book-coverage":{"get":{"summary":"Meta Book Coverage","description":"Per-book coverage gates across game, prop, and period markets.\n\nPublic, no auth, no credits. This is the machine-readable proof\nsurface behind the coverage dashboard: every current book/sport/kind\nrow gets source freshness, normalization, database, REST-shape, and\nstream-shape gates.","operationId":"meta_book_coverage_v1_meta_book_coverage_get","parameters":[{"name":"window_minutes","in":"query","required":false,"schema":{"type":"integer","maximum":1440,"minimum":5,"default":15,"title":"Window Minutes"}},{"name":"include_warn","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Include Warn"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/asyncapi.json":{"get":{"summary":"Get Asyncapi Spec","description":"AsyncAPI 3 spec describing the WebSocket and SSE surface.\n\nCompanion to /openapi.json for the streaming endpoints. Customers\ncan generate typed SDKs from this spec via the AsyncAPI generator\ntoolchain (30+ language templates available). No API key required.\n\nSee https://www.asyncapi.com for tooling. The spec covers connect\nhandshake, initial_state, odds_update, heartbeat, subscribe /\nunsubscribe commands, and the close-code matrix. Tier gate\ndocumented under `x-parlay-tier-gates`.\n\nsleep_iter_22 #521: emits ETag + supports If-None-Match. Spec is\nloaded at module init and only changes on deploy.","operationId":"get_asyncapi_spec_v1_asyncapi_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Specification"]}},"/v1/pricing":{"get":{"summary":"Get Pricing","description":"Public pricing endpoint. Returns the tier table for programmatic\nintegration (Stripe widgets, AI agents, comparison pages, the JSON\nside of the HTML /pricing page). No API key required.\n\nsleep_iter_21 #520: emits ETag + supports If-None-Match. Pricing\nchanges rarely (a few times a year at most). Clients with\nconditional-GET wiring skip the body on 304.","operationId":"get_pricing_v1_pricing_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Metadata"]}},"/v1/regions":{"get":{"summary":"List Regions","description":"List supported `regions` filter values used by /v1/sports/{key}/odds.\nPublic, no auth, no credits. iter_062 #474.\n\nsleep_iter_22 #521: emits ETag + supports If-None-Match.","operationId":"list_regions_v1_regions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Discovery"]}},"/v1/markets":{"get":{"summary":"List Markets","description":"List supported `markets` filter values across the API.\nPublic, no auth, no credits. iter_062 #474.\n\nThe market_key vocabulary is large (60+ values) and grows over the\nseason as books add new prop markets. This endpoint returns the\ncurrent canonical set used by /odds, /props, /ev, /consensus, and\n/parlay/price.\n\nsleep_iter_22 #521: emits ETag + supports If-None-Match.","operationId":"list_markets_v1_markets_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Discovery"]}},"/v1/bookmakers":{"get":{"summary":"List Bookmakers","description":"List supported bookmakers with their integration status.\n\nStatus values. These are INTEGRATION states, not liveness readings:\n active Wired up and served on the endpoints listed.\n merged Brand merged into another active bookmaker (see merged_into).\n decommissioned Bookmaker shut down or no longer publicly accessible.\n not_yet_integrated Known book we haven't wired up yet.\n suppressed A key we publish but serve on no endpoint.\n retired Operator closed. Not served on any live endpoint;\n its closing-line archive stays queryable on the\n historical endpoints (see the note).\n\n`active` says the book is integrated, not that it is writing rows this\nminute. Whether a book is flowing is computed from the data by\nGET /v1/bookmakers/{key}/freshness (`is_live`) and by\nGET /v1/meta/book-catalog (`live`, `rows_24h`). An active book that is\nnot currently producing carries a `note` saying so; read the note before\nbuilding a filter around a single book.\n\nDefaults to only active books. Pass ?all=true to see merged / decommissioned\nentries plus the explanation note for each.\n\nBy default each book carries an `endpoints` array (game_lines / live /\nprops / prediction / event_markets / historical) plus `example_paths`\nshowing how to query it. Pass `?include_endpoints=false` for the lean\ncatalog without those.\n\nsleep_iter_22 #521: emits ETag + supports If-None-Match. Registry\nchanges only on code edit (new book wired up; status flip).","operationId":"list_bookmakers_v1_bookmakers_get","parameters":[{"name":"all","in":"query","required":false,"schema":{"type":"boolean","description":"Include non-active (merged/decommissioned) entries","default":false,"title":"All"},"description":"Include non-active (merged/decommissioned) entries"},{"name":"include_endpoints","in":"query","required":false,"schema":{"type":"boolean","description":"Attach endpoints[] + example_paths per book. Set false for the lean catalog.","default":true,"title":"Include Endpoints"},"description":"Attach endpoints[] + example_paths per book. Set false for the lean catalog."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Discovery"]}},"/v1/bookmakers/{key}":{"get":{"summary":"Get Bookmaker","description":"Return one bookmaker's catalog entry with endpoints + example URLs.\n\nReturns 404 with the full active list if `key` is unknown so callers\ncan recover by suggestion.","operationId":"get_bookmaker_v1_bookmakers__key__get","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Discovery"]}},"/v1/bookmakers/{key}/freshness":{"get":{"summary":"Get Bookmaker Freshness","description":"Live data-freshness diagnostic for one book.\n\nReturns the most recent timestamp this book wrote into each backing\ntable, plus row counts in the last hour and 24 hours. Useful when\ncallers want to verify \"is this book actually flowing\" without\npolling /odds and /props themselves.\n\nTables checked:\n odds_snapshots (game-line h2h/spreads/totals)\n prop_snapshots (player props + prediction markets)\n period_odds_snapshots (1H, Q1-Q4, halves, etc.)\n\nFree, no auth, no credits charged. Intended for status pages and\ncustomer health checks.","operationId":"get_bookmaker_freshness_v1_bookmakers__key__freshness_get","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Discovery"]}},"/v1/sports/{sport_key}/events":{"get":{"summary":"List Events","description":"List upcoming events. FREE - no credits charged.","operationId":"list_events_v1_sports__sport_key__events_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"dateFormat","in":"query","required":false,"schema":{"type":"string","default":"iso","title":"Dateformat"}},{"name":"eventIds","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated event IDs","title":"Eventids"},"description":"Comma-separated event IDs"},{"name":"commenceTimeFrom","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Commencetimefrom"}},{"name":"commenceTimeTo","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Commencetimeto"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/participants":{"get":{"summary":"List Participants","description":"List teams or players for a sport in TOA participant shape. 1 credit.","operationId":"list_participants_v1_sports__sport_key__participants_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":1,"x-credit-cost-type":"fixed","x-credit-cost-description":"Team or player roster for a sport","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/events/canonical":{"get":{"summary":"List Canonical Events","description":"List events grouped by canonical ID across ALL sources.\n\nEach canonical event shows which sources have it and links their source-specific\nevent IDs + team name variations. Useful for joining data across books.\n\nCredits: 2","operationId":"list_canonical_events_v1_sports__sport_key__events_canonical_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":2,"x-credit-cost-type":"fixed","x-credit-cost-description":"Events grouped by canonical ID across sources","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/odds":{"get":{"summary":"Get Odds","description":"Get odds for upcoming and live events.\n\nCredits: markets_count x regions_count (same formula as the-odds-api).\n\n**Every sport key in GET /v1/sports is supported**, including every soccer\ncompetition in that list, esports, and volleyball.\n\n**Regions:** us, us2, uk, eu, fr, au, ca, mx, latam, br, asia. Use eu for Pinnacle and European bookmakers.\n\n**Markets:** h2h (moneyline/3-way), spreads, totals, alternate_spreads,\nalternate_totals, outrights, and player_* / batter_* / pitcher_* /\nanytime_* / futures_* prop keys. That list is the WHOLE list this\nendpoint can return.\n\n**Markets served elsewhere are not billed here.** A key this endpoint\ncannot emit is still accepted and the request is still answered (a\nvalid derived market never 400s here, so a migrating client's pipeline\nis never aborted), but it is dropped from the `markets x regions`\nmultiplier and costs nothing. The period keys (`h2h_1st_half`,\n`spreads_1st_half`, `totals_1st_half`, `h2h_1st_quarter`,\n`h2h_1st_period`, `h2h_1st_5_innings`, `spreads_1st_5_innings`,\n`totals_1st_5_innings`) are served by\n`/v1/sports/{sport_key}/live/period_markets?period=1H&market=h2h` and\n`/v1/historical/sports/{sport_key}/period_markets`. Team totals, BTTS,\ncorrect score, double chance, draw-no-bet and the racing keys are\nserved by `/v1/sports/{sport_key}/props?markets=`. Until\n2026-09-05 /odds billed markets x regions for all of them and dropped\nthem from the response (ticket #295).\n\n**What you were billed for.** Every charged response carries\n`x-markets-served`, the market keys the charge covered. A servable key\nwith no book pricing it right now still costs a credit and still\nappears there: that is coverage, not a gap. Keys we cannot serve here\nare listed in `x-markets-unservable` (billed zero) and routed by\n`x-markets-served-elsewhere`.\n\n**Filter by bookmaker:** `?bookmakers=pinnacle,draftkings`\n\n**Live only:** `?live=true` returns events that have already started (same\ncost as a normal call, no extra charge). For a dedicated in-play endpoint\nthat exposes live-tagged player props alongside game lines, see\n`/v1/sports/{sport_key}/live`.\n\n**Shape tokens:** `?include=slim` drops `raw_json` from any row that\ncarries one. `?include=raw` returns the identifying fields plus the\n`bookmakers` tree and `raw_json`, dropping derived fields\n(`canonical_event_id`, `sport_title`, `probable_pitchers`,\n`starting_lineups`). Events on this endpoint do not currently carry a\n`raw_json` field, so `slim` returns the same fields as `normalized` here\nand `raw` differs from `normalized` only by the derived fields it drops.\n\n**Verified-at signal:** every bookmaker's `last_update` is the freshest of\n(price-change, no-change verification heartbeat). On hot-cycle sources\n(Pinnacle, FanDuel) that means a maximum age around the 2-second poll\ninterval, even if the price hasn't moved. Add `?include=verification` to\nalso receive:\n - `verified_at`: the heartbeat timestamp (we polled and saw the same price)\n - `line_changed_at`: the last actual price-move timestamp\n - `is_current`: true iff verified_at is within the last 5 seconds\n\nCombine with shape tokens, e.g. `?include=slim,verification`.\n\n**Example:** `GET /v1/sports/soccer_epl/odds?regions=eu&markets=h2h,spreads&bookmakers=pinnacle`","operationId":"get_odds_v1_sports__sport_key__odds_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"regions","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated: us,us2,uk,eu,au. Default 'us'. iter_047 #420: was previously required (...). Made optional with 'us' default so consumers running pre-existing The-Odds-API-style code (which defaulted to us) don't 422.","default":"us","title":"Regions"},"description":"Comma-separated: us,us2,uk,eu,au. Default 'us'. iter_047 #420: was previously required (...). Made optional with 'us' default so consumers running pre-existing The-Odds-API-style code (which defaulted to us) don't 422. Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"markets","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated. Servable here: h2h, spreads, totals, alternate_spreads, alternate_totals, outrights, and any player_*/batter_*/pitcher_*/anytime_*/futures_* prop key. Any other key is still accepted and still answered, but it is NOT billed and returns nothing here: period keys (h2h_1st_half, totals_1st_5_innings, ...) belong to /v1/sports/{sport_key}/live/period_markets. The response says which keys were billed (x-markets-served), which were not (x-markets-unservable) and where those are served (x-markets-served-elsewhere).","default":"h2h","title":"Markets"},"description":"Comma-separated. Servable here: h2h, spreads, totals, alternate_spreads, alternate_totals, outrights, and any player_*/batter_*/pitcher_*/anytime_*/futures_* prop key. Any other key is still accepted and still answered, but it is NOT billed and returns nothing here: period keys (h2h_1st_half, totals_1st_5_innings, ...) belong to /v1/sports/{sport_key}/live/period_markets. The response says which keys were billed (x-markets-served), which were not (x-markets-unservable) and where those are served (x-markets-served-elsewhere). Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"oddsFormat","in":"query","required":false,"schema":{"type":"string","default":"decimal","title":"Oddsformat"}},{"name":"dateFormat","in":"query","required":false,"schema":{"type":"string","default":"iso","title":"Dateformat"}},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated bookmaker keys (overrides regions)","title":"Bookmakers"},"description":"Comma-separated bookmaker keys (overrides regions) Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"eventIds","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated event IDs","title":"Eventids"},"description":"Comma-separated event IDs"},{"name":"commenceTimeFrom","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Commencetimefrom"}},{"name":"commenceTimeTo","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Commencetimeto"}},{"name":"date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Shortcut: events whose commence_time falls on this UTC date (YYYY-MM-DD). Sugar for commenceTimeFrom=T00:00:00Z and commenceTimeTo=T23:59:59Z. Explicit commenceTime* values win.","title":"Date"},"description":"Shortcut: events whose commence_time falls on this UTC date (YYYY-MM-DD). Sugar for commenceTimeFrom=T00:00:00Z and commenceTimeTo=T23:59:59Z. Explicit commenceTime* values win."},{"name":"include","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated. Shape tokens: normalized (default, every field we build), slim (drops raw_json from any row that carries one), raw (the identifying fields plus the bookmakers tree and raw_json, dropping derived fields such as canonical_event_id, sport_title, probable_pitchers and starting_lineups). Events on this endpoint do not currently carry a raw_json field, so slim returns the same fields as normalized here and raw differs only by the derived fields it drops. Add 'verification' to include verified_at, line_changed_at, and is_current per bookmaker.","default":"normalized","title":"Include"},"description":"Comma-separated. Shape tokens: normalized (default, every field we build), slim (drops raw_json from any row that carries one), raw (the identifying fields plus the bookmakers tree and raw_json, dropping derived fields such as canonical_event_id, sport_title, probable_pitchers and starting_lineups). Events on this endpoint do not currently carry a raw_json field, so slim returns the same fields as normalized here and raw differs only by the derived fields it drops. Add 'verification' to include verified_at, line_changed_at, and is_current per bookmaker."},{"name":"verified","in":"query","required":false,"schema":{"type":"boolean","description":"Alias for include=verification. Adds verified_at, line_changed_at, and is_current per bookmaker.","default":false,"title":"Verified"},"description":"Alias for include=verification. Adds verified_at, line_changed_at, and is_current per bookmaker."},{"name":"live","in":"query","required":false,"schema":{"type":"boolean","description":"Live games only (commence_time at or before now). Equivalent to passing commenceTimeTo=. No extra cost.","default":false,"title":"Live"},"description":"Live games only (commence_time at or before now). Equivalent to passing commenceTimeTo=. No extra cost."},{"name":"include_live","in":"query","required":false,"schema":{"type":"boolean","description":"Include in-progress games in the response (off by default; /odds is for pregame analysis). Set true to get both pregame and live in one call.","default":false,"title":"Include Live"},"description":"Include in-progress games in the response (off by default; /odds is for pregame analysis). Set true to get both pregame and live in one call."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost-formula":"len(markets) x len(regions)","x-credit-cost-floor":1,"x-credit-cost-type":"variable","x-credit-cost-example":"?markets=h2h,spreads®ions=us = 2x1 = 2 credits","x-credit-cost-description":"Multi-market multi-region odds","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/events/{event_id}/odds":{"get":{"summary":"Get Event Odds","description":"Get odds for a single event.","operationId":"get_event_odds_v1_sports__sport_key__events__event_id__odds_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"event_id","in":"path","required":true,"schema":{"type":"string","title":"Event Id"}},{"name":"regions","in":"query","required":true,"schema":{"type":"string","title":"Regions"},"style":"form","explode":false,"description":"Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once."},{"name":"markets","in":"query","required":false,"schema":{"type":"string","default":"h2h","title":"Markets"},"style":"form","explode":false,"description":"Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once."},{"name":"oddsFormat","in":"query","required":false,"schema":{"type":"string","default":"decimal","title":"Oddsformat"}},{"name":"dateFormat","in":"query","required":false,"schema":{"type":"string","default":"iso","title":"Dateformat"}},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated bookmaker keys. Overrides regions. Returns only the listed books that have data for this event+market.","title":"Bookmakers"},"description":"Comma-separated bookmaker keys. Overrides regions. Returns only the listed books that have data for this event+market. Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"include","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated. 'verification' adds verified_at / line_changed_at / is_current per bookmaker.","default":"normalized","title":"Include"},"description":"Comma-separated. 'verification' adds verified_at / line_changed_at / is_current per bookmaker."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost-formula":"len(markets) x len(regions)","x-credit-cost-floor":1,"x-credit-cost-type":"variable","x-credit-cost-example":"?markets=h2h®ions=us,uk = 1x2 = 2 credits","x-credit-cost-description":"Single-event odds (deep markets supported)","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/player-ratings":{"get":{"summary":"Get Player Ratings","description":"Market-implied player ratings derived from PREGAME closing prices.\n\nSupported today for table_tennis (and its sub-leagues), where we hold\npaired 1v1 moneylines from bovada and tenbet.\n\nWhat the rating is: an iterative ELO fit to the de-vigged implied win\nprobability of the LAST price each book posted BEFORE the listed start\ntime. That is the market's pregame estimate of who is stronger.\n\nWhat it is not:\n - Not an official ITTF or WTT ranking.\n - Not a results based rating. A price quoted at or after first serve\n is an in play price and is excluded, because an in play price\n encodes who is currently ahead, which is close to encoding the\n result. On bovada table tennis, 83 percent of the moneyline rows\n we hold were written after the fixture started, so an endpoint\n that used \"the latest price\" was reporting results dressed up as\n a market estimate. That was the bug this endpoint used to have.\n - Not a prediction. It summarises market consensus.\n\nSelection rules, all reported back in the response:\n - A fixture is keyed by (home, away, commence_time), so the same two\n players meeting twice in one day is two fixtures, never one.\n - Both sides must have a pregame price or the fixture is dropped.\n - The two implied probabilities must sum to a plausible book total\n (0.98 to 1.30) or the fixture is dropped.\n - One fixture counts once, whatever the number of books pricing it.\n - A book names the side either bare (\"Adam Svoboda\") or as a\n compound market label (\"Adam Svoboda v Vaclav Dolezal · Match\n Winner · Adam Svoboda\"); both are read, and the trailing segment\n must equal a side name exactly. Reading only the bare form used\n to exclude every tenbet row and then report those fixtures as\n dropped for having no pregame price, which was a parsing\n exclusion wearing an in-play exclusion's label. Measured on prod\n 2026-09-04 over 30 days of table tennis, reading both forms takes\n usable fixtures from 418 to 8,750.\n\nmatches_dropped_inplay_only counts fixtures for which we hold a\nmatch-winner price named to a side, but every such price was quoted\nat or after the start time. It does not count fixtures we hold no\nmatch-winner price for at all: those never enter fixtures_seen.\n\nReturns the top `limit` players by rating, descending. Players below\n`min_matches` are excluded as noise.\n\nCost: 2 credits per call.","operationId":"get_player_ratings_v1_sports__sport_key__player_ratings_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"min_matches","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"description":"Minimum matches required for a player to appear","default":3,"title":"Min Matches"},"description":"Minimum matches required for a player to appear"},{"name":"window_days","in":"query","required":false,"schema":{"type":"integer","maximum":180,"minimum":1,"description":"Recency window for source matches","default":30,"title":"Window Days"},"description":"Recency window for source matches"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":2,"x-credit-cost-type":"fixed","x-credit-cost-description":"Market-implied ELO from pregame closing prices (1v1 sports)","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/scores":{"get":{"summary":"Get Scores","description":"Get live scores and recent results. 1-2 credits.\n\nCovers NHL, NBA, MLB, NFL, MMA/UFC, and major soccer leagues via ESPN.\nReturns live game state, scores, period/quarter/inning, and completion status.\n\n`daysFrom` adds completed games from the last 1-14 days and costs a\nsecond credit. It is only honoured, and only billed, for sports we can\nactually look back on; ask for it on a sport we hold no score history\nfor and you pay the base 1 credit instead of 2.\n\n**Example:** `GET /v1/sports/americanfootball_nfl/scores`","operationId":"get_scores_v1_sports__sport_key__scores_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"daysFrom","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":14,"minimum":1},{"type":"null"}],"description":"Days of history (1-14). Costs a second credit, and only on sports we hold score history for; on any other sport it is ignored and you are charged the base 1 credit. For longer windows use the historical endpoints.","title":"Daysfrom"},"description":"Days of history (1-14). Costs a second credit, and only on sports we hold score history for; on any other sport it is ignored and you are charged the base 1 credit. For longer windows use the historical endpoints."},{"name":"dateFormat","in":"query","required":false,"schema":{"type":"string","default":"iso","title":"Dateformat"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost-formula":"2 if daysFrom and the sport has score history else 1","x-credit-cost-floor":1,"x-credit-cost-type":"variable","x-credit-cost-example":"?daysFrom=3 = 2 credits, no daysFrom = 1 credit, ?daysFrom=3 on a sport with no score history = 1 credit","x-credit-cost-description":"Scores for live and completed games","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/historical/sports/{sport_key}/odds":{"get":{"summary":"Get Historical Odds","description":"Get historical odds at a point in time.\n\nCredits: 10 × billable markets × regions. This archive holds moneyline,\nspread and total columns and nothing else, so `h2h`, `spreads` and\n`totals` are the only keys that can be billed here. `outrights`, the\nalt ladder and every prop key are accepted, return nothing from this\narchive, cost zero, and come back named in `x-markets-unservable` with\nthe archive endpoint that does hold them in\n`x-markets-served-elsewhere` (prop history is\n`/v1/historical/sports/{sport_key}/closing-odds?markets=`; period\nhistory is `/v1/historical/sports/{sport_key}/period_markets`).\n\niter_058 #463 — `date=` vs `dateFrom=&dateTo=` return different rows\nfor the same date. The single-date path (`?date=YYYY-MM-DD`) reads\nfrom the long-term archive and includes the `_an` \"anchored\"\nclosing-line variants (e.g. `draftkings_an`, `fanduel_an`,\n`bet365_an`) alongside the regular bookmaker rows. The range path\n(`?dateFrom=...&dateTo=...`) reads from the rolling snapshot table\nand surfaces only the bare bookmaker keys (without `_an`).\nFor consistent backtest joins, prefer `?date=` when you need\nclosing-line anchors. See /v1/bookmakers/{key}.historical_variants\nfor the suffix convention.\n\n`date` omitted returns the latest snapshot (equivalent to current\n/v1/sports/{sport_key}/odds but routed through the historical\npricing). This matches the UX of /closing-odds which doesn't\nrequire an explicit date either - removes the 422-on-missing-param\ntrap that customers hit when they don't read the schema first.","operationId":"get_historical_odds_v1_historical_sports__sport_key__odds_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO 8601 timestamp OR YYYY-MM-DD date. Omit to get the most recent snapshot. See doc note below for behavior.","title":"Date"},"description":"ISO 8601 timestamp OR YYYY-MM-DD date. Omit to get the most recent snapshot. See doc note below for behavior."},{"name":"regions","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated: us,us2,uk,eu,au. Default us.","default":"us","title":"Regions"},"description":"Comma-separated: us,us2,uk,eu,au. Default us. Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"markets","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated: h2h,spreads,totals. Default h2h. Those three are the whole servable list here; any other key is answered but billed zero and routed by the x-markets-served-elsewhere header.","default":"h2h","title":"Markets"},"description":"Comma-separated: h2h,spreads,totals. Default h2h. Those three are the whole servable list here; any other key is answered but billed zero and routed by the x-markets-served-elsewhere header. Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"oddsFormat","in":"query","required":false,"schema":{"type":"string","default":"decimal","title":"Oddsformat"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost-formula":"10 x len(markets) x len(regions)","x-credit-cost-floor":10,"x-credit-cost-type":"variable","x-credit-cost-example":"?markets=h2h®ions=us = 10x1x1 = 10 credits","x-credit-cost-description":"Historical odds (10x markup over live odds)","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Historical"]}},"/v1/historical/sports/{sport_key}/matches":{"get":{"summary":"Get Historical Matches","description":"Historical match/result archive.\n\nUse this when a source has real historical match data but not historical\nprices. Rows with actual odds include `has_odds=true` and an `odds` object.","operationId":"get_historical_matches_v1_historical_sports__sport_key__matches_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Shortcut for dateFrom=dateTo=date (YYYY-MM-DD)","title":"Date"},"description":"Shortcut for dateFrom=dateTo=date (YYYY-MM-DD)"},{"name":"dateFrom","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Start date YYYY-MM-DD","title":"Datefrom"},"description":"Start date YYYY-MM-DD"},{"name":"dateTo","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"End date YYYY-MM-DD","title":"Dateto"},"description":"End date YYYY-MM-DD"},{"name":"sources","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated sources, e.g. hltv,opendota,vlrgg,pinnacle","title":"Sources"},"description":"Comma-separated sources, e.g. hltv,opendota,vlrgg,pinnacle"},{"name":"pricedOnly","in":"query","required":false,"schema":{"type":"boolean","description":"Only rows that include real odds/prices","default":false,"title":"Pricedonly"},"description":"Only rows that include real odds/prices"},{"name":"includeRaw","in":"query","required":false,"schema":{"type":"boolean","description":"Include raw source payload","default":false,"title":"Includeraw"},"description":"Include raw source payload"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":5000,"minimum":1,"default":1000,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":2,"x-credit-cost-type":"fixed","x-credit-cost-description":"Historical match/result archive","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Historical"]}},"/v1/historical/sports/{sport_key}/coverage":{"get":{"summary":"Get Historical Coverage","description":"Per-source row count for historical-matches in a window.\n\nAnswers \"before I burn credits filtering, which sources have actual\ndata for this sport over this date range, and how deep does each\none go?\" Useful for esports where some sources (HLTV for CS2)\ncarry years of match results while others (Pinnacle for CS2)\nonly go back a few weeks.\n\nCost: 1 credit. Returns:\n\n {\n \"sport_key\": \"esports_cs2\",\n \"window\": {\"date_from\": \"...\", \"date_to\": \"...\"},\n \"by_source\": {\n \"hltv\": {\"rows\": 15379, \"first_date\": \"2025-01-03\",\n \"last_date\": \"2026-05-07\", \"priced_rows\": 0},\n \"pinnacle\": {\"rows\": 50, \"first_date\": \"2026-05-01\",\n \"last_date\": \"2026-05-11\", \"priced_rows\": 50},\n ...\n },\n \"_note\": \"...\"\n }\n\nAll dates are ISO YYYY-MM-DD. Sources with zero rows in the window\nare omitted.","operationId":"get_historical_coverage_v1_historical_sports__sport_key__coverage_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"dateFrom","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Start date YYYY-MM-DD","title":"Datefrom"},"description":"Start date YYYY-MM-DD"},{"name":"dateTo","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"End date YYYY-MM-DD","title":"Dateto"},"description":"End date YYYY-MM-DD"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":1,"x-credit-cost-type":"fixed","x-credit-cost-description":"Per-source row count for historical matches","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Historical"]}},"/v1/sports/{sport_key}/line-movement":{"get":{"summary":"Get Line Movement","description":"Track how odds move over time for an event. 2 credits.\n\nReturns time-series of odds snapshots for the specified event, showing\nline/price changes across bookmakers. Useful for CLV analysis and\nsteam detection.\n\n**Example:** `GET /v1/sports/baseball_mlb/line-movement?eventId=abc123&market=player_hits&player=Aaron Judge`","operationId":"get_line_movement_v1_sports__sport_key__line_movement_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"eventId","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Eventid"}},{"name":"event_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Event Id"}},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"}},{"name":"bookmaker","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bookmaker"}},{"name":"market","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to market_key (e.g. player_points)","title":"Market"},"description":"Filter to market_key (e.g. player_points)"},{"name":"market_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Alias for market","title":"Market Key"},"description":"Alias for market"},{"name":"player","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to specific player","title":"Player"},"description":"Filter to specific player"},{"name":"hours","in":"query","required":false,"schema":{"type":"integer","description":"Lookback window in hours (max 168)","default":24,"title":"Hours"},"description":"Lookback window in hours (max 168)"},{"name":"window_minutes","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Lookback window in minutes (max 10080)","title":"Window Minutes"},"description":"Lookback window in minutes (max 10080)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":2,"x-credit-cost-type":"fixed","x-credit-cost-description":"Historical odds movement for an event","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/prediction-markets/{sport_key}":{"get":{"summary":"Get Prediction Markets","description":"Get prediction-market prices (Kalshi + Polymarket). 1 credit.\n\nReturns one row per (source, market) snapshot from the last hour.\nEXCLUSIVE: not available on the-odds-api.\n\nUse ?sources=kalshi or ?sources=polymarket to limit to one venue.\nFor free-text discovery across Kalshi, Polymarket, and Novig (no\ncredit charge), use /v1/event-markets/search instead.\n\nResponses are cached server-side for 30s; the `X-Cache` header is\n`HIT` or `MISS` so clients can see whether they hit Postgres.","operationId":"get_prediction_markets_v1_prediction_markets__sport_key__get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"sources","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated source keys. Supports kalshi, polymarket. Defaults to both.","default":"kalshi,polymarket","title":"Sources"},"description":"Comma-separated source keys. Supports kalshi, polymarket. Defaults to both."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":1,"x-credit-cost-type":"fixed","x-credit-cost-description":"Kalshi + Polymarket prediction markets for a sport","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Prediction Markets"]}},"/v1/prediction-markets/crypto/{asset}":{"get":{"summary":"Get Crypto Prediction Markets","description":"Latest snapshot of every open Kalshi crypto prediction market\nfor the requested asset.\n\nWhat is currently collected is BTC price ladders: `price_at_or_above`\n(one contract per strike, e.g. `KXBTCD-26AUG0117-T53499.99`, \"Bitcoin\nprice on Aug 1, 2026?\") and `price_range`. ETH is in the archive but\nstopped writing on 2026-07-30, so it is served by the historical\nendpoint and not by this one. Ask for a combination outside the window\nthis endpoint queries and you get a 404 naming what is live, with no\ncredit charged, rather than a billed empty array.\n\nEach row shows the latest yes/no bid/ask + mid-market implied\nprobability + volume + open interest. Use the historical endpoint\n(`/v1/historical/prediction-markets/crypto/{asset}`) for replay, and\nfor any asset that is in the archive but not currently live.\n\nCost: 1 credit. Refreshes within ~5s of the underlying collector\npoll.","operationId":"get_crypto_prediction_markets_v1_prediction_markets_crypto__asset__get","parameters":[{"name":"asset","in":"path","required":true,"schema":{"type":"string","title":"Asset"}},{"name":"market_type","in":"query","required":false,"schema":{"type":"string","description":"Filter by market_type. One of: direction_15m, direction_1h, direction_daily, price_range, price_above, all. Default all.","default":"all","title":"Market Type"},"description":"Filter by market_type. One of: direction_15m, direction_1h, direction_daily, price_range, price_above, all. Default all."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":200,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":1,"x-credit-cost-type":"fixed","x-credit-cost-description":"Kalshi crypto prediction markets (BTC/ETH/SOL)","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Prediction Markets"]}},"/v1/historical/prediction-markets/crypto/{asset}":{"get":{"summary":"Get Crypto Prediction Markets Historical","description":"Tick-by-tick replay of Kalshi crypto prediction markets.\n\nEvery poll snapshot we captured is preserved here. The collector\npolls each open market every ~5 s, so a one-hour replay returns\n~720 rows per market.\n\nThe archive runs from 2026-05-13 and holds two assets: BTC\n(`price_at_or_above`, `price_range`) and ETH (`price_at_or_above`,\n`other`). ETH is reachable here even though it is no longer live.\nA combination we have never carried returns 404 with no credit\ncharged; a range we simply have no rows for returns an empty\n`snapshots` array, because coverage is measured over the archive and\nnot over your requested window. Provide `market_ticker` to focus on\none resolution slot.\n\nCost: 2 credits. Replay depth is bounded by your plan's historical\nwindow; an explicit `from` before it returns 403 HISTORICAL_LIMIT.","operationId":"get_crypto_prediction_markets_historical_v1_historical_prediction_markets_crypto__asset__get","parameters":[{"name":"asset","in":"path","required":true,"schema":{"type":"string","title":"Asset"}},{"name":"market_type","in":"query","required":false,"schema":{"type":"string","description":"Same as live endpoint: direction_15m / direction_1h / direction_daily / price_range / price_above / all","default":"all","title":"Market Type"},"description":"Same as live endpoint: direction_15m / direction_1h / direction_daily / price_range / price_above / all"},{"name":"market_ticker","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Replay a single Kalshi market_ticker (e.g. KXBTCD-26MAY13H1515).","title":"Market Ticker"},"description":"Replay a single Kalshi market_ticker (e.g. KXBTCD-26MAY13H1515)."},{"name":"from","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Start timestamp (unix ms). Defaults to 6 hours ago.","title":"From"},"description":"Start timestamp (unix ms). Defaults to 6 hours ago."},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"End timestamp (unix ms). Defaults to now.","title":"To"},"description":"End timestamp (unix ms). Defaults to now."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":2000,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":2,"x-credit-cost-type":"fixed","x-credit-cost-description":"Historical Kalshi crypto market snapshots","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Historical"]}},"/v1/sports/{sport_key}/props":{"get":{"summary":"Get Props","description":"Get player prop odds from 10+ sources. 3 credits.\n\nSourced from DraftKings, FanDuel, Caesars, Bovada, Pinnacle, Fliff (real\nAmerican odds), PrizePicks, Underdog, Betr, Pick6, Sleeper, Novig, ProphetX,\nPolymarket (event markets, opt-in via include_event_markets=true).\n\n**Freshness.** Active books are polled every few seconds; the poll cadence\nper source runs ~30-120s depending on the book. This endpoint serves the\nmost recent row per book written within the last 60 minutes, so a quiet\nmarket can return a line several minutes old. Every row (and every entry in\na grouped `books[]`) carries `age_seconds`, the real age of that write. To\nenforce a tighter window pass `?maxAgeSec=` (e.g. `?maxAgeSec=120` for\nprops no older than two minutes); rows past the bound, and rows whose write\ntime can't be parsed, are dropped.\n\n**Supported sports:** NFL, NBA, MLB, NHL, MMA/UFC, Soccer, Tennis, Golf, and more.\n\n**Market keys:** player_points, player_rebounds, player_assists, player_three_pointers,\nplayer_strikeouts, player_hits, player_home_runs, player_total_bases, player_runs,\nplayer_rbis, player_goals, player_shots_on_goal, player_pts_rebs_asts, and 50+ more.\n\n**Example:** `GET /v1/sports/basketball_nba/props?markets=player_points,player_rebounds`\n\n**Filter by player:** `?player=LeBron`\n\n**Filter by book:** `?bookmakers=fliff,pinnacle`\n\n**Polymarket / Kalshi event markets:** `?bookmakers=polymarket` (auto-includes\nevent markets) or `?include_event_markets=true` (any source). For a dedicated\nprediction-market endpoint with question text + event_url, use\n`/v1/prediction-markets/{sport_key}`.\n\n**Pagination headers** (sleep_iter_7 #508): every response carries\n`x-result-page-size`, `x-result-row-count`, `x-result-limit`,\n`x-result-offset`, `x-result-has-more`. When `x-result-has-more: true`, the\nresponse also carries `x-next-offset: ` showing the offset to request\nfor the next page. Use these to walk a large result set without truncating\nsilently. The default limit of 5000 is fine for most queries; a\nfull bet365 baseball_mlb response needs ~3 pages of 5000 to retrieve\nall available rows.\n\n**Pagination with `grouped=true`:** `limit` and `offset` count ROWS (one row\nper book) in both modes, so `x-next-offset` means the same thing either way.\nA grouped page therefore returns FEWER entries than `limit` - several rows\ncollapse into one prop - and `x-result-page-size` (props returned) will sit\nbelow `x-result-row-count` (rows behind them). Trust `x-result-has-more`,\nnot the entry count, to decide whether to ask for another page. A prop whose\nbooks straddle a page boundary arrives with a partial `books[]` on each\npage; merge pages on (event_id, player, market_key, line). To avoid that\nentirely, narrow with `?markets=` / `?bookmakers=` so the whole result fits\nin one page.\n\nReturns over/under lines with American odds from each source.","operationId":"get_props_v1_sports__sport_key__props_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"markets","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated prop market keys (e.g. player_pass_yds,player_points)","title":"Markets"},"description":"Comma-separated prop market keys (e.g. player_pass_yds,player_points) Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated bookmaker keys","title":"Bookmakers"},"description":"Comma-separated bookmaker keys Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"player","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by player name (partial match)","title":"Player"},"description":"Filter by player name (partial match)"},{"name":"eventId","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by event ID","title":"Eventid"},"description":"Filter by event ID"},{"name":"oddsFormat","in":"query","required":false,"schema":{"type":"string","default":"american","title":"Oddsformat"}},{"name":"dfsOdds","in":"query","required":false,"schema":{"type":"string","description":"DFS normalization: 'midpoint' = +100/-100 (default, zero-vig), 'effective' = per-book implied (PrizePicks/Underdog = -137/-137)","default":"midpoint","title":"Dfsodds"},"description":"DFS normalization: 'midpoint' = +100/-100 (default, zero-vig), 'effective' = per-book implied (PrizePicks/Underdog = -137/-137)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"description":"Max rows returned (default 5000, max 10000)","default":5000,"title":"Limit"},"description":"Max rows returned (default 5000, max 10000)"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":0,"description":"Page offset within the result set. Combine with limit for pagination. For results past 10000 rows, narrow via ?markets= or ?bookmakers= filters instead.","default":0,"title":"Offset"},"description":"Page offset within the result set. Combine with limit for pagination. For results past 10000 rows, narrow via ?markets= or ?bookmakers= filters instead."},{"name":"grouped","in":"query","required":false,"schema":{"type":"boolean","description":"Return one entry per prop with a books[] array (recommended) instead of one row per book","default":false,"title":"Grouped"},"description":"Return one entry per prop with a books[] array (recommended) instead of one row per book"},{"name":"include_event_markets","in":"query","required":false,"schema":{"type":"boolean","description":"Include futures and prediction-market rows that lack a single home/away_team (Polymarket yes/no questions, Underdog season-longs, etc). Auto-enabled when bookmakers includes 'polymarket'.","default":false,"title":"Include Event Markets"},"description":"Include futures and prediction-market rows that lack a single home/away_team (Polymarket yes/no questions, Underdog season-longs, etc). Auto-enabled when bookmakers includes 'polymarket'."},{"name":"maxAgeSec","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":3600,"minimum":1},{"type":"null"}],"description":"Only return prop rows written within this many seconds. Every row already carries age_seconds; this drops any older than the bound (and any whose write time can't be parsed). Omit to serve the full recency window (up to 3600s / 60 min).","title":"Maxagesec"},"description":"Only return prop rows written within this many seconds. Every row already carries age_seconds; this drops any older than the bound (and any whose write time can't be parsed). Omit to serve the full recency window (up to 3600s / 60 min)."},{"name":"include","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Accepted only as 'normalized', which is what this endpoint already returns. /props serves one shape, so any other token (for example 'raw' or 'slim') is rejected with 400 instead of being ignored. The shape tokens live on GET /v1/sports/{sport_key}/odds.","title":"Include"},"description":"Accepted only as 'normalized', which is what this endpoint already returns. /props serves one shape, so any other token (for example 'raw' or 'slim') is rejected with 400 instead of being ignored. The shape tokens live on GET /v1/sports/{sport_key}/odds."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":3,"x-credit-cost-type":"fixed","x-credit-cost-description":"Player props for one sport","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/props/coverage":{"get":{"summary":"Get Props Coverage","description":"Show which fresh books survive the exact /props request filters.\n\nThis endpoint is for support diagnostics and does not charge credits.","operationId":"get_props_coverage_v1_sports__sport_key__props_coverage_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"markets","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated prop market keys","title":"Markets"},"description":"Comma-separated prop market keys Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated bookmaker keys","title":"Bookmakers"},"description":"Comma-separated bookmaker keys Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"player","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by player name","title":"Player"},"description":"Filter by player name"},{"name":"eventId","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by event ID","title":"Eventid"},"description":"Filter by event ID"},{"name":"oddsFormat","in":"query","required":false,"schema":{"type":"string","default":"american","title":"Oddsformat"}},{"name":"dfsOdds","in":"query","required":false,"schema":{"type":"string","default":"midpoint","title":"Dfsodds"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":5000,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/odds/coverage":{"get":{"summary":"Get Odds Coverage","description":"Per-book game-line coverage for this sport: which books have fresh\nh2h / spread / total prices, how many games each, and the freshest\ntimestamp per book.\n\nUse this before hitting /odds to check whether the books you care\nabout are actually live for the sport you're requesting. Diagnostic\nendpoint, no credit charge.","operationId":"get_odds_coverage_v1_sports__sport_key__odds_coverage_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"fresh_within_seconds","in":"query","required":false,"schema":{"type":"integer","maximum":86400,"minimum":60,"description":"Only count books with at least one update within this window.","default":900,"title":"Fresh Within Seconds"},"description":"Only count books with at least one update within this window."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sports & Odds"]}},"/v1/pinnacle-coverage":{"get":{"summary":"Pinnacle Coverage","description":"**Public, no-auth.** Pinnacle presence/absence per sport_key.\n\nFor every sport we track, shows whether Pinnacle has fresh prices\nand how stale the latest capture is. Use this to verify Pinnacle\nis actually live for the sports you're betting before committing\nto a backtest or arb pipeline that anchors on Pinnacle.\n\nCached 5 min. iter_049 #424.","operationId":"pinnacle_coverage_v1_pinnacle_coverage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Discovery"]}},"/v1/status":{"get":{"summary":"Status Json","description":"**Public, no-auth.** Live endpoint health: per-source freshness,\nrequest-rate-log p50/p95, total request count last hour.\n\nSame data the /status page renders. Refreshes every 90 seconds.\n\nCached 90s with stale-while-revalidate plus background refresh, so a\nrequest never blocks on the heavy per-source aggregate. Warm cache is\nserved directly; a stale cache is served immediately while one worker\nrecomputes off the request path; only a truly cold cache computes inline,\nsingle-flighted and time-bounded so a slow aggregate returns a quick 503\ninstead of hanging into a proxy 500. (Pre-fix, concurrent cold polls each\nran the same ~3s aggregate at once; I/O contention inflated every one to\n~18s and tripped the worker timeout into 500s.)","operationId":"status_json_v1_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Metadata"]}},"/v1/sports/{sport_key}/props/markets":{"get":{"summary":"List Prop Markets","description":"List available prop market keys for a sport. FREE.\n\nReturns all prop market types we have data for (e.g. player_points, player_pass_yds).\nUse these keys with the /props endpoint's markets parameter.\n\nsleep_iter_51 #549: ETag-enabled now that iter_51 fixed the\ncross-worker non-determinism in get_available_prop_markets\n(the SQL ORDER BY now includes secondary sort keys).\nCustomers polling at SDK boot save bandwidth via 304\nround-trips. 5min cache TTL.","operationId":"list_prop_markets_v1_sports__sport_key__props_markets_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sports & Odds"]}},"/v1/historical/sports/{sport_key}/closing-odds":{"get":{"summary":"Get Historical Closing Odds","description":"Historical closing lines: game-line h2h/spreads/totals plus player props. 10 credits.\n\nGame lines come from the historical_odds archive (1.3M+ rows across 267 sport keys,\nsources include Pinnacle, bet365, Betway, William Hill, Interwetten, Ladbrokes,\nBetVictor, betfair_exchange).\n\nPlayer props come from prop_closing_lines (15.2M+ rows across 993 sport keys, all\nstandard player markets). Use `markets=player_strikeouts` (or any other player_*\nkey) to pull these. The `player=` param filters to a specific player by substring.\n\nPass `include_imports=true` to also union your own customer-imported rows\nfrom `POST /v1/historical/closing-lines/import` (cost: 0 extra credits).\n\nROW CAP AND ORDERING (contract): every leg of the answer is returned\nNEWEST GAME_DATE FIRST under a hard row cap, so when a request matches\nmore rows than its cap the OLDEST rows are the ones dropped, never the\nmost recent. The two PUBLIC legs (game lines, props) are capped at\n`x-archive-row-cap` each; the `include_imports` leg is your own upload,\nscoped to your API key, and carries a separate, larger 10,000-row cap that\n`x-archive-row-cap` does NOT describe.\n\n`x-archive-truncated` is `false`, or a comma-separated list of the public\nlegs that hit their cap (`game_lines`, `props`). The imports leg is never\nnamed there. When a leg is clipped the response also carries\n`x-archive-oldest-game-date`: the exact value to send back as `dateTo` to\ncontinue reading older rows without skipping any.\n\nPAGING: `limit` + `offset`, or `dateTo`. `limit` and `offset` apply to each\nPUBLIC leg independently and are the reliable way to read a match set\nbigger than one page: walking `offset` 0, limit, 2*limit, ... returns every\nmatching row exactly once, because both public legs are ordered on a key\nthat is unique within the leg (props: the seven-column natural key the\ndedupe is built on; game lines: game_date plus the row id). Omit both and\nyou get exactly the pre-paging behaviour: one page of `x-archive-row-cap`\nrows per public leg. The response echoes what it served as\n`x-archive-limit` / `x-archive-offset`, and a clipped page carries\n`x-archive-next-offset`, the value to send back.\n\nTHE `include_imports` LEG IS SERVED ONLY ON `offset=0`. It is your own\nupload under its own larger cap, not part of the paged archive read, so\nre-sending it on every page would duplicate every imported row across your\npages. Page 1 carries it in full; later pages carry archive rows only.\n\nA dateTo WALK IS STILL SUPPORTED AND IS CHEAPER OVER LONG RANGES, but it\nTERMINATES ONLY WHEN THE DATE MOVES: `dateTo` cannot advance when a single\ngame_date fills the whole cap on its own (measured 2026-08-08:\nsoccer_uefa_conference_league with a game-level market named returns 5,000\nrows that are all game_date 2026-08-06, a day that holds 6,983). In that\ncase `x-archive-oldest-game-date` equals the newest date you received, the\nresponse says so in `x-archive-truncated-hint`, and re-requesting with that\n`dateTo` returns the identical page for another 10 credits. Use `offset=`\nto read past it, or narrow with `markets=`, `bookmakers=` or `player=`.\nClients that loop on dates should stop when `x-archive-oldest-game-date`\nstops decreasing.\n\nTHE CAP IS SHARED ACROSS BOOKS, so a dense book can crowd a sparse one off\nthe page even when the date cursor advances normally. Pass `bookmakers=`\nwhen you only need one book's closing line. Measured 2026-08-08 on\nsoccer_uefa_conference_league_qualifiers with\n`markets=moneyline,correct_score,corners,double_chance`: the answer is\n4,940 tenbet rows and 60 pinnacle rows, and the pinnacle rows older than\n2026-08-04 sit behind the cursor rather than in the first page.\n`bookmakers=pinnacle` returns all 256 of them in one call.\n\nExamples:\n\n- `GET /v1/historical/sports/baseball_mlb/closing-odds?markets=player_strikeouts&date=2025-08-15&player=Skenes`\n- `GET /v1/historical/sports/soccer_epl/closing-odds?bookmakers=pinnacle&season=2023-24`\n- `GET /v1/historical/sports/basketball_nba/closing-odds?markets=h2h,player_points&dateFrom=2024-12-01&dateTo=2024-12-31&include_imports=true`\n- `GET /v1/historical/sports/soccer_uefa_conference_league/closing-odds?markets=player_shots_on_goal&date=2026-08-06&limit=5000&offset=5000`\n (second page of a day whose props overflow one page)","operationId":"get_historical_closing_odds_v1_historical_sports__sport_key__closing_odds_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"markets","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated. Game lines: h2h, spreads, totals. Player props: player_strikeouts, player_total_bases, player_points, player_rebounds, player_assists, player_pass_yds, player_rush_yds, player_shots_on_goal, etc. (any market_key from prop_closing_lines). Mix freely: markets=h2h,player_strikeouts.","default":"h2h","title":"Markets"},"description":"Comma-separated. Game lines: h2h, spreads, totals. Player props: player_strikeouts, player_total_bases, player_points, player_rebounds, player_assists, player_pass_yds, player_rush_yds, player_shots_on_goal, etc. (any market_key from prop_closing_lines). Mix freely: markets=h2h,player_strikeouts. Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Comma-separated. Game-line queries default to pinnacle. Prop-only queries default to all tracked books.","title":"Bookmakers"},"description":"Comma-separated. Game-line queries default to pinnacle. Prop-only queries default to all tracked books. Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"season","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Season filter for game lines (e.g. 2023-24)","title":"Season"},"description":"Season filter for game lines (e.g. 2023-24)"},{"name":"date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Specific date YYYY-MM-DD (shortcut for dateFrom=dateTo=date)","title":"Date"},"description":"Specific date YYYY-MM-DD (shortcut for dateFrom=dateTo=date)"},{"name":"dateFrom","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Start date (YYYY-MM-DD)","title":"Datefrom"},"description":"Start date (YYYY-MM-DD)"},{"name":"dateTo","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"End date (YYYY-MM-DD)","title":"Dateto"},"description":"End date (YYYY-MM-DD)"},{"name":"player","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter props to a specific player name (substring match)","title":"Player"},"description":"Filter props to a specific player name (substring match)"},{"name":"include_imports","in":"query","required":false,"schema":{"type":"boolean","description":"Also include rows you've imported via POST /v1/historical/closing-lines/import","default":false,"title":"Include Imports"},"description":"Also include rows you've imported via POST /v1/historical/closing-lines/import"},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":5000,"minimum":1},{"type":"null"}],"description":"Rows per PUBLIC leg (game lines, props) for this page, 1..5000. Omit for the full row cap, which is exactly the behaviour this endpoint had before paging existed.","title":"Limit"},"description":"Rows per PUBLIC leg (game lines, props) for this page, 1..5000. Omit for the full row cap, which is exactly the behaviour this endpoint had before paging existed."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"description":"Rows to skip in each PUBLIC leg. Use with the same limit to read past the row cap, including the case where one game_date fills a whole page and dateTo paging cannot advance. Both public legs are ordered on a unique key, so offset paging neither skips nor repeats rows.","default":0,"title":"Offset"},"description":"Rows to skip in each PUBLIC leg. Use with the same limit to read past the row cap, including the case where one game_date fills a whole page and dateTo paging cannot advance. Both public legs are ordered on a unique key, so offset paging neither skips nor repeats rows."},{"name":"oddsFormat","in":"query","required":false,"schema":{"type":"string","default":"american","title":"Oddsformat"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":10,"x-credit-cost-type":"fixed","x-credit-cost-description":"Historical closing-line archive","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Historical"]}},"/v1/historical/closing-lines/import":{"post":{"summary":"Import Customer Closing Lines","description":"Bring-your-own historical closing lines (Pro tier+).\n\nStores rows in a per-customer namespace (customer_imported_closing_lines\ntable, isolated by your api_key). **The public historical archive\n(historical_odds) is read-only via this endpoint** — you can only\nsee / delete YOUR OWN imports. Other customers' imports are\ninvisible to you and yours to them.\n\nQuery your imports back through the standard\n`GET /v1/historical/sports/{sport_key}/closing-odds?include_imports=true`\nendpoint.\n\nCost: 1 credit per 1,000 rows submitted (rounded up).\nIdempotent: re-submitting the same row is a no-op via ON CONFLICT DO NOTHING.\n\nTier gate: Pro+ (iteration_019 #197 — free-tier was accepted at\nauth but the use case is paid-customer-only, so we gate explicitly).\n\nExample body:\n\n```\n[\n {\n \"game_date\": \"2025-08-15\",\n \"sport_key\": \"baseball_mlb\",\n \"home_team\": \"Detroit Tigers\",\n \"away_team\": \"Texas Rangers\",\n \"source\": \"fanduel\",\n \"player_name\": \"Tarik Skubal\",\n \"market_key\": \"player_strikeouts\",\n \"line\": 7.5,\n \"over_price\": -118,\n \"under_price\": -102\n }\n]\n```","operationId":"import_customer_closing_lines_v1_historical_closing_lines_import_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"Array of closing-line rows. Each row must have at minimum: game_date (YYYY-MM-DD), sport_key, home_team, away_team, source, market_key, line. Optional: commence_time, player_name, market_label, over_price, under_price, raw_json.","title":"Rows"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost-formula":"ceil(rows / 1000)","x-credit-cost-floor":1,"x-credit-cost-type":"variable","x-credit-cost-example":"5000-row import = 5 credits","x-credit-cost-description":"Bring-your-own historical close lines","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Historical"]},"delete":{"summary":"Delete Customer Closing Imports","description":"Delete YOUR OWN imported closing-line rows.\n\nScoped to the customer_imported_closing_lines table, filtered by\nyour api_key — this endpoint **cannot touch the public historical\narchive** (historical_odds is read-only via the API).\n\nWithout `sport_key`, deletes every row YOU'VE imported. With it,\nonly your rows for that sport. Always requires `confirm=true`.\n\nTier gate: Pro+ (matches the import sibling — iteration_019 #198).","operationId":"delete_customer_closing_imports_v1_historical_closing_lines_import_delete","parameters":[{"name":"sport_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a sport_key (optional)","title":"Sport Key"},"description":"Filter to a sport_key (optional)"},{"name":"confirm","in":"query","required":false,"schema":{"type":"boolean","description":"Must be true to actually delete","default":false,"title":"Confirm"},"description":"Must be true to actually delete"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Historical"]}},"/v1/historical/coverage":{"get":{"summary":"Historical Coverage","description":"**Public, no-auth.** Cross-source historical odds coverage stats:\ntotal rows, span, sources per sport, score-coverage percentage.\nUse this to verify the archive is rich enough for your backtest\nbefore committing to a paid tier.\n\nCached 5 min with stale-while-revalidate (single-flight refresh).\niter_13 #501.","operationId":"historical_coverage_v1_historical_coverage_get","parameters":[{"name":"sport_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter the by_sport array + summary to a single sport. iter_062 #476.","title":"Sport Key"},"description":"Filter the by_sport array + summary to a single sport. iter_062 #476."},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter the by_source array + summary to a single source. iter_062 #476.","title":"Source"},"description":"Filter the by_source array + summary to a single source. iter_062 #476."},{"name":"min_rows","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"Drop by_sport / by_source entries with fewer than this many rows. iter_062 #476.","title":"Min Rows"},"description":"Drop by_sport / by_source entries with fewer than this many rows. iter_062 #476."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Historical"]}},"/v1/historical/stats":{"get":{"summary":"Get Historical Stats","description":"Public endpoint. Stats about our historical odds archive.","operationId":"get_historical_stats_v1_historical_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Historical"]}},"/v1/status/history":{"get":{"summary":"Status History","description":"Trailing-window SLA history per source. Public, no auth.\n\nPairs with /v1/status (which is point-in-time). This endpoint\nreturns the timeline that backs the /status page's uptime chart.\nDefault window is 24 hours (86400s). Max is 7 days.\n\nMemory rule compliance: returns metadata only (SLA classification\n+ age). No prices touched. Reads from a Redis ring buffer populated\nby the collector's source_quality_history_loop; the buffer is\nbounded so the endpoint cost is constant regardless of uptime.\n\nAn empty `sources` always comes with a `_note` saying why, and\n`newest_sample_age_s` when the buffer holds samples older than the\nrequested window, so a stopped sampler is distinguishable from a quiet\none without opening a support ticket.\n\nReturns:\n {\n \"as_of\": \"...\",\n \"window_s\": 86400,\n \"sources\": {\n \"pinnacle\": [\n {\"ts\": ..., \"sla\": \"ok\", \"age_s\": 4.2},\n ...\n ],\n ...\n }\n }","operationId":"status_history_v1_status_history_get","parameters":[{"name":"window_s","in":"query","required":false,"schema":{"type":"integer","maximum":604800,"minimum":300,"default":86400,"title":"Window S"}},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional single source to return.","title":"Source"},"description":"Optional single source to return."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/historical/source-quality.csv":{"get":{"summary":"Historical Source Quality Csv","description":"Historical per-source observation rate, hourly buckets. Public, no\nauth, no credits.\n\nReturns one row per (hour_bucket, source, table_name) triple with\nthe observed row count and the latest observation timestamp inside\nthe bucket. This is the historical companion to\n/v1/meta/source-quality (which only shows the live snapshot).\n\nUseful for SLA validation: did Pinnacle actually maintain its\ndeclared freshness yesterday? Did the FanDuel ingest dip during the\nSunday-noon NFL kickoff window?\n\nMemory rule compliance: read-only on metadata. No prices touched.","operationId":"historical_source_quality_csv_v1_historical_source_quality_csv_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":720,"minimum":1,"description":"Window in hours, 1 to 720 (max 30 days).","default":24,"title":"Hours"},"description":"Window in hours, 1 to 720 (max 30 days)."},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional source filter (e.g. pinnacle, draftkings).","title":"Source"},"description":"Optional source filter (e.g. pinnacle, draftkings)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Historical"]}},"/v1/historical/source-quality.json":{"get":{"summary":"Historical Source Quality Json","description":"JSON variant of /v1/historical/source-quality.csv.\n\nSame data, same hourly bucketing, structured as JSON for\nprogrammatic consumers that prefer JSON parsing over CSV. Public,\nno auth, no credits. 5-min server-side cache.\n\nResponse shape:\n {\n \"as_of\": \"...\",\n \"window_hours\": N,\n \"source_filter\": \"...\" or null,\n \"row_count\": M,\n \"rows\": [\n {\"bucket_start_utc\": \"...\", \"source\": \"...\",\n \"table_name\": \"...\", \"rows_seen\": N,\n \"latest_observation_utc\": \"...\"},\n ...\n ]\n }","operationId":"historical_source_quality_json_v1_historical_source_quality_json_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":720,"minimum":1,"description":"Window in hours, 1 to 720 (max 30 days).","default":24,"title":"Hours"},"description":"Window in hours, 1 to 720 (max 30 days)."},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional source filter (e.g. pinnacle, draftkings).","title":"Source"},"description":"Optional source filter (e.g. pinnacle, draftkings)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Historical"]}},"/v1/historical/closing-lines.json":{"get":{"summary":"Closing Lines Json","description":"JSON variant of /v1/historical/closing-lines.csv. Same data,\nsame 6-hour server cache, structured as JSON for programmatic\nconsumers that prefer JSON parsing over CSV.\n\nMetered: 1 credit per 1,000 rows delivered, minimum 1 credit.\nThe response carries X-Export-Rows, X-Export-Credits and\nX-Export-Rate so the cost of a call is always visible. Rows are\ncounted as DELIVERED, not as requested, so a wide limit on a thin\nslate costs only what it returns.\n\nResponse shape:\n {\n \"as_of\": \"...\",\n \"date\": \"YYYY-MM-DD\",\n \"filters\": {\"sport_key\": \"...\", \"source\": \"...\", \"limit\": N},\n \"row_count\": N,\n \"rows\": [\n {\"game_date\":\"...\",\"sport_key\":\"...\",\"commence_time\":\"...\",\n \"home_team\":\"...\",\"away_team\":\"...\",\"source\":\"...\",\n \"player_name\":\"...\",\"market_key\":\"...\",\"market_label\":\"...\",\n \"line\":N,\"over_price\":N,\"under_price\":N,\n \"over_implied_prob\":N,\"under_implied_prob\":N,\n \"snapshot_time\":\"...\"},\n ...\n ]\n }\n\nA row from a retired book (an operator that has closed; see\nRETIRED_SOURCES) additionally carries \"retired\": true and \"closed_on\".\n\nMemory rule compliance: every row comes from prop_closing_lines,\nwhich only carries observed closing snapshots. We never synthesize\na closing price.","operationId":"closing_lines_json_v1_historical_closing_lines_json_get","parameters":[{"name":"date","in":"query","required":true,"schema":{"type":"string","description":"YYYY-MM-DD (UTC). One day per request.","title":"Date"},"description":"YYYY-MM-DD (UTC). One day per request."},{"name":"sport_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional sport_key filter.","title":"Sport Key"},"description":"Optional sport_key filter."},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional source filter (e.g. pinnacle, draftkings).","title":"Source"},"description":"Optional source filter (e.g. pinnacle, draftkings)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500000,"minimum":1,"default":50000,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":1,"x-credit-cost-type":"fixed","x-credit-cost-description":"Daily closing-line JSON download (cached 6h)","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Historical"]}},"/v1/historical/closing-lines.csv":{"get":{"summary":"Closing Lines Csv","description":"Daily closing-line dump in CSV.\n\nMetered: 1 credit per 1,000 rows delivered, minimum 1 credit.\nThe response carries X-Export-Rows, X-Export-Credits and\nX-Export-Rate so the cost of a call is always visible. Rows are\ncounted as DELIVERED, not as requested, so a wide limit on a thin\nslate costs only what it returns.\n\nReturns one row per (game_date, home_team, away_team, player_name,\nmarket_key, line, source) tuple for the requested date. The natural\nkey is enforced by a unique constraint on prop_closing_lines so\nduplicate rows are impossible; each row is the latest-snapshot\narchive for that prop close.\n\nDesigned for backtesters who want a flat-file daily snapshot\nrather than the JSON shape of /v1/sports/{sport_key}/closing-lines.\n\nHeaders: text/csv; Content-Disposition attachment named by date.\nStream-safe for large windows up to limit=500000 rows.\n\nThe two trailing columns, `retired` and `closed_on`, are filled only\non rows from a retired book (an operator that has closed; see\nRETIRED_SOURCES) and empty everywhere else, so the JSON twin and this\nfile say the same thing about the same row.\n\nMemory rule compliance: every row comes from prop_closing_lines,\nwhich only carries observed closing snapshots. We never synthesize\na closing price.","operationId":"closing_lines_csv_v1_historical_closing_lines_csv_get","parameters":[{"name":"date","in":"query","required":true,"schema":{"type":"string","description":"YYYY-MM-DD (UTC). One day per request.","title":"Date"},"description":"YYYY-MM-DD (UTC). One day per request."},{"name":"sport_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional sport_key filter.","title":"Sport Key"},"description":"Optional sport_key filter."},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional source filter (e.g. pinnacle, draftkings).","title":"Source"},"description":"Optional source filter (e.g. pinnacle, draftkings)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500000,"minimum":1,"default":50000,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":1,"x-credit-cost-type":"fixed","x-credit-cost-description":"Daily closing-line CSV download (cached 6h)","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Historical"]}},"/v1/sports/{sport_key}/closing-lines":{"get":{"summary":"Get Closing Lines","description":"Get closing lines (last odds before match start). 5 credits.\n\nEXCLUSIVE. Returns the final odds snapshot for each bookmaker before each\nmatch commenced. Essential for CLV (closing line value) analysis.\n\n\"Before commenced\" is enforced, not assumed. A price quoted at or after\nthe listed start time is an in play price, not a closing line, and is\nexcluded; so is a row whose start time we cannot read, because we cannot\nattest which side of first serve it falls on. A fixture with no such\nprice is therefore absent from the response rather than represented by\nan in play price. That trims coverage on books that reprice during a\nmatch: measured 2026-09-04 over 30 days of table tennis, 54,120 of the\n55,353 bovada side rows this endpoint used to return were quoted after\nthe start time.\n\nSupports every sport key in GET /v1/sports. Pinnacle closing lines available\nfor the soccer competitions in that list.\n\n**Bookmakers:** pinnacle (default), draftkings, fanduel, betmgm, caesars, bovada, and more.\n\nDepth is bounded by your plan's historical window; an explicit\n`daysFrom` beyond it returns 403 HISTORICAL_LIMIT.\n\n**Example:** `GET /v1/sports/soccer_epl/closing-lines?bookmakers=pinnacle&daysFrom=7`\n\nResponse includes h2h (3-way with draw for soccer), spreads, and totals.\nEach event includes the bookmaker's last update timestamp before kickoff.","operationId":"get_closing_lines_v1_sports__sport_key__closing_lines_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bookmakers"},"style":"form","explode":false,"description":"Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once."},{"name":"daysFrom","in":"query","required":false,"schema":{"type":"integer","maximum":30,"minimum":1,"default":3,"title":"Daysfrom"}},{"name":"oddsFormat","in":"query","required":false,"schema":{"type":"string","default":"american","title":"Oddsformat"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"description":"sleep_iter_15 #514: max closing-line rows returned. Default 10000 effectively returns all (preserves prior behavior). Lower for top-N quick views.","default":10000,"title":"Limit"},"description":"sleep_iter_15 #514: max closing-line rows returned. Default 10000 effectively returns all (preserves prior behavior). Lower for top-N quick views."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":0,"description":"Page offset within the result set. Combine with limit for pagination.","default":0,"title":"Offset"},"description":"Page offset within the result set. Combine with limit for pagination."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":5,"x-credit-cost-type":"fixed","x-credit-cost-description":"Pre-match closing odds for CLV analysis","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/futures":{"get":{"summary":"Get Futures","description":"Get futures/outrights odds (championship winners, MVP, etc). 5 credits.\n\nReturns long-term markets like championship winners, division winners,\nMVP awards, and season-long props.\n\n**Example:** `GET /v1/sports/icehockey_nhl/futures`","operationId":"get_futures_v1_sports__sport_key__futures_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bookmakers"},"style":"form","explode":false,"description":"Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once."},{"name":"oddsFormat","in":"query","required":false,"schema":{"type":"string","description":"american (default) | decimal. Selections get a matching `decimal` field alongside the existing `american` keys when decimal is requested.","default":"american","title":"Oddsformat"},"description":"american (default) | decimal. Selections get a matching `decimal` field alongside the existing `american` keys when decimal is requested."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":5,"x-credit-cost-type":"fixed","x-credit-cost-description":"Season-long futures markets","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/live":{"get":{"summary":"Get Live Odds","description":"In-play odds only. 3 credits.\n\nReturns events whose `commence_time` is within the last `max_age_hours`\nhours AND is at or before now (i.e. started and presumably still in play).\nPulls from the same `odds_snapshots` table that powers\n/v1/sports/{key}/odds. Game lines (h2h/spreads/totals) come from there;\n\"live\"-tagged player props that some books emit are merged in too. The\ndistinction from /odds is the time filter, not the data source.\n\nsleep_iter_6 #507 (pass-10 finding 6 from the public audit): the prior\nfilter was `commence_time <= now` with no lower bound, which returned\ngames that started 13+ hours ago and had been over for many hours.\nThe default 6-hour window now matches typical game lengths and the\n`max_age_hours` knob lets callers tune it for their sport.\n\n**Example:** `GET /v1/sports/baseball_mlb/live?bookmakers=pinnacle&markets=h2h&max_age_hours=4`","operationId":"get_live_odds_v1_sports__sport_key__live_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bookmakers"},"style":"form","explode":false,"description":"Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once."},{"name":"markets","in":"query","required":false,"schema":{"type":"string","description":"h2h, spreads, totals (comma-separated)","default":"h2h","title":"Markets"},"description":"h2h, spreads, totals (comma-separated) Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"regions","in":"query","required":false,"schema":{"type":"string","description":"us, eu, uk, au (comma-separated)","default":"us","title":"Regions"},"description":"us, eu, uk, au (comma-separated) Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"oddsFormat","in":"query","required":false,"schema":{"type":"string","default":"american","title":"Oddsformat"}},{"name":"include","in":"query","required":false,"schema":{"type":"string","description":"Comma-separated. 'verification' adds verified_at / line_changed_at / is_current per bookmaker.","default":"normalized","title":"Include"},"description":"Comma-separated. 'verification' adds verified_at / line_changed_at / is_current per bookmaker."},{"name":"max_age_hours","in":"query","required":false,"schema":{"type":"number","maximum":24.0,"minimum":0.5,"description":"Only include events whose commence_time is within the last N hours. Default 6 covers a typical MLB or NBA game plus extra innings / overtime. Raise to 12 for events with longer windows (long tennis matches, soccer with stoppage time), or lower to 3 for stricter in-play filtering.","default":6.0,"title":"Max Age Hours"},"description":"Only include events whose commence_time is within the last N hours. Default 6 covers a typical MLB or NBA game plus extra innings / overtime. Raise to 12 for events with longer windows (long tennis matches, soccer with stoppage time), or lower to 3 for stricter in-play filtering."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":3,"x-credit-cost-type":"fixed","x-credit-cost-description":"In-play odds for currently live games","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/compare":{"get":{"summary":"Compare Odds","description":"Compare odds across all bookmakers for each event. 5 credits.\n\nEXCLUSIVE. Returns every event with odds from all available bookmakers\nside-by-side, plus the best odds and hold percentage for each outcome.\n\n**Example:** `GET /v1/sports/americanfootball_nfl/compare?markets=h2h`\n\nFor each event, returns each bookmaker's odds + which book has the best line.","operationId":"compare_odds_v1_sports__sport_key__compare_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"markets","in":"query","required":false,"schema":{"type":"string","description":"Market type","default":"h2h","title":"Markets"},"description":"Market type Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"oddsFormat","in":"query","required":false,"schema":{"type":"string","default":"american","title":"Oddsformat"}},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"CSV of bookmaker keys to include. Omit for all books. iter_060 #466.","title":"Bookmakers"},"description":"CSV of bookmaker keys to include. Omit for all books. iter_060 #466. Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":5,"x-credit-cost-type":"fixed","x-credit-cost-description":"Cross-book price comparison + best-line","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/best-line":{"get":{"summary":"Best Line Alias","description":"Alias for `/v1/sports/{key}/compare`. Same response shape, same\ncredit cost (5 credits). Provided so customers migrating from the-odds-api\nwho followed our migration page can hit the URL it advertised.","operationId":"best_line_alias_v1_sports__sport_key__best_line_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"markets","in":"query","required":false,"schema":{"type":"string","default":"h2h","title":"Markets"},"style":"form","explode":false,"description":"Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once."},{"name":"oddsFormat","in":"query","required":false,"schema":{"type":"string","default":"american","title":"Oddsformat"}},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bookmakers"},"style":"form","explode":false,"description":"Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/odds/props":{"get":{"summary":"Odds Props Alias","description":"Alias for `/v1/sports/{key}/props`. Same response shape, same\ncredit cost (3 credits). Provided so customers migrating from the-odds-api\nwho followed our migration page can hit the URL it advertised.\n\nSame parameters as `/v1/sports/{key}/props`, including `maxAgeSec` and\n`include`.","operationId":"odds_props_alias_v1_sports__sport_key__odds_props_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"markets","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Markets"},"style":"form","explode":false,"description":"Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once."},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bookmakers"},"style":"form","explode":false,"description":"Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once."},{"name":"player","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Player"}},{"name":"eventId","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Eventid"}},{"name":"oddsFormat","in":"query","required":false,"schema":{"type":"string","default":"american","title":"Oddsformat"}},{"name":"dfsOdds","in":"query","required":false,"schema":{"type":"string","default":"midpoint","title":"Dfsodds"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":5000,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":0,"default":0,"title":"Offset"}},{"name":"grouped","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Grouped"}},{"name":"include_event_markets","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Event Markets"}},{"name":"maxAgeSec","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":3600,"minimum":1},{"type":"null"}],"title":"Maxagesec"}},{"name":"include","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Include"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Sports & Odds"]}},"/v1/inplay/arbs":{"get":{"summary":"Get Inplay Arbs","description":"Real-time in-play arbitrage opportunities. 5 credits.\n\nBackground scanner checks all live/upcoming games every 5 seconds for\ncross-book arbs. Returns the most recent flagged opportunities with\nbest_over@book_A, best_under@book_B, profit %, and optimal stake split.\n\nUpdates are also broadcast to /ws/live subscribers as\n`{\"type\": \"arb_flagged\", \"data\": [...]}`.\n\nParameter aliases (iter_042 #348): we accept `minProfit`,\n`min_profit`, and `min_profit_pct` interchangeably; same for\n`sport`, `sport_key`, and `sports`. First non-None wins. Earlier\ncallers using natural-language guesses got silent param drop;\nfixed.","operationId":"get_inplay_arbs_v1_inplay_arbs_get","parameters":[{"name":"minProfit","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Minimum profit % to include","title":"Minprofit"},"description":"Minimum profit % to include"},{"name":"min_profit","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Alias for minProfit (snake-case)","title":"Min Profit"},"description":"Alias for minProfit (snake-case)"},{"name":"min_profit_pct","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Alias for minProfit (alt snake-case)","title":"Min Profit Pct"},"description":"Alias for minProfit (alt snake-case)"},{"name":"sport","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by sport_key","title":"Sport"},"description":"Filter by sport_key"},{"name":"sport_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Alias for sport","title":"Sport Key"},"description":"Alias for sport"},{"name":"sports","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"CSV alias for sport (single value)","title":"Sports"},"description":"CSV alias for sport (single value)"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":5,"x-credit-cost-type":"fixed","x-credit-cost-description":"Live arbitrage across all sports","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Live & In-Play"]}},"/v1/parlay/price":{"post":{"summary":"Price Parlay","description":"Combine multiple legs into a parlay and return per-bookmaker\ncombined odds. iteration_022 #209: in a product called ParlayAPI,\ncallers had to client-side multiply decimal odds across N /odds\ncalls themselves; this endpoint does that work server-side.\n\nCost: 2 credits per leg (min 4).\n\nRequest body:\n```\n{\n \"legs\": [\n {\"sport_key\":\"basketball_nba\",\"event_id\":\"\",\n \"market\":\"h2h\",\"outcome\":\"Cleveland Cavaliers\"},\n {\"sport_key\":\"soccer_epl\",\"event_id\":\"\",\n \"market\":\"h2h\",\"outcome\":\"Tottenham Hotspur\"},\n {\"sport_key\":\"baseball_mlb\",\"event_id\":\"\",\n \"market\":\"totals\",\"outcome\":\"Over\",\"point\":9.0}\n ],\n \"bookmakers\": [\"fanduel\",\"draftkings\",\"betmgm\"]\n}\n```\nReturns one entry per bookmaker that prices ALL legs, sorted by\ncombined decimal odds DESCending (best to worst). Books missing\neven one leg appear in `missing_books` with which legs they couldn't\nprice. SGP flag set when 2+ legs share the same event_id; raw\nmultiplication is the LOWER bound for SGP (real books apply a\ncorrelation discount we don't replicate).","operationId":"price_parlay_v1_parlay_price_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"x-credit-cost-formula":"max(4, 2 x len(legs))","x-credit-cost-floor":4,"x-credit-cost-type":"variable","x-credit-cost-example":"3-leg parlay = max(4, 6) = 6 credits","x-credit-cost-description":"Multi-leg parlay pricing","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Calculators"]}},"/v1/clv":{"post":{"summary":"Grade Clv","description":"Closing-line-value scoring for a list of bets.\n\nAccepts a bet slip and returns per-bet CLV vs the closing line at a\nsharp book (default pinnacle, falls back to novig if pinnacle didn't\nprice the market). CLV is the +EV-volume bettor's primary validation\nmetric: positive CLV means you beat the closing line, which over a\nlarge sample size is the most reliable signal that your strategy is\nactually +EV regardless of individual-bet win/loss outcomes.\n\nCost: 2 credits per bet (min 5).\n\nRequest body:\n```\n{\n \"bets\": [\n {\n \"sport_key\": \"baseball_mlb\",\n \"game_date\": \"2026-05-12\",\n \"player\": \"Nico Hoerner\", // omit for game-line bets\n \"home_team\": \"Atlanta Braves\", // required for game-line bets\n \"away_team\": \"Chicago Cubs\",\n \"market\": \"player_runs\", // or h2h, spreads, totals\n \"outcome\": \"over\", // over/under for props/totals;\n // team name for h2h/spreads\n \"line\": 0.5, // null for h2h\n \"taken_odds\": -110, // your fill in American odds\n \"taken_at\": \"2026-05-12T18:00:00Z\" // optional\n }\n ],\n \"sharp_book\": \"pinnacle\" // optional; defaults to pinnacle\n}\n```\n\nResponse: per-bet `clv_cents`, `clv_pct`, no-vig adjusted CLV when both\nsides have a closing price, plus a `summary` with aggregate stats.","operationId":"grade_clv_v1_clv_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"x-credit-cost-formula":"max(5, 2 x len(bets))","x-credit-cost-floor":5,"x-credit-cost-type":"variable","x-credit-cost-example":"10-bet batch = max(5, 20) = 20 credits","x-credit-cost-description":"Closing-line-value batch grader","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Calculators"]}},"/v1/clv/history":{"post":{"summary":"Clv History","description":"Batch CLV history grader with date-range + period-market coverage.\n\nBody:\n```\n{\n \"bets\": [, ...], # same shape as /v1/clv\n \"date_from\": \"YYYY-MM-DD\", # optional, inclusive\n \"date_to\": \"YYYY-MM-DD\", # optional, inclusive\n \"sharp_book\": \"pinnacle\", # optional, default pinnacle\n \"include_period_archive\": false, # optional, default false\n \"period_key\": \"Q1\", # required when include_period_archive\n # AND any bet's market is a period market\n}\n```\n\nCost: max(15, 10 + 3 * len(bets)).\n\nGated by `CLV_HISTORY_ENABLED=1`. Default OFF; returns 503\nwith explanatory body when disabled.","operationId":"clv_history_v1_clv_history_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"x-credit-cost-formula":"max(15, 10 + 3 x len(bets))","x-credit-cost-floor":15,"x-credit-cost-type":"variable","x-credit-cost-example":"50-bet history grade = max(15, 160) = 160 credits","x-credit-cost-description":"Batch CLV grader with date-range + period-market coverage","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Calculators"]}},"/v1/sgp/price":{"post":{"summary":"Sgp Price","description":"Same-game parlay correlation pricer.\n\nBody: `{legs: [, ...]}`. Each leg: sport_key, market,\nside, book (always); player + line (player props);\nhome_team + away_team + game_date (always).\n\nResponse: per-leg marginals, independent baseline, literature-\ncorrelation-adjusted parlay implied, book payout multiplier,\nEV ratio. Unpriced legs land in `unpriced_legs`; the parlay\nestimate collapses to null when any leg is unpriced (we NEVER\nfabricate a missing marginal). Correlation audit names the\nliterature source used per pair.\n\nCost: 10 base + 5 credits per leg. Min 15.\n\nGated by `SGP_PRICER_ENABLED=1`. Default OFF; returns 503\nwith explanatory body when disabled.","operationId":"sgp_price_v1_sgp_price_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"x-credit-cost-formula":"max(15, 10 + 5 x len(legs))","x-credit-cost-floor":15,"x-credit-cost-type":"variable","x-credit-cost-example":"3-leg SGP = max(15, 25) = 25 credits","x-credit-cost-description":"Same-game parlay correlation pricer with literature-cited rho","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}]}},"/v1/sports/{sport_key}/arbitrage":{"get":{"summary":"Find Arbitrage","description":"Find arbitrage opportunities across bookmakers. 10 credits.\n\nEXCLUSIVE. Scans all events across all bookmakers and identifies games\nwhere the combined implied probability is less than 100%, meaning a\nguaranteed profit is possible by betting both sides at different books.\n\n**Example:** `GET /v1/sports/americanfootball_nfl/arbitrage?minProfit=1`\n\nReturns events with arb opportunities, the optimal bet split, and expected profit %.\n\n**Pagination headers** (sleep_iter_14): every response carries\n`x-result-page-size`, `x-result-limit`, `x-result-offset`,\n`x-result-has-more`, `x-result-total-available`, and (when has-more)\n`x-next-offset`. Body shape unchanged.","operationId":"find_arbitrage_v1_sports__sport_key__arbitrage_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"include_live","in":"query","required":false,"schema":{"type":"boolean","description":"Include games that have already started or finished. Off by default: these endpoints rank BETS, and a bet on a finished game is not actionable. Set true to see commenced games too.","default":false,"title":"Include Live"},"description":"Include games that have already started or finished. Off by default: these endpoints rank BETS, and a bet on a finished game is not actionable. Set true to see commenced games too."},{"name":"minProfit","in":"query","required":false,"schema":{"type":"number","description":"Minimum profit % to include (e.g. 1.5)","default":0,"title":"Minprofit"},"description":"Minimum profit % to include (e.g. 1.5)"},{"name":"exclude_exchanges","in":"query","required":false,"schema":{"type":"boolean","description":"Exclude arbs where either side is anchored on an exchange (novig, prophetx). Exchange asks can be no-volume 'shill' orders that aren't actually takeable. iter_062 #473. When false (default), arbs still surface with an `is_exchange_anchored` flag so callers can choose.","default":false,"title":"Exclude Exchanges"},"description":"Exclude arbs where either side is anchored on an exchange (novig, prophetx). Exchange asks can be no-volume 'shill' orders that aren't actually takeable. iter_062 #473. When false (default), arbs still surface with an `is_exchange_anchored` flag so callers can choose."},{"name":"exclude_books","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"CSV of book keys to exclude from EITHER side of every arb. iter_064 #481. Example: `exclude_books=prophetx,novig` to drop arbs anchored on exchanges. Cleaner than exclude_exchanges when you want to drop additional books.","title":"Exclude Books"},"description":"CSV of book keys to exclude from EITHER side of every arb. iter_064 #481. Example: `exclude_books=prophetx,novig` to drop arbs anchored on exchanges. Cleaner than exclude_exchanges when you want to drop additional books."},{"name":"markets","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"CSV of market_keys to limit the arb scan to. iter_064 #481. Example: `markets=h2h,spreads,totals` for game lines only; `markets=player_points` to focus on points props.","title":"Markets"},"description":"CSV of market_keys to limit the arb scan to. iter_064 #481. Example: `markets=h2h,spreads,totals` for game lines only; `markets=player_points` to focus on points props. Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"description":"sleep_iter_14 #513: max arb opportunities returned per call. Default 10000 effectively returns all (preserves prior behavior since typical query yields <500). Lower it (e.g. limit=20) for a top-N quick view.","default":10000,"title":"Limit"},"description":"sleep_iter_14 #513: max arb opportunities returned per call. Default 10000 effectively returns all (preserves prior behavior since typical query yields <500). Lower it (e.g. limit=20) for a top-N quick view."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":0,"description":"Page offset within the sorted (best profit first) arb list. Combine with limit for pagination.","default":0,"title":"Offset"},"description":"Page offset within the sorted (best profit first) arb list. Combine with limit for pagination."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":10,"x-credit-cost-type":"fixed","x-credit-cost-description":"Pre-match arbitrage finder","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/ev":{"get":{"summary":"Find Positive Ev","description":"Find +EV bets by comparing sharp vs soft book lines. 10 credits.\n\nEXCLUSIVE. Compares Pinnacle (or another sharp book) lines against\nsoft books (DraftKings, FanDuel, Caesars, Bovada). When a soft book's\nodds imply a lower probability than the sharp book, that's +EV.\n\n**Example:** `GET /v1/sports/americanfootball_nfl/ev?sharpBook=pinnacle&minEdge=3`\n\nA 5% edge means the soft book is offering odds that are 5% better than\nthe sharp book's assessment of true probability.\n\nParameter aliases (iter_056 #453): `min_edge_pct` is an alias for\n`minEdge` (snake-case). Filters honored:\n - `markets=` CSV market_key filter (e.g. player_points,player_assists)\n - `min_books=` minimum number of books in the comparison\n - `minEdge` / `min_edge_pct` minimum edge % to include\nEarlier these were silently dropped per iter_056 tester report.","operationId":"find_positive_ev_v1_sports__sport_key__ev_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"include_live","in":"query","required":false,"schema":{"type":"boolean","description":"Include games that have already started or finished. Off by default: these endpoints rank BETS, and a bet on a finished game is not actionable. Set true to see commenced games too.","default":false,"title":"Include Live"},"description":"Include games that have already started or finished. Off by default: these endpoints rank BETS, and a bet on a finished game is not actionable. Set true to see commenced games too."},{"name":"sharpBook","in":"query","required":false,"schema":{"type":"string","description":"Sharp book to use as true odds baseline","default":"pinnacle","title":"Sharpbook"},"description":"Sharp book to use as true odds baseline"},{"name":"minEdge","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Minimum edge % to include (default 2.0)","title":"Minedge"},"description":"Minimum edge % to include (default 2.0)"},{"name":"min_edge_pct","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Snake-case alias for minEdge","title":"Min Edge Pct"},"description":"Snake-case alias for minEdge"},{"name":"markets","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"CSV of market_keys to include (e.g. player_points,player_assists)","title":"Markets"},"description":"CSV of market_keys to include (e.g. player_points,player_assists) Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"min_books","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":50,"minimum":1},{"type":"null"}],"description":"Minimum books_compared per row","title":"Min Books"},"description":"Minimum books_compared per row"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"Max EV picks returned (default 200, max 500). sleep_iter_13 #512: prior hardcoded 200 cap is now caller-controllable. Combined with offset for pagination.","default":200,"title":"Limit"},"description":"Max EV picks returned (default 200, max 500). sleep_iter_13 #512: prior hardcoded 200 cap is now caller-controllable. Combined with offset for pagination."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":0,"description":"Page offset within the sorted EV pick list. Combine with limit for pagination.","default":0,"title":"Offset"},"description":"Page offset within the sorted EV pick list. Combine with limit for pagination."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":10,"x-credit-cost-type":"fixed","x-credit-cost-description":"Positive expected-value picks","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/sports/{sport_key}/consensus":{"get":{"summary":"Get Consensus","description":"Get consensus (average) odds across all bookmakers. 3 credits.\n\nReturns the average odds, best odds, worst odds, and hold/vig\nfor each market across all bookmakers. Useful for identifying\nwhere your book stands vs the market.\n\nThe average is taken in probability space: each book's American\nprice is converted to its implied probability, those are averaged\ninto `consensus_prob`, and `consensus_odds` is that probability\nconverted back. Averaging American prices directly is invalid\nbecause the scale jumps across zero, so +150 and -150 would\naverage to 0 rather than to even money.\n\niter_058 #462: prediction markets are excluded from the rollup by\ndefault. Kalshi was producing wildly off prices like +3233 for\n'Phillies win' that turned out to reference a different market\n(e.g. series winner, not next game ML) and skewed consensus_odds\nto nonsense values. Use `?include_prediction_markets=true` to\nrestore the prior behavior of mixing them in.","operationId":"get_consensus_v1_sports__sport_key__consensus_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"include_prediction_markets","in":"query","required":false,"schema":{"type":"boolean","description":"Include Kalshi/Polymarket prices in the consensus rollup. Defaults to False because prediction markets often price derivative questions (e.g. 'Phillies win series') that look like h2h ML but skew the average. Set true if you specifically want their prices mixed in.","default":false,"title":"Include Prediction Markets"},"description":"Include Kalshi/Polymarket prices in the consensus rollup. Defaults to False because prediction markets often price derivative questions (e.g. 'Phillies win series') that look like h2h ML but skew the average. Set true if you specifically want their prices mixed in."},{"name":"bookmakers","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"CSV of bookmaker keys to include in the rollup. Omit to include all books for the sport. iter_060 #465.","title":"Bookmakers"},"description":"CSV of bookmaker keys to include in the rollup. Omit to include all books for the sport. iter_060 #465. Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false},{"name":"markets","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"CSV of market_keys to filter to (e.g. h2h,player_points)","title":"Markets"},"description":"CSV of market_keys to filter to (e.g. h2h,player_points) Repeating this parameter is the same as the comma form: ?markets=h2h&markets=spreads and ?markets=h2h,spreads are one request, billed identically, and return the union. A value repeated across occurrences is counted once.","style":"form","explode":false}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":3,"x-credit-cost-type":"fixed","x-credit-cost-description":"Consensus / no-vig fair line","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}],"tags":["Sports & Odds"]}},"/v1/usage":{"get":{"summary":"Get Usage","description":"Check your API usage and remaining credits.\n\n/v1/account is the path the published MCP server calls (tool\nparlayapi_account_info, mcp-server/parlayapi_mcp/server.py:273).\nAliasing it to /v1/usage so the MCP tool doesn't 404 (#040).\n\nPass `?by_endpoint=true` to get a per-endpoint breakdown of\ncredit usage in the current period (iter_060 #469).","operationId":"get_usage_v1_usage_get","parameters":[{"name":"by_endpoint","in":"query","required":false,"schema":{"type":"boolean","description":"Include a per-endpoint credit-usage breakdown for the current billing period. iter_060 #469: customers asked for 'where did my credits go this month'. Off by default so the response stays lean.","default":false,"title":"By Endpoint"},"description":"Include a per-endpoint credit-usage breakdown for the current billing period. iter_060 #469: customers asked for 'where did my credits go this month'. Off by default so the response stays lean."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/account":{"get":{"summary":"Get Usage","description":"Check your API usage and remaining credits.\n\n/v1/account is the path the published MCP server calls (tool\nparlayapi_account_info, mcp-server/parlayapi_mcp/server.py:273).\nAliasing it to /v1/usage so the MCP tool doesn't 404 (#040).\n\nPass `?by_endpoint=true` to get a per-endpoint breakdown of\ncredit usage in the current period (iter_060 #469).","operationId":"get_usage_v1_account_get","parameters":[{"name":"by_endpoint","in":"query","required":false,"schema":{"type":"boolean","description":"Include a per-endpoint credit-usage breakdown for the current billing period. iter_060 #469: customers asked for 'where did my credits go this month'. Off by default so the response stays lean.","default":false,"title":"By Endpoint"},"description":"Include a per-endpoint credit-usage breakdown for the current billing period. iter_060 #469: customers asked for 'where did my credits go this month'. Off by default so the response stays lean."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/stats":{"get":{"summary":"Get Stats","description":"Public endpoint - data availability stats.\n\nCached 5 minutes. The underlying query takes ~10s (DISTINCT source\n+ MIN/MAX game_date over 33M rows) and the answer changes slowly\nenough that every poll re-running it is wasteful. Customers\nsurveying capabilities don't need second-resolution freshness on\na \"what bookmakers do you cover\" answer.\n\nStale-while-revalidate semantics: if the cache is fresh (under 5\nmin), return it. If it's expired but still present, return the\nstale copy AND spawn a background refresh so the next request hits\na fresh value. If it's never been populated (first call on a cold\nworker), do the full query inline - the warmup loop catches up\nwithin ~13 s of spawn so this only affects requests in that\nnarrow window. iter_031/033 #327/#329 HIGH timeouts.","operationId":"get_stats_v1_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/pricing":{"get":{"summary":"Pricing","description":"Pricing comparison for browsers and JSON clients.","operationId":"pricing_pricing_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/exchange/{sport_key}/markets":{"get":{"summary":"Get Exchange Markets","description":"Get exchange/prediction market data with liquidity depth.\n\n**EXCLUSIVE** - No competitor offers this.\n\nReturns real-time order book data from betting exchanges including:\n- Best available prices (bid/ask)\n- Last traded prices\n- Market volume (liquidity in USD)\n- Player props with exchange pricing\n\n**Exchanges:** Novig (peer-to-peer sports exchange)\n\n**Example:** `GET /v1/exchange/americanfootball_nfl/markets?min_volume=100`\n\nCredits: 3 per request.","operationId":"get_exchange_markets_v1_exchange__sport_key__markets_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}},{"name":"exchange","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by exchange: novig, kalshi","title":"Exchange"},"description":"Filter by exchange: novig, kalshi"},{"name":"market_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter: MONEY, SPREAD, TOTAL, PLAYER_GOALS, etc.","title":"Market Type"},"description":"Filter: MONEY, SPREAD, TOTAL, PLAYER_GOALS, etc."},{"name":"min_volume","in":"query","required":false,"schema":{"type":"number","description":"Minimum volume in USD","default":0,"title":"Min Volume"},"description":"Minimum volume in USD"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-credit-cost":3,"x-credit-cost-type":"fixed","x-credit-cost-description":"Exchange order book data (Novig, ProphetX)","security":[{"apiKeyHeader":[]},{"apiKeyQuery":[]},{"bearerAuth":[]}]}},"/v1/exchanges":{"get":{"summary":"List Exchanges","description":"List available betting exchanges. FREE - no auth needed.\n\nFiltered through is_served(), the SAME predicate the data paths use.\nAdvertising an exchange we refuse to serve is worse than omitting it: a\ncustomer picks a book from this catalogue, and until 2026-07-31 it named\nProphetX while /v1/exchange returned its SANDBOX prices, so the customer\nhad no reason to filter them out.","operationId":"list_exchanges_v1_exchanges_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/collections/postman.json":{"get":{"summary":"Postman Collection","description":"Postman 2.1 collection auto-generated from the live OpenAPI spec.\n\nDrop into Postman / Insomnia / Bruno via Import -> URL:\n https://parlay-api.com/collections/postman.json\n\nGenerated lazily on first request and cached in-process by the\nOpenAPI spec digest, so a deploy that updates the spec produces a\nfresh collection on the next request. Public, no auth.","operationId":"postman_collection_collections_postman_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/try/sgp/price":{"post":{"summary":"Try Sgp Price","description":"**Free no-auth SGP independent-baseline pricer.** Takes a list\nof legs with American prices, returns the independent-baseline\nparlay price (product of marginal implied probs converted back to\nAmerican). Capped at 60 requests/hour per IP and at 4 legs.\n\nThis is *not* a real SGP price — real SGP prices include\ncorrelation modeling (correlated outcomes from the same game push\nthe parlay price away from the independent product in either\ndirection). Use the full /v1/sgp/price for literature-based\ncorrelation-adjusted pricing.\n\nBody: `{\"legs\": [{\"price\": -110}, {\"price\": +150}, ...]}`.","operationId":"try_sgp_price_v1_try_sgp_price_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/meta/movers":{"get":{"summary":"Meta Movers","description":"Live biggest market movers. Returns events whose moneyline\nhas moved the most over the requested window, ranked by absolute\ndelta. Computed from the odds_snapshots first-vs-last comparison\non the canonical moneyline (home_ml, away_ml).\n\nPublic, no auth, no credits. 90-second server-side cache.\n\nResponse shape:\n {\n \"as_of\": \"...\",\n \"window_minutes\": N,\n \"sport_key\": \"...\",\n \"movers\": [\n {\n \"sport_key\": \"...\",\n \"home_team\": \"...\", \"away_team\": \"...\",\n \"commence_time\": \"...\",\n \"home_ml_first\": -110, \"home_ml_last\": -135,\n \"home_ml_delta\": -25,\n \"away_ml_first\": -110, \"away_ml_last\": +110,\n \"away_ml_delta\": +20,\n \"abs_max_delta\": 25,\n \"snapshots_seen\": 47,\n \"source\": \"pinnacle\"\n },\n ...\n ]\n }\n\nUse `source=pinnacle` semantics: this is the sharp anchor's view\nof how the line moved. To get movers from a soft book's view,\ncustomer can run their own join against /v1/sports/{sport}/odds\nhistory. For most users, sharp-anchor movement is the more\ninteresting signal.","operationId":"meta_movers_v1_meta_movers_get","parameters":[{"name":"sport_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional sport filter. Defaults to all sports.","title":"Sport Key"},"description":"Optional sport filter. Defaults to all sports."},{"name":"window_minutes","in":"query","required":false,"schema":{"type":"integer","maximum":360,"minimum":5,"description":"Lookback window in minutes (5 to 360, default 60).","default":60,"title":"Window Minutes"},"description":"Lookback window in minutes (5 to 360, default 60)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50,"minimum":1,"description":"Top N movers to return (default 15).","default":15,"title":"Limit"},"description":"Top N movers to return (default 15)."},{"name":"pre_game_only","in":"query","required":false,"schema":{"type":"boolean","description":"If true (default), only return events whose commence_time is still in the future. Filters out the in-play price-collapse cases that otherwise dominate (tennis match in progress, late-game hockey, etc). Pass false to include live-game moves.","default":true,"title":"Pre Game Only"},"description":"If true (default), only return events whose commence_time is still in the future. Filters out the in-play price-collapse cases that otherwise dominate (tennis match in progress, late-game hockey, etc). Pass false to include live-game moves."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/meta/limits":{"get":{"summary":"Meta Limits","description":"Per-tier rate-limit + credit-limit reference. Machine-readable\nJSON; companion to the human-readable /limits HTML.\n\nPublic, no auth. The values returned here are the canonical\noperator-set limits and match what's enforced in production.","operationId":"meta_limits_v1_meta_limits_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Metadata"]}},"/v1/key/successor":{"get":{"summary":"Key Successor","description":"Collect the replacement for the key you are calling with.\n\nWhen we rotate a key (because it was exposed, or on request), the old key\nkeeps working for a grace window and a successor is minted immediately.\nThis endpoint hands that successor to whoever holds the current key, once,\nso a client can swap itself without a human copying a string out of an\nemail:\n\n r = requests.get(BASE + \"/v1/key/successor\",\n headers={\"X-API-Key\": current_key})\n if r.status_code == 200:\n save(r.json()[\"new_key\"]) # use it from the next call onward\n\nThe secret is wiped on collection, so it exists at rest only between\nrotation and pickup. Free: rotating a credential should never cost credits.\n\n200 with the new key, or 404 when there is nothing to collect (the normal\ncase, so polling this is harmless).","operationId":"key_successor_v1_key_successor_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/meta/api-key-check":{"get":{"summary":"Meta Api Key Check","description":"Validate an API key without consuming credits. Useful for\nCI/CD pre-flight, signup-flow validation, and SDK boot-time\nsanity checks.\n\nPass the key via the same channels as any authenticated endpoint:\n`X-API-Key` header, `Authorization: Bearer `, or\n`?apiKey=...` query param.\n\nNo credit cost. Returns the tier, active state, credit headroom,\nand a `valid` boolean. Distinguishes 4 outcomes:\n - 200 `{\"valid\": true, ...}` for a healthy key\n - 200 `{\"valid\": false, \"reason\": \"...\"}` for an invalid key\n (this lets CI scripts treat invalid-key as a soft failure\n without parsing 401 envelopes)\n - 200 `{\"valid\": false, \"reason\": \"credit_exhausted\", ...}`\n for a key that is structurally valid but has burned its\n monthly allowance\n - 200 `{\"valid\": false, \"reason\": \"key_inactive\", ...}` for\n a key that has been deactivated by the owner","operationId":"meta_api_key_check_v1_meta_api_key_check_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Metadata"]}},"/v1/meta/usage":{"get":{"summary":"Meta Usage","description":"Caller's current-period usage summary. Returns credits used\nthis month, credits remaining, daily breakdown for the requested\nhistory window, and top endpoints by credit consumption.\n\nPublic auth: any valid key works. Costs 0 credits to read your\nown usage. Pair with /v1/meta/api-key-check for the pre-flight\ncase.","operationId":"meta_usage_v1_meta_usage_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":90,"minimum":1,"description":"Days of usage history to summarize (default 7, max 90).","default":7,"title":"Days"},"description":"Days of usage history to summarize (default 7, max 90)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/meta/diagnostic":{"get":{"summary":"Meta Diagnostic","description":"Customer-facing connectivity diagnostic. Returns everything the\ncaller needs to debug \"why can't I reach the API.\"\n\nPublic, no auth, no credits. The endpoint is intentionally simple\nso a misconfigured firewall / ISP filter / corporate proxy can be\ndistinguished from a real outage. Pair with /diag (HTML) which\nruns every check from the browser and renders results.\n\nResponse shape:\n {\n \"as_of\": \"...\", // server-side time\n \"as_of_unix\": 1234567890,\n \"client_ip\": \"...\",\n \"client_country_hint\": \"...\", // IP geo if available\n \"client_asn_hint\": \"...\", // ASN if available\n \"request_received_via\": {\n \"scheme\": \"https\",\n \"host\": \"parlay-api.com\",\n \"user_agent\": \"...\"\n },\n \"expected_signature\": \"PARLAY-OK-2026\", // string the\n // browser can grep for\n \"tls_fingerprint\": null, // not yet\n \"headers_we_set_back\": {...}, // CORS, CSP, etc.\n \"tests\": [\n {\"name\":\"server_time\", \"ok\":true, \"note\":\"...\"},\n {\"name\":\"tls_active\", \"ok\":true, \"note\":\"...\"},\n {\"name\":\"cors\", \"ok\":true, \"note\":\"...\"},\n ...\n ]\n }\n\nIf a customer can hit this endpoint at all, their network can reach\nus. If they cannot (DNS NXDOMAIN, ISP filter HTML, timeout, etc.)\nthey know it's NOT us and they need to look at their network path.","operationId":"meta_diagnostic_v1_meta_diagnostic_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Metadata"]}},"/v1/meta/status-history.json":{"get":{"summary":"Meta Status History Json","description":"Machine-readable incident / status history. JSON companion to\n/status/history HTML and the existing /v1/meta/incidents endpoint\n(which only surfaces structured incidents we explicitly logged).\n\nPublic, no auth. 5-min server-side cache.\n\nResponse shape:\n {\n \"as_of\": \"...\",\n \"window_days\": N,\n \"incident_count\": N,\n \"incidents\": [\n {\"start\": \"...\", \"end\": \"...\", \"status\": \"resolved\",\n \"severity\": \"minor|major|critical\",\n \"title\": \"...\",\n \"summary\": \"...\",\n \"components_affected\": [...]},\n ...\n ],\n \"uptime_pct_window\": 99.95\n }","operationId":"meta_status_history_json_v1_meta_status_history_json_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":180,"minimum":1,"description":"Days of history to return (default 30, max 180).","default":30,"title":"Days"},"description":"Days of history to return (default 30, max 180)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/meta/book-catalog":{"get":{"summary":"Meta Book Catalog","description":"Full registered-book catalog with class labels and ingest status.\n\nSister to /v1/meta/source-stack: where source-stack is curated and\ndeep (full integration metadata for each book we've genuinely\nwired live), this endpoint is breadth-oriented and surfaces every\nbook in the classification registry alongside a `live` flag\nindicating whether it's currently producing rows.\n\nUse cases:\n - Customer auditing \"do you cover sportsbook X?\" with one HTTP\n GET instead of having to read the source-stack and figure out\n what's missing\n - AI agents enumerating coverage for a comparison page\n - Operators identifying which scaffolded modules haven't yet\n been promoted to live ingest\n\nResponse shape:\n {\n \"as_of_iso\": \"...\",\n \"aggregate\": {\n \"books_total\": N, // every key in classification registry\n \"books_live\": N, // subset currently producing rows\n \"books_scaffolded_not_live\": N,\n \"by_class\": {\"us_retail\": N, \"uk\": N, ...},\n \"by_class_with_live\": {\n \"us_retail\": {\"total\": N, \"live\": N}, ...\n }\n },\n \"books\": [\n {\n \"key\": \"fanduel\",\n \"class\": \"us_retail\",\n \"title\": \"FanDuel\", // display name from BOOKMAKER_TITLES\n \"live\": true, // saw rows in last 24h\n \"rows_24h\": N, // 0 if not live\n \"variants\": [...], // feeds folded in; absent if just one\n \"variant_of\": \"unibet\" // absent unless this key IS a variant\n },\n ...\n ]\n }\n\nrows_24h is credited to the OPERATOR: a book's country-licensed feeds\n(unibet_se, unibet_ro, ...) count toward the book /odds serves, and\n`variants` names them. Two Unibet variants are registered in their own\nright and keep their own rows; they carry `variant_of` so counting\noperators means counting `variant_of or key`.\n\nPublic, no auth, no credits. 5-minute server-side cache so\npolling is cheap; live counts refresh on a 5-min cadence.","operationId":"meta_book_catalog_v1_meta_book_catalog_get","parameters":[{"name":"class","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to a single class (e.g. us_retail, uk, br, exchange). Omit for all classes.","title":"Class"},"description":"Filter to a single class (e.g. us_retail, uk, br, exchange). Omit for all classes."},{"name":"live","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"true: only books that produced rows in past 24h. false: only scaffolded-not-live. Omit for all.","title":"Live"},"description":"true: only books that produced rows in past 24h. false: only scaffolded-not-live. Omit for all."},{"name":"region","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter to books with this region tag (e.g. US-NJ, BR, ES, FR, DE, AU). Matches against the regions list of each entry in /v1/meta/source-stack (when available).","title":"Region"},"description":"Filter to books with this region tag (e.g. US-NJ, BR, ES, FR, DE, AU). Matches against the regions list of each entry in /v1/meta/source-stack (when available)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/meta/event-search":{"get":{"summary":"Meta Event Search","description":"Server-side event search by team name. Useful for \"find me all\nupcoming NBA games featuring the Lakers\" or \"what's the next\nPinnacle-priced Yankees game.\"\n\nPublic, no auth, no credits. 60-second server-side cache. Searches\nforward-looking events only; for historical lookups use\n/v1/historical/closing-lines.json.\n\nThe match is case-insensitive substring against `home_team` and\n`away_team`. Multi-word queries are split on whitespace and require\nall tokens to be present somewhere in the matchup.\n\nResponse shape:\n {\n \"as_of\": \"...\",\n \"query\": \"lakers\",\n \"sport_key\": null,\n \"hours_ahead\": 168,\n \"result_count\": N,\n \"results\": [\n {\"sport_key\":\"...\",\"commence_time\":\"...\",\n \"home_team\":\"...\",\"away_team\":\"...\",\n \"books_available\": N,\n \"earliest_observation\": \"...\"},\n ...\n ]\n }","operationId":"meta_event_search_v1_meta_event_search_get","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":80,"description":"Search query. Matches against home_team, away_team, or both.","title":"Q"},"description":"Search query. Matches against home_team, away_team, or both."},{"name":"sport_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional sport filter (e.g. baseball_mlb).","title":"Sport Key"},"description":"Optional sport filter (e.g. baseball_mlb)."},{"name":"hours_ahead","in":"query","required":false,"schema":{"type":"integer","maximum":720,"minimum":1,"description":"Only return events with commence_time within the next N hours (default 168=7 days).","default":168,"title":"Hours Ahead"},"description":"Only return events with commence_time within the next N hours (default 168=7 days)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"Max results to return (default 20).","default":20,"title":"Limit"},"description":"Max results to return (default 20)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/meta/markets":{"get":{"summary":"Meta Markets","description":"List of every market_key the API can return, grouped by family.\n\nPublic, no auth, no credits. Static catalog plus dynamic per-source\ncoverage hints from the parser-coverage matrix (use\n/v1/meta/parser-coverage for the live \"which books expose which\nmarkets\" answer).","operationId":"meta_markets_v1_meta_markets_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Metadata"]}},"/v1/meta/regions":{"get":{"summary":"Meta Regions","description":"Region codes accepted by the `regions=` query param, which books in\neach one are actually writing prices, and the exact book set `regions=`\nnarrows /odds to.\n\n`active_books` is the coverage answer: a book is active in a region only\nif the region declares it, it is neither suppressed nor retired, it is in\nthe registry /v1/bookmakers publishes, and it has written rows in the\nlast 24 hours. `books` / `odds_filter_books` remain the /odds filter,\nwhich is a different and larger set.\n\nPublic, no auth, no credits. Pair with /v1/meta/markets (canonical\nmarket keys), /v1/meta/parser-coverage (live per-book per-sport\ncoverage matrix), and /v1/meta/per-book-sla (freshness thresholds).","operationId":"meta_regions_v1_meta_regions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"tags":["Metadata"]}},"/badge/{kind}.svg":{"get":{"summary":"Status Badge Svg","description":"Embeddable SVG status badges. shields.io-compatible shape; can\nbe dropped into a README or status page with a plain img tag.\n\nSupported `kind` values:\n - status: live worst SLA across sources (ok / degraded / breach / stale)\n - sources: count of live sources in the last 10 min\n - books: count of bookmakers we ingest\n - sports: count of sport_keys we cover\n\nPublic, no auth, cached 60s on the server.","operationId":"status_badge_svg_badge__kind__svg_get","parameters":[{"name":"kind","in":"path","required":true,"schema":{"type":"string","title":"Kind"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/try/{sport_key}/odds":{"get":{"summary":"Try Odds","description":"**Free no-auth demo.** Live moneyline odds for a popular US sport,\nno API key required. Capped at 60 requests/hour per IP and the\nfirst 5 events.\n\nAvailable sports: baseball_mlb, basketball_nba, americanfootball_nfl,\nicehockey_nhl, soccer_epl, mma_mixed_martial_arts.\n\nFor full access (every sport key, all 32 books, all markets, no\nrate limit beyond your tier): https://parlay-api.com/signup","operationId":"try_odds_v1_try__sport_key__odds_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/try/{sport_key}/arbitrage":{"get":{"summary":"Try Arbitrage","description":"**Free no-auth arbitrage demo.** Pre-computed cross-book arbitrage\nopportunities for a popular US sport, no API key required. Capped\nat 30 requests/hour per IP and the top 5 opportunities by edge\npercent.\n\nAvailable sports: baseball_mlb, basketball_nba, americanfootball_nfl,\nicehockey_nhl, soccer_epl, mma_mixed_martial_arts.\n\nFor full access (all sports, no result cap, no rate limit beyond\nyour tier): https://parlay-api.com/signup","operationId":"try_arbitrage_v1_try__sport_key__arbitrage_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/try/{sport_key}/ev":{"get":{"summary":"Try Ev","description":"**Free no-auth positive-EV demo.** Surfaces the top moneyline\npositive-EV opportunities for a popular US sport, no API key\nrequired. Capped at 60 requests/hour per IP and the top 5\nopportunities by edge percent.\n\nHow edge is computed: Pinnacle (sharp book) two-sided ML price is\ndevigged to a no-vig fair probability per side. For every other\nbook's offered price on the same side, edge = fair_prob -\nbook_implied_prob. Anything above 0 is a \"+EV\" play vs Pinnacle as\nanchor.\n\nAvailable sports: baseball_mlb, basketball_nba, americanfootball_nfl,\nicehockey_nhl, soccer_epl, mma_mixed_martial_arts.\n\nFor full access (all sports, all markets, spreads/totals/props, no\ncap): https://parlay-api.com/signup","operationId":"try_ev_v1_try__sport_key__ev_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/try/{sport_key}/middles":{"get":{"summary":"Try Middles","description":"**Free no-auth middling demo.** Surfaces the widest cross-book\ntotals middle windows for a popular US sport, no API key required.\nCapped at 60 requests/hour per IP and the top 5 opportunities by\nmiddle window width.\n\nA \"middle\" on totals: bet Over X.5 at one book and Under Y.5 at\nanother, where X < Y. If the final game total lands in (X, Y) you\nwin BOTH bets. We rank by window width (the integer count of\npossible winning totals).\n\nAvailable sports: baseball_mlb, basketball_nba, americanfootball_nfl,\nicehockey_nhl, soccer_epl, mma_mixed_martial_arts.\n\nFor spreads middles, all markets, all sports, no cap:\nhttps://parlay-api.com/signup","operationId":"try_middles_v1_try__sport_key__middles_get","parameters":[{"name":"sport_key","in":"path","required":true,"schema":{"type":"string","title":"Sport Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Root","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}},"head":{"summary":"Root","operationId":"root__head","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/changelog.json":{"get":{"summary":"Changelog Json","description":"Machine-readable changelog. JSON variant of /changelog (HTML)\nand /changelog.rss. Same content, parsed from static/changelog.html\nso all three views stay in sync.\n\nPublic, no auth, no credits. 1h server cache.\n\nResponse shape:\n {\n \"as_of\": \"...\",\n \"entries\": [\n {\"title\": \"...\", \"date\": \"YYYY-MM-DD\", \"body_text\": \"...\"},\n ...\n ]\n }","operationId":"changelog_json_changelog_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/meta/changelog":{"get":{"summary":"Meta Changelog Json","description":"Machine-readable changelog. Public, no auth, no credits.\n\nReturns an ordered list of changelog entries (newest first). Each\nentry has `date` (ISO 8601 yyyy-mm-dd), `title`, `tags` (list of\nshort strings), `summary` (first paragraph as plain text), and\n`url` (anchor into the human-readable HTML page).\n\nUse cases:\n- SDK release-notes banner: poll once per deploy, show new entries\n since last seen `worker_started_at`.\n- Customer dashboard \"what's new\" widget.\n- AI agent summarizing platform changes in conversation.\n- Slack / Discord bot in your team channel mirroring updates.\n\nThe HTML page at /changelog and RSS feed at /changelog.rss share\nthis same source-of-truth (static/changelog.html), so all three\nsurfaces are guaranteed in sync.","operationId":"meta_changelog_json_v1_meta_changelog_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/meta/incidents":{"get":{"summary":"Incidents Json","description":"Machine-readable incident history. Public, no auth, no credits.\n\nReturns an ordered list of platform incidents (most recent first)\nwith start / resolved timestamps, impact summary, root cause,\nresolution, affected endpoints. Customers ingest this for:\n\n- Vendor reliability scoring matrix.\n- SLA-compliance verification (the times we missed our targets).\n- Status-page rollups (some customers render their own).\n- AI agent answering \"has ParlayAPI been reliable recently?\"\n\nEmpty / short record is itself a meaningful signal — the absence\nof incidents over a long window indicates a stable platform.\n\nSource-of-truth is src/api/static/incidents.json (human-edited,\ngit-versioned). Each incident has a stable `id` so customers\ncan dedupe across polls.\n\nStability promise: `id`, `title`, `status`, `severity`,\n`started_at`, `resolved_at`, `impact` keys will not be removed\nor renamed without bumping `version`.","operationId":"incidents_json_v1_meta_incidents_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by status: 'resolved' (closed), 'investigating', 'identified', 'monitoring', 'open' (all non-resolved). Omit for all.","title":"Status"},"description":"Filter by status: 'resolved' (closed), 'investigating', 'identified', 'monitoring', 'open' (all non-resolved). Omit for all."},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO 8601 date (YYYY-MM-DD) to filter incidents started on or after this date.","title":"Since"},"description":"ISO 8601 date (YYYY-MM-DD) to filter incidents started on or after this date."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/meta/uptime":{"get":{"summary":"Uptime Stats","description":"Per-worker request counters since worker boot. Public, no\nauth, no credits.\n\nReturns:\n - total_requests: every request the worker has served\n - by_status_class: {2xx, 3xx, 4xx, 5xx}\n - error_rate_pct: 5xx / total * 100 (server-error rate)\n - success_rate_pct: 2xx / total * 100\n - server_error_first_at / server_error_last_at: timestamps of\n first/last 5xx (null if none)\n - worker_started_at + worker_uptime_seconds (matches api-info)\n\nReset on every rolling reload (per-worker, in-process). The 5-\nworker pool means a customer polling sees one worker's stats per\npoll. Over time the round-robin load balancer surfaces a\nrepresentative sample.\n\nFor a cross-worker aggregate uptime % a future iter could push\nthese counters to Redis on a periodic flush. For now the per-\nworker view is the meaningful signal: \"is this worker healthy?\"\n\nPairs with /v1/meta/sla (targets) + /v1/meta/incidents (record\nof misses). All three discoverable through /v1/meta/api-info.\n\nStability: this is /v1/meta/uptime version 0.1. Keys may evolve.","operationId":"uptime_stats_v1_meta_uptime_get","parameters":[{"name":"by_endpoint","in":"query","required":false,"schema":{"type":"boolean","description":"If true, include per-endpoint breakdown (templated paths only, capped at 200).","default":false,"title":"By Endpoint"},"description":"If true, include per-endpoint breakdown (templated paths only, capped at 200)."},{"name":"sort_by","in":"query","required":false,"schema":{"type":"string","description":"When by_endpoint=true: sort by '5xx' (default; descending), 'total' (descending), or 'path' (alphabetical).","default":"5xx","title":"Sort By"},"description":"When by_endpoint=true: sort by '5xx' (default; descending), 'total' (descending), or 'path' (alphabetical)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"description":"When by_endpoint=true: cap on number of endpoints in response.","default":50,"title":"Limit"},"description":"When by_endpoint=true: cap on number of endpoints in response."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"tags":["Metadata"]}},"/v1/metrics":{"get":{"summary":"Prometheus Metrics","description":"Prometheus exposition format metrics. Public, no auth, no\ncredits. text/plain content-type.\n\nIndustry-standard scrape target: customers' Prometheus,\nGrafana Agent, Datadog OpenMetrics integration, or any\nOpenMetrics-compatible scraper polls this endpoint every\n15-60 seconds. The metrics include:\n\n- parlayapi_requests_total{status_class=\"2xx|3xx|4xx|5xx\"}\n- parlayapi_endpoint_requests_total{path, status_class}\n- parlayapi_endpoint_request_duration_seconds{path, status_class}\n- parlayapi_worker_uptime_seconds\n- parlayapi_api_version (gauge with version_string label)\n- parlayapi_postgres_deadlocks_total (always 0 since iter_8)\n- parlayapi_idempotency_keys_active (currently 0 if Redis miss)\n\nAll metrics carry a `worker` label so customers' Prometheus\ndeduplicates correctly across the 5-worker pool. Customers\naggregate across workers via `sum by(status_class)` queries.\n\nCache-Control: no-cache (always fresh). The body is small\n(~2 KB) so the cost is trivial.\n\nsleep_iter_40 #539.","operationId":"prometheus_metrics_v1_metrics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api":{"get":{"summary":"Api Root","operationId":"api_root_api_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/keys":{"get":{"summary":"List Api Keys","description":"List the API keys associated with the authenticated user.\n\niter_066 #487: GET was previously 405. Customers had no way to\ninventory their own keys; rotation forced a DELETE + recreate\neven when the user just wanted to verify a key existed.\n\nReturns the calling key (always) plus any sibling keys on the same\nuser account. Key strings themselves are TRUNCATED — only the first\n8 chars + ... + last 4 chars are returned (Stripe-style fingerprint)\nso leaked logs can't reconstruct working credentials. To actually\nuse a key the customer must remember the one they got at creation\ntime; we don't store an un-truncated copy retrievable by ID.","operationId":"list_api_keys_v1_keys_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Create Api Key","description":"Create a free API key. No credit card required.","operationId":"create_api_key_v1_keys_post","parameters":[{"name":"email","in":"query","required":true,"schema":{"type":"string","title":"Email"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AlertCreate":{"properties":{"sport_key":{"type":"string","title":"Sport Key"},"player_name":{"type":"string","title":"Player Name"},"market_key":{"type":"string","title":"Market Key"},"threshold_line":{"type":"number","title":"Threshold Line"},"webhook_id":{"type":"integer","title":"Webhook Id"},"book":{"type":"string","title":"Book","default":"any"},"direction":{"type":"string","title":"Direction","default":"any_cross"},"match_mode":{"type":"string","title":"Match Mode","default":"any_book"},"cooldown_seconds":{"type":"integer","title":"Cooldown Seconds","default":300},"expires_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expires At"},"event_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Event Id"}},"type":"object","required":["sport_key","player_name","market_key","threshold_line","webhook_id"],"title":"AlertCreate"},"AlertPatch":{"properties":{"enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Enabled"},"cooldown_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Cooldown Seconds"},"threshold_line":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Threshold Line"},"direction":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Direction"},"expires_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expires At"}},"type":"object","title":"AlertPatch"},"Body_admin_consolidate_subscriptions_dashboard_admin_subscriptions_consolidate_post":{"properties":{"email":{"type":"string","title":"Email"},"keep_sub_id":{"type":"string","title":"Keep Sub Id"}},"type":"object","required":["email","keep_sub_id"],"title":"Body_admin_consolidate_subscriptions_dashboard_admin_subscriptions_consolidate_post"},"Body_affiliates_update_dashboard_admin_affiliates_update_post":{"properties":{"id":{"type":"integer","title":"Id"},"affiliate_url":{"type":"string","title":"Affiliate Url","default":""},"status":{"type":"string","title":"Status","default":""},"bonus_text":{"type":"string","title":"Bonus Text","default":""}},"type":"object","required":["id"],"title":"Body_affiliates_update_dashboard_admin_affiliates_update_post"},"Body_forgot_password_submit_forgot_password_post":{"properties":{"email":{"type":"string","title":"Email"}},"type":"object","required":["email"],"title":"Body_forgot_password_submit_forgot_password_post"},"Body_grant_credits_dashboard_admin_grant_credits_post":{"properties":{"user_id":{"type":"integer","title":"User Id"},"credits":{"type":"integer","title":"Credits"},"reason":{"type":"string","title":"Reason","default":"admin grant via Users tab"}},"type":"object","required":["user_id","credits"],"title":"Body_grant_credits_dashboard_admin_grant_credits_post"},"Body_grant_trial_dashboard_admin_grant_trial_post":{"properties":{"user_id":{"type":"integer","title":"User Id"},"trial_tier":{"type":"string","title":"Trial Tier"},"duration_hours":{"type":"number","title":"Duration Hours","default":6.0},"lazy_start":{"type":"boolean","title":"Lazy Start","default":true}},"type":"object","required":["user_id","trial_tier"],"title":"Body_grant_trial_dashboard_admin_grant_trial_post"},"Body_import_bet_screenshot_v1_clv_import_screenshot_post":{"properties":{"image":{"anyOf":[{"type":"string","contentMediaType":"application/octet-stream"},{"type":"null"}],"title":"Image"}},"type":"object","title":"Body_import_bet_screenshot_v1_clv_import_screenshot_post"},"Body_login_submit_login_post":{"properties":{"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"next":{"type":"string","title":"Next","default":"/dashboard"}},"type":"object","required":["email","password"],"title":"Body_login_submit_login_post"},"Body_reset_password_submit_reset_password_post":{"properties":{"token":{"type":"string","title":"Token"},"password":{"type":"string","title":"Password"}},"type":"object","required":["token","password"],"title":"Body_reset_password_submit_reset_password_post"},"Body_signup_submit_signup_post":{"properties":{"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"next":{"type":"string","title":"Next","default":""},"attribution":{"type":"string","title":"Attribution","default":""}},"type":"object","required":["email","password"],"title":"Body_signup_submit_signup_post"},"Body_support_submit_support_post":{"properties":{"email":{"type":"string","title":"Email"},"subject":{"type":"string","title":"Subject"},"message":{"type":"string","title":"Message"},"category":{"type":"string","title":"Category","default":"general"},"hp_website":{"type":"string","title":"Hp Website","default":""}},"type":"object","required":["email","subject","message"],"title":"Body_support_submit_support_post"},"Body_switch_plan_dashboard_admin_switch_plan_post":{"properties":{"user_id":{"type":"integer","title":"User Id"},"tier":{"type":"string","title":"Tier"}},"type":"object","required":["user_id","tier"],"title":"Body_switch_plan_dashboard_admin_switch_plan_post"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WebhookCreate":{"properties":{"url":{"type":"string","title":"Url"},"events":{"items":{"type":"string"},"type":"array","title":"Events"},"sport_filter":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Sport Filter"}},"type":"object","required":["url","events"],"title":"WebhookCreate"},"WebhookPatch":{"properties":{"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"events":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Events"},"sport_filter":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Sport Filter"},"is_active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"}},"type":"object","title":"WebhookPatch"}},"securitySchemes":{"apiKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key","description":"API key passed in the X-API-Key header. Recommended."},"apiKeyQuery":{"type":"apiKey","in":"query","name":"apiKey","description":"API key passed as the ?apiKey= query parameter. Useful for browser fetch() and webhooks where header control is limited. Equivalent to X-API-Key."},"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"APIKey","description":"API key passed via Authorization: Bearer . Equivalent to X-API-Key for compatibility with auth libraries that expect bearer tokens."}}},"servers":[{"url":"https://parlay-api.com","description":"Production (primary; HTTP/2, TLS 1.3)."},{"url":"https://api.parlay-api.com","description":"Production (high-volume; bypasses Cloudflare edge for trading bots above 30 req/min). Same origin, same auth, same endpoints."}],"tags":[{"name":"Sports & Odds","description":"Game-line and player-prop odds across all books for a sport."},{"name":"Live & In-Play","description":"Live odds, in-play arbs, and SSE streaming for active games."},{"name":"Historical","description":"Historical odds archive (game closing lines from 2005), closing lines, and period markets."},{"name":"Prediction Markets","description":"Kalshi and Polymarket prediction-market prices including crypto."},{"name":"Calculators","description":"Parlay pricing, CLV grading, Kelly / hedge / edge / free-bet math."},{"name":"Discovery","description":"Enumerate supported sports, bookmakers, market keys, and regions."},{"name":"Metadata","description":"Machine-readable platform metadata: pricing, SLA, incidents, uptime, credit costs."},{"name":"Health","description":"Liveness (/livez), readiness (/readyz), legacy deep check (/healthz)."},{"name":"Specification","description":"OpenAPI and AsyncAPI specs describing the REST and streaming surfaces."},{"name":"Sandbox","description":"Auth-less deterministic responses for integration testing without credits."},{"name":"Service Discovery","description":"/.well-known/ shortcuts and security disclosure (RFC 9116)."}]}