ParlayAPI

REST API for live and historical sports odds, player props, prediction-market prices, arbitrage/EV scanning, and account webhooks, with keyless sandbox and try surfaces, credit-based metering, and an AsyncAPI- described WebSocket/SSE streaming layer.

Operations 224

GET /v1/health Health Check #
HEAD /v1/health Health Check #
GET /healthz Health Check #
HEAD /healthz Health Check #
GET /health Health Check #
POST /billing/checkout Create Checkout #
POST /billing/portal Create Portal #
GET /billing/portal Open Portal Via Token #
POST /billing/subscription/cancel Cancel Subscription Route #
POST /billing/portal/send-link Admin Send Portal Link #
POST /billing/portal/revoke Admin Revoke Portal Link #
GET /billing/success Checkout Success #
GET /billing/success-page Checkout Success Page Alias #
GET /billing/cancel Checkout Cancel #
GET /billing/cancel-page Checkout Cancel Page Alias #
GET /signup Signup Page #
POST /signup Signup Submit #
GET /login Login Page #
POST /login Login Submit #
GET /logout Logout #
GET /forgot-password Forgot Password Page #
POST /forgot-password Forgot Password Submit #
GET /reset-password Reset Password Page #
POST /reset-password Reset Password Submit #
POST /v1/agent/signup Agent Signup #
POST /v1/agent/magic-link Agent Magic Link #
POST /v1/agent/checkout-link Agent Checkout Link #
GET /dashboard Dashboard Page #
GET /dashboard/billing Billing Page #
GET /dashboard/billing/cancel Billing Cancel Page #
GET /support Support Page #
POST /support Support Submit #
GET /v1/webhooks List Webhooks #
POST /v1/webhooks Create Webhook #
PATCH /v1/webhooks/{webhook_id} Update Webhook #
GET /v1/webhooks/{webhook_id} Get Webhook #
DELETE /v1/webhooks/{webhook_id} Delete Webhook #
POST /v1/webhooks/{webhook_id}/rotate-secret Rotate Webhook Secret #
GET /v1/webhooks/{webhook_id}/deliveries List Webhook Deliveries #
POST /v1/webhooks/{webhook_id}/test Test Webhook #
GET /live Live Page #
GET /live/api/data_flow Live Data Flow #
GET /live/api/sports Live Sports #
GET /live/api/games Live Games #
GET /live/api/game/{event_id} Live Game Detail #
GET /live/game/{event_id} Live Game Page #
GET /live/api/game/{event_id}/view Live Game View Json #
GET /live/api/search Live Search #
GET /live/api/command_center Live Command Center #
GET /live/api/pbp Live Pbp #
GET /live/api/disagreement Live Disagreement #
GET /live/api/sparkline Live Sparkline #
GET /live/api/best-books Live Best Books #
GET /best-books Best Books Page #
GET /moves.json Moves Json #
GET /divergence.json Divergence Json #
GET /v1/sports/{sport_key}/live/sse Sports Live Sse #
GET /v1/sports/{sport_key}/live/points Sports Live Points #
GET /v1/sports/{sport_key}/live/book_latency Sports Live Book Latency #
GET /v1/sports/{sport_key}/live/source-health Sports Live Source Health #
GET /v1/sports/{sport_key}/live/period_markets Sports Live Period Markets #
GET /v1/sports/{sport_key}/live/disagreement Sports Live Disagreement #
GET /v1/sports/{sport_key}/live/period_markets/sources Sports Live Period Sources #
GET /v1/historical/sports/{sport_key}/period_markets Historical Period Markets #
GET /v1/sports/{sport_key}/injuries Sports Injuries #
GET /v1/sports/{sport_key}/injuries/{athlete_name} Injury Lookup #
GET /v1/sports/{sport_key}/news Sports News #
GET /v1/sports/baseball_mlb/probable-pitchers Mlb Probable Pitchers #
POST /v1/alerts/prop-line Create Prop Line Alert #
GET /v1/alerts/prop-line List Prop Line Alerts #
GET /v1/alerts/prop-line/{alert_id} Get Prop Line Alert #
PATCH /v1/alerts/prop-line/{alert_id} Patch Prop Line Alert #
DELETE /v1/alerts/prop-line/{alert_id} Delete Prop Line Alert #
POST /v1/clv/import-screenshot Import Bet Screenshot #
GET /v1/affiliates List Affiliates #
GET /go/{book_key} Affiliate Redirect #
GET /partners Partners Page #
GET /terms Terms Of Service #
GET /privacy Privacy Policy #
GET /acceptable-use Acceptable Use Policy #
GET /dmca Dmca Policy #
GET /collections/ParlayAPI.postman_collection.json Postman Collection Curated #
GET /datasets/nfl-week-1-opening-lines-2026.csv Week1 Opening Lines Csv #
GET /datasets/nfl-week-1-opening-lines-2026.json Week1 Opening Lines Json #
GET /v1/sandbox/sports Sandbox Sports #
GET /v1/sandbox/sports/{sport_key}/odds Sandbox Odds #
GET /v1/sandbox/sports/{sport_key}/props Sandbox Props #
GET /v1/sandbox/sports/{sport_key}/live/period_markets Sandbox Period Markets #
GET /v1/sandbox/sports/{sport_key}/live/sse Sandbox Sse #
GET /v1/sandbox/sports/{sport_key}/arbitrage Sandbox Arbitrage #
GET /v1/sandbox/sports/{sport_key}/ev Sandbox Ev #
GET /v1/sandbox/sports/{sport_key}/consensus Sandbox Consensus #
GET /v1/sandbox/sports/{sport_key}/futures Sandbox Futures #
GET /v1/sandbox/sports/{sport_key}/scores Sandbox Scores #
GET /v1/sandbox/sports/{sport_key}/events Sandbox Events #
GET /v1/event-markets/examples Event Market Examples #
GET /v1/prediction-markets/search Search Event Markets #
GET /v1/event-markets/search Search Event Markets #
GET /v1/calc/kelly Calc Kelly #
GET /v1/calc/hedge Calc Hedge #
GET /v1/calc/edge Calc Edge #
GET /v1/calc/free-bet Calc Free Bet #
GET /v1/sports/{sport_key}/middles Find Middles #
GET /v1/verdict Verdict #
GET /v1/verdict/prefs Get Verdict Prefs #
POST /v1/verdict/prefs Set Verdict Prefs #
POST /v1/parlay/verdict Parlay Verdict #
GET /v1/sports/{sport_key}/best-bets Best Bets #
GET /v1/try/verdict Try Verdict #
GET /v1/widget/odds Widget Odds #
GET /v1/sse/odds/{sport_key} Sse Odds #
GET /v1/sse/hot/{sport_key} Sse Odds #
GET /v1/odds-drop/{sport_key} Sse Odds Drop #
GET /v1/sports List Sports #
GET /v1/meta/api-info Api Info #
GET /v1/meta/credit-costs Credit Costs #
POST /v1/meta/quote Quote Credits #
POST /v1/meta/batch-quote Batch Quote Credits #
GET /v1/meta/sla Get Sla #
GET /v1/meta/sdks Get Sdks #
GET /v1/meta/source-capabilities Source Capabilities #
GET /v1/meta/endpoints List Endpoints #
GET /v1/meta/webhooks Webhooks Spec #
GET /v1/meta/provider-state Provider State #
GET /v1/meta/source-quality Source Quality #
GET /v1/meta/source-health Source Health Alias #
GET /v1/meta/collector-flush Collector Flush Meta #
GET /v1/meta/latest-state Latest State Meta #
GET /v1/meta/per-book-sla Meta Per Book Sla #
GET /v1/meta/parser-coverage Meta Parser Coverage #
GET /v1/meta/book-coverage Meta Book Coverage #
GET /v1/asyncapi.json Get Asyncapi Spec #
GET /v1/pricing Get Pricing #
GET /v1/regions List Regions #
GET /v1/markets List Markets #
GET /v1/bookmakers List Bookmakers #
GET /v1/bookmakers/{key} Get Bookmaker #
GET /v1/bookmakers/{key}/freshness Get Bookmaker Freshness #
GET /v1/sports/{sport_key}/events List Events #
GET /v1/sports/{sport_key}/participants List Participants #
GET /v1/sports/{sport_key}/events/canonical List Canonical Events #
GET /v1/sports/{sport_key}/odds Get Odds #
GET /v1/sports/{sport_key}/events/{event_id}/odds Get Event Odds #
GET /v1/sports/{sport_key}/player-ratings Get Player Ratings #
GET /v1/sports/{sport_key}/scores Get Scores #
GET /v1/historical/sports/{sport_key}/odds Get Historical Odds #
GET /v1/historical/sports/{sport_key}/matches Get Historical Matches #
GET /v1/historical/sports/{sport_key}/coverage Get Historical Coverage #
GET /v1/sports/{sport_key}/line-movement Get Line Movement #
GET /v1/prediction-markets/{sport_key} Get Prediction Markets #
GET /v1/prediction-markets/crypto/{asset} Get Crypto Prediction Markets #
GET /v1/historical/prediction-markets/crypto/{asset} Get Crypto Prediction Markets Historical #
GET /v1/sports/{sport_key}/props Get Props #
GET /v1/sports/{sport_key}/props/coverage Get Props Coverage #
GET /v1/sports/{sport_key}/odds/coverage Get Odds Coverage #
GET /v1/pinnacle-coverage Pinnacle Coverage #
GET /v1/status Status Json #
GET /v1/sports/{sport_key}/props/markets List Prop Markets #
GET /v1/historical/sports/{sport_key}/closing-odds Get Historical Closing Odds #
POST /v1/historical/closing-lines/import Import Customer Closing Lines #
DELETE /v1/historical/closing-lines/import Delete Customer Closing Imports #
GET /v1/historical/coverage Historical Coverage #
GET /v1/historical/stats Get Historical Stats #
GET /v1/status/history Status History #
GET /v1/historical/source-quality.csv Historical Source Quality Csv #
GET /v1/historical/source-quality.json Historical Source Quality Json #
GET /v1/historical/closing-lines.json Closing Lines Json #
GET /v1/historical/closing-lines.csv Closing Lines Csv #
GET /v1/sports/{sport_key}/closing-lines Get Closing Lines #
GET /v1/sports/{sport_key}/futures Get Futures #
GET /v1/sports/{sport_key}/live Get Live Odds #
GET /v1/sports/{sport_key}/compare Compare Odds #
GET /v1/sports/{sport_key}/best-line Best Line Alias #
GET /v1/sports/{sport_key}/odds/props Odds Props Alias #
GET /v1/inplay/arbs Get Inplay Arbs #
POST /v1/parlay/price Price Parlay #
POST /v1/clv Grade Clv #
POST /v1/clv/history Clv History #
POST /v1/sgp/price Sgp Price #
GET /v1/sports/{sport_key}/arbitrage Find Arbitrage #
GET /v1/sports/{sport_key}/ev Find Positive Ev #
GET /v1/sports/{sport_key}/consensus Get Consensus #
GET /v1/usage Get Usage #
GET /v1/account Get Usage #
GET /v1/stats Get Stats #
GET /pricing Pricing #
GET /v1/exchange/{sport_key}/markets Get Exchange Markets #
GET /v1/exchanges List Exchanges #
GET /collections/postman.json Postman Collection #
POST /v1/try/sgp/price Try Sgp Price #
GET /v1/meta/movers Meta Movers #
GET /v1/meta/limits Meta Limits #
GET /v1/key/successor Key Successor #
GET /v1/meta/api-key-check Meta Api Key Check #
GET /v1/meta/usage Meta Usage #
GET /v1/meta/diagnostic Meta Diagnostic #
GET /v1/meta/status-history.json Meta Status History Json #
GET /v1/meta/book-catalog Meta Book Catalog #
GET /v1/meta/event-search Meta Event Search #
GET /v1/meta/markets Meta Markets #
GET /v1/meta/regions Meta Regions #
GET /badge/{kind}.svg Status Badge Svg #
GET /v1/try/{sport_key}/odds Try Odds #
GET /v1/try/{sport_key}/arbitrage Try Arbitrage #
GET /v1/try/{sport_key}/ev Try Ev #
GET /v1/try/{sport_key}/middles Try Middles #
GET / Root #
HEAD / Root #
GET /changelog.json Changelog Json #
GET /v1/meta/changelog Meta Changelog Json #
GET /v1/meta/incidents Incidents Json #
GET /v1/meta/uptime Uptime Stats #
GET /v1/metrics Prometheus Metrics #
GET /api Api Root #
GET /v1/keys List Api Keys #
POST /v1/keys Create Api Key #

Documentation

Specifications

Other Resources

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/parlayapi"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

parlay-api-openapi.json Raw ↑
{"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