Every API here is available over the APIs.io API and to AI agents over MCP.
{"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 <key>`.\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=<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"}}}}}
# --- truncated at 32 KB (317 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/parlay-api/refs/heads/main/openapi/parlay-api-openapi.json