# Lumify — Technical docs payload for AI agents # https://lumify.ai/docs/llms-full.txt # # Intended for AI systems, coding agents, and crawlers that need the # complete technical reference with cite-back URLs. For GEO/orientation # (coverage, comparisons, FAQ) see https://lumify.ai/llms-full.txt. # For the short overview see https://lumify.ai/llms.txt. # # Sources are concatenated from public HTML docs (text-extracted), markdown # twins under docs/external/, and the OpenAPI-derived endpoint dump. # Last updated: 2026-08-02 --- # Docs overview Cite-back: https://lumify.ai/docs.md # Lumify Documentation > Canonical URL: https://lumify.ai/docs.md > HTML twin: https://lumify.ai/docs Quickstart, authentication, rate limits, and versioning for the sports intelligence API. # Quickstart Get your first Lumify API response in under 60 seconds. ### 1 — Get your API key Create a key in your [API Keys dashboard](/api-keys). Keys look like lmfy-xxxxxx.yyyy… and are passed as a Bearer token on every request. ### 2 — Make your first request Fetch today's MLB schedule: ```bash curl "https://lumify.ai/v1/events?sport=mlb&status=scheduled" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### 3 — Get bet intelligence for a game Take any id from the events response and fetch its pre-game confidence scores, signals, and recommended bets: ```bash curl "https://lumify.ai/v1/events/{id}/intelligence" \ -H "Authorization: Bearer YOUR_API_KEY" ``` > **Note:** That's it. From here, explore the full [API Reference](/docs/reference). Common next steps: [look up a player](/docs/reference#players), [fetch live odds](/docs/reference#event-odds), or [poll a live score](/docs/reference#event-score). Prefer a longer walkthrough, or want the markdown version for an agent? See the [Quick Start Guide](/docs/getting-started/quick-start.md). # Introduction The Lumify v1 API gives you structured, real-time sports intelligence — schedules, live scores, team and player data — all in one consistent interface. Every endpoint is authenticated with a bearer token and returns JSON. > **Note:** Base URL  https://lumify.ai All timestamps are ISO 8601 UTC strings (YYYY-MM-DD HH:MM:SS). Date-only fields use YYYY-MM-DD. All request bodies and responses use UTF-8 JSON. Prefer an interactive explorer or a machine-readable contract? The full OpenAPI schema is at [/openapi.json](/openapi.json) — browse it live in [ReDoc](/api/redoc) or [Swagger UI](/api/docs). ## Authentication Every /v1/* request must include a valid Lumify API key as a **Bearer token**. Keys are created in your [dashboard](/api-keys) and follow the format lmfy-xxxxxx.yyyyyyyy… > **Warning:** You don't have any API keys yet. Create one to start making requests. [Create API key →](/api-keys) ### Request header | Header | Value | | --- | --- | | Authorization | Bearer YOUR_API_KEY | ### Example ```bash curl https://lumify.ai/v1/events \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Auth errors | Status | Reason | When it occurs | | --- | --- | --- | | 401 | Unauthorized | Missing, malformed, invalid, inactive, or expired API key. Expired keys return the same 401 as invalid keys (no expiry leak). Check Authorization: Bearer lmfy-…. | | 403 | Forbidden | Valid key but denied for this resource — e.g. sport scope not granted (error.code = sport_scope_denied). See upgrade_url in the error body. | ## Rate Limits Limits are enforced per API key on a sliding 60-second window. Every response — success or error — includes the following headers so you can throttle proactively: | Response header | Description | | --- | --- | | X-RateLimit-Limit | Maximum requests allowed in the window | | X-RateLimit-Remaining | Requests remaining in the current window | | X-RateLimit-Reset | Unix timestamp when the window resets | | X-Credits-Used | Credits charged for this successful authenticated call (variable-cost endpoints may be > 1) | | X-Credits-Remaining | Best-effort remaining balance after this call. Omitted when the plan is unlimited (pure metered PAYG) or the balance cannot be resolved. | ### Limits by plan | Plan | Requests / minute | | --- | --- | | Free Tier | 20 | | Pay As You Go | 60 | | Growth | 120 | | Enterprise | Custom | Limits are enforced per API key on a sliding 60-second window. Exceeding your limit returns 429 Too Many Requests. No credits are consumed on a 429 response. ### 429 response ```json { "error": { "code": "rate_limit_exceeded", "message": "Rate limit exceeded", "status": 429, "doc_url": "https://lumify.ai/docs/reference#error-codes", "retry_after": 23 }, "detail": "Rate limit exceeded" } ``` > **Tip:** Best practice: Read X-RateLimit-Remaining on every response and back off before you hit zero. When you receive a 429, wait error.retry_after seconds (also mirrored in the Retry-After header) before retrying. Switch on error.code. ## Versioning The API version is part of the path (/v1/…). We guarantee backwards compatibility within a major version. Breaking changes ship under a new prefix (/v2/…) with at least 90 days notice. --- # Guides Cite-back: https://lumify.ai/docs/guides.md # Lumify Guides > Canonical URL: https://lumify.ai/docs/guides.md > HTML twin: https://lumify.ai/docs/guides MCP setup, agent resources, and task-oriented recipes for building on Lumify. # Guides Task-oriented walkthroughs for connecting an agent to Lumify — MCP setup, integration recipes, and copy-paste code for common jobs. > **Tip:** For agents: the machine-readable twin of this page is [/docs/guides.md](/docs/guides.md). For deeper copy-paste recipes see the [agent cookbook](/docs/agent-cookbook.md). # Model Context Protocol (MCP) Lumify runs a hosted MCP server so AI agents can call the entire sports-intelligence API as native tools — no wrapper code required. > **Note:** Endpoint  https://lumify.ai/mcp Transport  Streamable HTTP (JSON mode, stateless)  · Auth  Bearer API key  · Protocol  2025-06-18 Any MCP-compatible client connects by pointing at https://lumify.ai/mcp and passing your Lumify API key as a Bearer token. The server is fully self-describing — a client's tools/list call returns the current tool catalogue with input schemas. You can inspect the discovery document with a browser GET to [/mcp](/mcp) (JSON). Tool calls use POST only — Lumify does not offer a GET Server-Sent Events stream on this URL. Some clients briefly probe with Accept: text/event-stream and receive 405 Allow: POST; that is expected transport negotiation, then they continue over POST JSON (streamable HTTP). [Add Lumify MCP to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=lumify&config=eyJ1cmwiOiJodHRwczovL2x1bWlmeS5haS9tY3AiLCJoZWFkZXJzIjp7IkF1dGhvcml6YXRpb24iOiJCZWFyZXIgWU9VUl9BUElfS0VZIn19) [Get an API key first →](/register) [AI-assisted setup guide →](/docs/ai) The one-click install opens Cursor with a placeholder key — replace YOUR_API_KEY with a key from your [dashboard](/api-keys) before approving. ### Connect from Cursor Recommended (remote): add Lumify to ~/.cursor/mcp.json (or a project-local .cursor/mcp.json): ```json { "mcpServers": { "lumify": { "url": "https://lumify.ai/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` Stdio bridge (optional): if you prefer a local process, use the published @lumifyai/mcp package: ```json { "mcpServers": { "lumify": { "command": "npx", "args": ["-y", "@lumifyai/mcp"], "env": { "LUMIFY_API_KEY": "YOUR_API_KEY" } } } } ``` ### Connect from Claude Desktop Claude Desktop speaks stdio. Prefer the official bridge package: ```json { "mcpServers": { "lumify": { "command": "npx", "args": ["-y", "@lumifyai/mcp"], "env": { "LUMIFY_API_KEY": "YOUR_API_KEY" } } } } ``` Alternatively, bridge with mcp-remote: ```json { "mcpServers": { "lumify": { "command": "npx", "args": [ "-y", "mcp-remote", "https://lumify.ai/mcp", "--header", "Authorization: Bearer YOUR_API_KEY" ] } } } ``` ### Connect from VS Code / Copilot Add a server entry under mcp.servers in your VS Code settings (or .vscode/mcp.json): ```json { "servers": { "lumify": { "type": "http", "url": "https://lumify.ai/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` ### Available tools Each tool maps 1:1 to a REST endpoint and returns identical data. Credit cost mirrors the equivalent REST call. | Tool | Description | Credits | | --- | --- | --- | | list_sports | Supported sports with current active season. | 1 | | list_seasons | Seasons per sport/league; optional current-only filter. | 1 | | list_events | Schedule / scores, filterable by sport, league, status, date, season. | 1 | | get_event | Single event; optional include_odds (scoped by bookmaker, default pinnacle) / include_intelligence. | 1 (+1 odds single book / +2 multi or all; +1 intel) | | batch_get_events | Multiple events by id in one call (max 25). Missing ids cost nothing. | Sum of each event's cost | | query_events | Natural-language event search (e.g. "live nfl games today") — rule-based, mapped to list_events filters. | 1 | | get_live_score | Lightweight live score snapshot. | 1 | | get_odds | Current moneyline / spread / total lines. | 1 (2 for all books) | | get_odds_history | Recorded line-movement history. | 1 (2 for all books) | | get_stats | Raw team/match statistics — team strength, form, H2H, rest, venue splits (soccer only). No scoring attached. | 1 | | get_splits | Public betting splits (bets% / handle%). | 1 | | get_intelligence | Confidence scores, signals, narratives, recommended bets. | 1 | | list_teams | Team directory with sport/league/conference filters. | 1 | | get_team | Single team profile with home venue. | 1 | | search_players | Player search by name, sport, country, ranking. | 1 | | get_player | Single player profile. | 1 | | get_player_events | A player's schedule / results (±30 days by default). | 1 | | estimate_cost | Pre-call credit-cost estimate (min–max range) for one or more planned tool calls. | Free | ### Billing The MCP handshake is free: initialize, tools/list, and ping never cost credits. Only tools/call is metered, at the same rate as the matching REST endpoint. Each result reports its cost under _meta.credits_used. Calls that return no usable data because it isn't available yet — odds, line history, splits, or intelligence for a match that hasn't been priced/computed — are free (_meta.credits_used: 0). > **Warning:** Web connectors (ChatGPT / Claude.ai): browser-based connectors require OAuth, which Lumify's key-based MCP server does not implement yet. Use Cursor, Claude Desktop (via npx @lumifyai/mcp), or any client that supports Bearer-token headers. > **Tip:** Building with AI? See the [AI-assisted development guide](/docs/ai) for Cursor/Claude prompts, llms.txt, OpenAPI, and copy-paste agent recipes. ## Cookbook & Postman Deeper, copy-paste-ready references for building on Lumify: | Resource | Description | | --- | --- | | [Agent cookbook](/docs/agent-cookbook.md) | End-to-end recipes for REST + MCP, including verified client configs and billing behaviour. | | [Postman collection](/docs/lumify.postman_collection.json) | Importable collection covering the REST endpoints and MCP JSON-RPC calls. | | [llms.txt](/llms.txt) | Compact machine-readable overview for LLM agents. | | [agent.json](/.well-known/agent.json) | Agent manifest with the MCP endpoint and transport. | | [OpenAPI schema](/openapi.json) | Full machine-readable spec for the REST surface. | ### Task Recipes Copy-paste workflows that combine multiple endpoints for a specific job. ## Track live odds movement Two ways to watch a line move without hammering [/v1/events/{id}/odds/history](/docs/reference#event-odds-history) on a tight poll loop: - **Push (recommended):** create a [webhook subscription](/docs/reference#webhooks) with event_types: ["line_move"] — you'll be notified the moment a price or point changes. Transient delivery failures retry automatically; inspect [delivery history](/docs/reference#webhook-deliveries) if a callback looks stuck. - **Pull:** poll [odds history](/docs/reference#event-odds-history) every few minutes and diff against the last recorded_at you've seen. History is not cached, so every call reflects the latest ingest. ```bash # Subscribe once — deliveries arrive at your endpoint from then on curl -X POST https://lumify.ai/v1/webhooks \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/hooks/lumify", "event_types": ["line_move"]}' ``` ## Build an MCP betting-splits agent Give an LLM agent read access to public betting splits with no REST wrapper code — connect to the hosted [MCP server](#mcp) and call its tools directly: 1. Connect Cursor, Claude Desktop, or any MCP client to https://lumify.ai/mcp with your API key as the Bearer token (see the [MCP section](#mcp) for client configs). 2. Call the list_events tool filtered by sport/date to find event IDs. 3. Call the get_splits tool per event ID — the agent reasons over bets%/handle% directly, no JSON parsing code required. > **Tip:** Prefer REST? The equivalent call is [GET /v1/events/{id}/splits](/docs/reference#event-splits) — MCP and REST share the same billing and rate limits. ## Pull a full event intelligence report in one call Fetch schedule, odds, and bet intelligence together instead of three separate round trips: ```bash curl "https://lumify.ai/v1/events/4812?include_odds=true&include_intelligence=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` This costs **3 credits** total (1 base + 1 per include) instead of 3 separate 1-credit calls, and saves two round trips. See [Get an event](/docs/reference#event-detail) for the full parameter reference. ## Provision API access programmatically Let an agent onboard itself — mint a key, check its balance, and top up credits — without a human touching the dashboard: ```bash # 1. Mint a key (needs an existing session or key to bootstrap) curl -X POST https://lumify.ai/api/agent/keys \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "agent-worker-1"}' # 2. Check balance before a big batch of calls curl https://lumify.ai/api/agent/credits \ -H "Authorization: Bearer YOUR_API_KEY" # 3. Top up if running low curl -X POST https://lumify.ai/api/agent/credits/topup \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"pack_id": 1}' ``` Full parameter and error reference: [Manage API keys](/docs/reference#agent-keys) and [Credits & credit packs](/docs/reference#agent-credits). --- # API reference Cite-back: https://lumify.ai/docs/reference.md # Lumify API Reference > Canonical URL: https://lumify.ai/docs/reference.md > HTML twin: https://lumify.ai/docs/reference Every v1 capability — endpoints, parameters, streaming, webhooks, agent onboarding, and error codes. # API Reference Every capability of the Lumify v1 API, grouped by what it does rather than its raw path. New to the API? Start with the [Docs](/docs) overview for auth and rate limits. > **Tip:** For agents: the Markdown twin of this page is [/docs/reference.md](/docs/reference.md). Full concatenated technical payload: [/docs/llms-full.txt](/docs/llms-full.txt). Endpoint dump alone: [/openapi-llms.txt](/openapi-llms.txt). GEO orientation (FAQ/pricing/coverage): [/llms-full.txt](/llms-full.txt). Explore interactively via [ReDoc](/api/redoc) / [/openapi.json](/openapi.json). Discovery/manifest for autonomous agents lives at [/.well-known/agent.json](/.well-known/agent.json). ### Reference Data Sports catalogue and season lookup — stable metadata used to filter all other endpoints. [MCP: list_sports, list_seasons →](/docs/guides#mcp) ## List sports `GET /v1/sports` Returns all supported sports and their associated leagues. Each league entry includes the currently active season where one exists. Use the slug values to filter other endpoints. ### Query parameters | Parameter | Type | | Default | Description | | --- | --- | --- | --- | --- | | active_only | boolean | optional | true | When true, exclude sports marked inactive. Pass false to include all sports regardless of status. | **Request** ```bash curl https://lumify.ai/v1/sports \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response** ```json { "sports": [ { "id": 1, "slug": "nhl", // use as ?sport= filter on other endpoints "name": "NHL", "is_team_sport": true, // false for Tennis (individual sport) "leagues": [ { "id": 1, "slug": "nhl", // use as ?league= filter "name": "National Hockey League", "abbreviation": "NHL", "league_type": "team_league", // team_league | individual_tour | tournament "country_code": "USA", "current_season": { "id": 1, // use as ?season_id= on /v1/events "year": 2026, "name": "NHL 2025-26", "phase": "playoffs", // preseason | regular_season | playoffs "start_date": "2025-10-07", "end_date": "2026-06-30" } // null if no season is currently active } ] } ], "total": 6 } ``` ## List seasons `GET /v1/seasons` Returns seasons across all leagues. By default only currently active seasons are returned. Use this endpoint to look up a season_id before filtering events by season; pass current_only=false to include historical seasons. ### Query parameters | Parameter | Type | | Default | Description | | --- | --- | --- | --- | --- | | sport | string | optional | — | Filter to seasons for a single sport slug (e.g. nhl, tennis). Returns an empty list for unknown slugs. | | current_only | boolean | optional | true | When true (default), return only seasons currently in progress (is_current = true). Pass false to include historical seasons. | **Request** ```bash curl "https://lumify.ai/v1/seasons?sport=nhl¤t_only=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response** ```json { "seasons": [ { "id": 1, "year": 2026, "name": "NHL 2025-26", "phase": "playoffs", // preseason | regular_season | playoffs "start_date": "2025-10-07", "end_date": "2026-06-30", "is_current": true, "sport": { "slug": "nhl", "name": "NHL" }, "league": { "slug": "nhl", "name": "National Hockey League", "abbreviation": "NHL" } } ], "total": 1 } ``` ### Schedule & Scores Event calendar, natural-language search, batch lookup, full event detail, and live score polling. [MCP: list_events, query_events, get_event, batch_get_events, get_live_score →](/docs/guides#mcp) [Guide: Track live odds movement →](/docs/guides#recipe-odds-movement) ## List events `GET /v1/events` Returns a paginated, filterable list of events — games, matches, or contests across all supported sports. Results are sorted chronologically by starts_at ASC. ### Query parameters | Parameter | Type | | Default | Description | | --- | --- | --- | --- | --- | | sport | string | optional | — | Filter by sport slug: nfl, nba, mlb, nhl, tennis, soccer, ncaaf, ncaab. Unknown slugs return an empty list. | | league | string | optional | — | Narrow to a specific league slug (e.g. atp, fifa_world_cup). More specific than sport. | | status | string | optional | — | Filter by lifecycle status. See the [Status Values](#status-values) table. Returns 400 for unrecognised values. | | date | string | optional | — | Single-day filter. Format: YYYY-MM-DD (UTC). Mutually exclusive with from / to — combining them returns 400. | | from | string | optional | — | Range start date (UTC, inclusive). Pair with to. Format: YYYY-MM-DD. | | to | string | optional | — | Range end date (UTC, inclusive). Max range: 90 days. Returns 400 if exceeded. | | season_id | integer | optional | — | Restrict to a single season. Obtain the ID from /v1/seasons. | | team_id | integer | optional | — | Restrict to events where this team participates. Resolve the ID via GET /v1/teams?q=…. Preferred over natural-language team names on POST /v1/query. | | after_id | integer | optional | — | Pagination cursor. Pass the next_after_id from the previous response to retrieve the next page. | | limit | integer | optional | 25 | Page size. Range: 1–100. | | include_scores | boolean | optional | false | When true, each event in the list includes full participants, draw_type, broadcast, court, and order_of_play. Bypasses cache. Intended for small result sets (≤ 200 events). | | has_recommend | boolean | optional | — | When true, returns only events where the intelligence pipeline has found at least one recommended bet (has_recommend = true). Useful for polling a filtered picks feed without fetching intelligence for every event individually. Requires the analysis pipeline to have run — events not yet analyzed will not appear. | | sort | string | optional | time | time — chronological by effective start time (default). status — priority order Live → Delayed/Upcoming → Final → Cancelled/Postponed, then chronological within each group. Cursor pagination (after_id) is not supported with sort=status — combining them returns 400. | **Request** ```bash # All live NHL games today curl "https://lumify.ai/v1/events?sport=nhl&status=inprogress" \ -H "Authorization: Bearer YOUR_API_KEY" # A week of NBA games curl "https://lumify.ai/v1/events?sport=nba&from=2026-05-10&to=2026-05-17&limit=50" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response** ```json { "events": [ { "id": 4812, "name": "Bruins vs Maple Leafs", "sport": "nhl", "league": "nhl", "season_id": 1, "starts_at": "2026-05-10T23:00:00Z", // UTC "scheduled_start_at": null, // set for tennis when start drifts "starts_at_qualifier": null, // exact | not_before | following | tbd "status": "scheduled", // see Status Values table "result_type": null, // regulation | overtime | shootout | … "period": null, // "3", "Top 7th", "Set 2" when live "period_label": null, // human-readable: "Set 2", "Q3" — sport-aware "clock": null, // "8:42" when live (sport-dependent) "round": "Round 2", "neutral_site": false, "competition": { // null if event is not linked to a competition "id": 1, "name": "ATP Rome", // clean tournament name, e.g. "ATP Rome", "WTA Roland Garros" "surface": "clay", // clay | grass | hard | indoor_hard | null "tier": "masters_1000" // grand_slam | masters_1000 | atp_500 | atp_250 | wta_1000 | wta_500 | wta_250 | null }, "venue": { "id": 1, "name": "TD Garden", "city": "Boston", "surface": null, "roof_type": null, "timezone": "America/New_York" } } ], "total": 25, // events on this page "next_after_id": 4836 // null on the last page — no more results } ``` > **Note:** Pagination: Pass next_after_id as ?after_id= on the next request. Repeat until next_after_id is null. The cursor is stable even if new events are ingested between pages. > **Warning:** Date filter note: ?date and ?from / ?to are mutually exclusive. Combining them returns 400. ## Natural-language event search `POST /v1/query` Map free text to the same filters GET /v1/events accepts, then return those events. This is a small **rule-based** mapper — not an LLM call — so results are deterministic and auditable. Costs **1 credit**, same as listing events; interpreting the query text is free. The response includes the parsed filters (interpreted), the literal equivalent REST call (equivalent_request), and any words that didn't map (unrecognized_terms) so agents can see exactly what was understood. ### Request body | Field | Type | | Description | | --- | --- | --- | --- | | query | string | required | Free text, max 500 characters. Example: live nfl games today. | | limit | integer | optional | Overrides any limit parsed from the text. Range: 1–100. | ### What the mapper recognizes | Filter | Examples | | --- | --- | | sport | nfl, nba, mlb, nhl, tennis, soccer, ncaaf, ncaab; aliases hockey, basketball, baseball, american football, college football, college basketball. Bare football is ambiguous and left unrecognized. | | status | live / in progress / in-progress → inprogress; final, upcoming, postponed, cancelled, delayed, suspended, walkover. | | date / range | today, tomorrow, yesterday; this week / next week / last week (rolling UTC days); next 3 days / last 2 weeks; one YYYY-MM-DD → date, two → from/to. | | limit | A bare integer 1–100 in the text (e.g. 5 nhl games), overridden by the body field when present. | **Request** ```bash curl -X POST https://lumify.ai/v1/query \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query":"live nhl games today","limit":5}' ``` **Response** ```json { "query": "live nhl games today", "interpreted": { "sport": "nhl", "status": "inprogress", "date": "2026-07-16", "from": null, "to": null, "limit": 5 }, "unrecognized_terms": [], "equivalent_request": "GET /v1/events?sport=nhl&status=inprogress&date=2026-07-16&limit=5", "events": [/* same EventSummary objects as GET /v1/events */ ], "total": 2, "next_after_id": null } ``` > **Note:** Agent tip: Always check unrecognized_terms and equivalent_request before acting on results. An empty interpreted.sport with unrecognized terms like football means the query was ambiguous — clarify rather than searching all sports. ## Get an event `GET /v1/events/{id}` Returns the full record for a single event — all participants (teams or players), complete venue data, schedule metadata, and result. Completed events are cached for **1 hour**; all other statuses for **5 minutes**. Use ?include_odds=true and/or ?include_intelligence=true to embed the current odds and bet intelligence directly in this response, saving extra round trips. Odds are scoped by bookmaker (default Pinnacle: +1 credit; all or a list: +2). Intelligence adds +1 when available. Use [/v1/events/{id}/score](#event-score) when you only need live score data. ### Path parameters | Parameter | Type | | Description | | --- | --- | --- | --- | | id | integer | required | Lumify event ID. Non-integer values return 422. Unknown IDs return 404. | ### Query parameters | Parameter | Type | | Default | Description | | --- | --- | --- | --- | --- | | include_odds | boolean | optional | false | When true, embeds the current odds payload under an odds key — same shape as [GET /v1/events/{id}/odds](#event-odds), scoped by bookmaker (default: pinnacle). Adds +1 credit for a single book or +2 credits for all / a comma-separated list. | | include_intelligence | boolean | optional | false | When true, embeds the bet intelligence payload under an intelligence key — same shape as [GET /v1/events/{id}/intelligence](#event-intelligence). Adds +1 credit (total 2 credits). | | bookmaker | string | optional | system default | Bookmaker for inlined odds (when include_odds=true) and for intelligence.bets[].market prices (when include_intelligence=true). Valid values: pinnacle, fanduel, draftkings, betmgm, caesars, bet365, circa, hardrock, betonline, all. Defaults to pinnacle. Single book = +1 credit for odds; all or a comma-separated list = +2. | > **Note:** Credit costs: A plain call costs 1 credit. Adding include_odds=true costs 2 credits for a single book (default Pinnacle) or 3 credits for bookmaker=all / a list. Adding include_intelligence=true costs 2 credits total. Using both with default bookmaker costs 3 credits — cheaper than fetching event + odds + intelligence as three separate calls. **Request** ```bash # Basic — 1 credit curl https://lumify.ai/v1/events/4812 \ -H "Authorization: Bearer YOUR_API_KEY" # Compound — event + odds + intelligence in one call (3 credits) curl "https://lumify.ai/v1/events/4812?include_odds=true&include_intelligence=true" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response** ```json // Team sport example (NHL) { "id": 4812, "name": "Bruins vs Maple Leafs", "sport": "nhl", "league": "nhl", "season_id": 1, "starts_at": "2026-05-10T23:00:00Z", // UTC "scheduled_start_at": null, // original time before drift; set for tennis "starts_at_qualifier": null, // exact | not_before | following | tbd "status": "final", "result_type": "overtime", // regulation | overtime | shootout | retired | walkover "period": null, // null after game ends; "3", "OT" while live "period_label": null, // human-readable period, e.g. "Set 2", "Q3" "clock": null, // null after game ends; "8:42" while live "round": "Round 2", "draw_type": null, // singles | doubles (tennis); null for team sports "neutral_site": false, "broadcast": "ESPN", "court": null, // named court, e.g. "Centre Court" (tennis) "order_of_play": null, // 1 = first match of the day on this court "competition": null, // populated for tennis ATP/WTA events — see tennis example below "venue": { "id": 1, "name": "TD Garden", "city": "Boston", "country": "USA", "surface": "ice", "capacity": 17850, "timezone": "America/New_York" // convert starts_at to local time with this }, "participants": [ { "role": "home", // home | away (team sports); player_1 | player_2 (tennis) "score": "3", // null before game starts; "6-4, 7-5" for tennis "game_score": null, // live in-game score, e.g. "40-15" (tennis); null otherwise "is_winner": true, // null until final; true/false after "team": { "id": 1, "name": "Boston Bruins", "abbreviation": "BOS", "country_code": "USA" }, "player": null, // null for team sports; see tennis example below "period_scores": [] // per-period breakdown; empty [] if unavailable }, { "role": "away", "score": "2", "game_score": null, "is_winner": false, "team": { "id": 2, "name": "Toronto Maple Leafs", "abbreviation": "TOR", "country_code": "CAN" }, "player": null, "period_scores": [] } ], "updated_at": "2026-05-11T02:14:37Z" } // Tennis singles example — note name format, competition object, and player shape { "id": 4821, "name": "Jacob Fearnley v. Giovanni Mpetshi Perricard", // "First Last v. First Last" "sport": "tennis", "league": "atp", "season_id": 12, "starts_at": "2026-05-08T10:00:00Z", "scheduled_start_at": "2026-05-08T11:00:00Z", // original announced time "starts_at_qualifier": "not_before", "status": "final", "result_type": "regulation", "period": null, "period_label": null, "clock": null, "round": "Round of 16", "draw_type": "singles", "neutral_site": false, "broadcast": null, "court": "Campo Centrale", "order_of_play": 2, "competition": { "id": 1, "name": "ATP Rome", "surface": "clay", // clay | grass | hard | indoor_hard "tier": "masters_1000" // grand_slam | masters_1000 | atp_500 | atp_250 | wta_1000 | wta_500 | wta_250 }, "venue": { "id": 7, "name": "Foro Italico", "city": "Rome", "country": "ITA", "surface": "clay", "capacity": null, "timezone": "Europe/Rome" }, "participants": [ { "participant_id": 307, // stable Lumify join ID for this participant in this event "role": "player_1", "score": "6-4, 3-6, 6-3", "game_score": null, // live: "40-15"; null when not in a game point "is_winner": true, "team": null, "player": { "id": 102, "name": "Jacob Fearnley", // "First Last" format "country_code": "GBR", // ISO 3-letter code "image_url": "https://lumify.ai/media/players/tennis/028BdVOj.png" }, "period_scores": [// per-set scores { "period": "S1", "score": 6, "tiebreak": null, "confirmed": true }, { "period": "S2", "score": 3, "tiebreak": null, "confirmed": true }, { "period": "S3", "score": 6, "tiebreak": null, "confirmed": true } ] }, { "participant_id": 308, "role": "player_2", "score": "4-6, 6-3, 3-6", "game_score": null, "is_winner": false, "team": null, "player": { "id": 217, "name": "Giovanni Mpetshi Perricard", "country_code": "FRA", "image_url": null // null until enrichment runs; format: lumify.ai/media/players/tennis/{hash}.png }, "period_scores": [ { "period": "S1", "score": 4, "tiebreak": null, "confirmed": true }, { "period": "S2", "score": 6, "tiebreak": null, "confirmed": true }, { "period": "S3", "score": 3, "tiebreak": null, "confirmed": true } ] } ], "updated_at": "2026-05-08T14:31:00Z" } ``` **Compound response — with `include_odds=true&include_intelligence=true`** ```json // Standard event fields are unchanged — two additional top-level keys are appended { "id": 4812, // ... all standard event fields ... "updated_at": "2026-05-10T02:14:37Z", "odds": { "available": true, "bookmakers": [ { "bookmaker": "pinnacle", "markets": [ { "key": "h2h", "label": "moneyline", "outcomes": [ { "outcome": "Boston Bruins", "price": -140, "point": null }, { "outcome": "Toronto Maple Leafs", "price": 120, "point": null } ] } ], "captured_at": "2026-05-10T01:30:00Z" } ], "last_updated": "2026-05-10T01:30:00Z" }, "intelligence": { "available": true, "odds_source": "pinnacle", "has_recommend": true, "analyst_take": "...", "match_overview": null, "intelligence_updated_at": "2026-05-10T01:45:00Z", "bets": [/* same bet objects as GET /v1/events/{id}/intelligence */ ] } } ``` > **Warning:** Caching note: When include_odds=true, the compound response is cached for 2 minutes (pre-game/live) rather than the standard 5-minute event TTL, to reflect the faster-moving odds data. Final events remain cached for 1 hour. > **Note:** Tennis scheduling: For tennis, starts_at is a floor (not a fixed time). scheduled_start_at preserves the original announced time. starts_at_qualifier will be not_before for order-of-play matches. Use court and order_of_play to understand draw position. ## Batch get events `POST /v1/events/batch` Fetch multiple events by id in a single round-trip — for agents that already have a list of ids (e.g. from GET /v1/events) and want full detail for each without one GET per event. Max **25** ids per call. Each returned event has the same shape as [GET /v1/events/{id}](#event-detail). Credits are the sum of each event's normal compound cost. Duplicate ids are billed once. Ids that don't exist are returned under not_found and cost nothing. Unavailable odds/intelligence add-ons remain free (billing fairness). ### Request body | Field | Type | | Description | | --- | --- | --- | --- | | event_ids | integer[] | required | 1–25 event ids. Order is preserved in the response. | | include_odds | boolean | optional | Inline current odds scoped by bookmaker (default: pinnacle). +1 credit per event for a single book when available; +2 for all or a comma-separated list. | | include_intelligence | boolean | optional | Inline bet intelligence on each event (+1 credit per event when available). | | bookmaker | string | optional | Bookmaker for inlined odds and intelligence market prices. Defaults to pinnacle. Valid: pinnacle, fanduel, draftkings, betmgm, caesars, bet365, circa, hardrock, betonline, all. | **Request** ```bash curl -X POST https://lumify.ai/v1/events/batch \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"event_ids":[101,102,999],"include_odds":true}' ``` **Response** ```json { "events": [/* EventDetail objects, same shape as GET /v1/events/{id} */ ], "not_found": [999], "total": 2 } ``` ## Get an event's score `GET /v1/events/{id}/score` Lightweight score snapshot optimised for live-polling. Returns only the fields needed to render a scoreboard: status, period, clock, and per-participant scores. Cache TTL is **30 seconds** when status=inprogress, and 5 minutes otherwise — poll this endpoint instead of /v1/events/{id} when you only need score state. ### Path parameters | Parameter | Type | | Description | | --- | --- | --- | --- | | id | integer | required | Lumify event ID. Non-integer values return 422. Unknown IDs return 404. | **Request** ```bash curl https://lumify.ai/v1/events/4812/score \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response** ```json { "event_id": 4812, "status": "inprogress", "period": "3", // sport-specific — see period format table below "period_label": "Q3", // human-readable; null if unavailable "clock": "8:42", // null for MLB, Tennis, NHL (not in source) "scores": [ { "role": "home", "name": "Boston Celtics", "abbreviation": "BOS", "score": "101", "game_score": null, // live in-game score (tennis only, e.g. "40-15") "is_winner": null, // null while in-progress; true/false when final "period_scores": [] // per-period breakdown; empty [] if unavailable }, { "role": "away", "name": "Los Angeles Lakers", "abbreviation": "LAL", "score": "98", "game_score": null, "is_winner": null, "period_scores": [] } ], "updated_at": "2026-05-10T01:18:44Z" // last ingest write — use to detect staleness } ``` ### Period format by sport The period field is a free-form string. null when the event has not yet started or the source does not provide this data. | Sport | Example period | Example clock | | --- | --- | --- | | NHL | "1" "2" "3" "OT" "SO" | null | | NBA / NFL | "1" "2" "3" "4" "OT" | "8:42" "0:00" | | MLB | "Top 7th" "Bot 9th" | null | | Tennis | "Set 1" "Set 2" "Set 3" | null | | Soccer | "1H" "2H" "ET1" "ET2" "PKs" | "45'+2" "67'" | ### Bet Intelligence Raw team/match statistics (Data), confidence scores and signal breakdowns (Intelligence), validator output, LLM-generated narratives, and public betting splits per event. [MCP: get_stats, get_intelligence, get_splits →](/docs/guides#mcp) [Guide: Build an MCP betting-splits agent →](/docs/guides#recipe-mcp-splits) [Guide: Pull a full intelligence report →](/docs/guides#recipe-intelligence-report) ## Get raw match statistics for an event `GET /v1/events/{id}/stats` Returns the deterministic, reproducible team and match statistics behind an event — team strength, recent form, head-to-head history, rest days, home/away splits, per-game boxscore rates (shots/SoT for & against, possession, corners, cards, save rate) over explicit windows (rates_l5 / rates_season), club league-table rank/GD/W-D-L, strength-of-schedule (sos), and lineups/formation — computed directly from completed results and ESPN team box scores/rosters already in the public record. This is the **Data** layer: no market/odds data (see /v1/events/{id}/odds for that) and no scoring, weighting, confidence, or narrative is attached. /v1/events/{id}/intelligence is the **Intelligence** layer built on top of these same inputs — use this endpoint when you want to run your own analysis on Lumify's underlying aggregates instead of (or alongside) Lumify's judgment. Always returns 200 when the event exists — check available to determine whether both teams have resolved. Returns 404 only if the event ID does not exist, and 400 if the event's sport is not yet supported. > **Note:** Sports coverage: Soccer only today (FIFA World Cup and club leagues via the SoccerLeagueProfile split — see field descriptions below). Other sports will get their own raw-stat endpoint as their equivalent data-fetch layer is factored out the same way. ### Path parameters | Name | Type | Description | | --- | --- | --- | | id | integer | Lumify event ID. | **Request** ```bash curl https://lumify.ai/v1/events/8815/stats \ -H "Authorization: Bearer $LUMIFY_API_KEY" ``` **Response — club league (e.g. MLS)** ```json { "event_id": 8815, "available": true, "league_slug": "mls", "profile": "club", "neutral_site": false, "teams": { "home": { "team_id": 210, "name": "LA Galaxy", "abbreviation": "LAG", "rest_days": 6, "recent_form": { "window": 5, "results": ["W", "W", "D", "L", "W"], "goals_scored": [2, 1, 1, 0, 3], "goals_conceded": [0, 1, 1, 2, 0] }, "team_strength": { "source": "league_table_ppg", "ppg": 1.8, "games": 20 }, "venue": { "source": "home_away_ppg_split", "home_ppg": 2.1, "home_games": 12, "away_ppg": 1.5, "away_games": 12 }, "rates_l5": { "source": "event_stats_avg", "window": "l5", "window_size": 5, "games": 5, "shots_for": 14.2, "shots_against": 11.0, "shots_on_target_for": 5.1, "shots_on_target_against": 4.0, "possession_pct": 52.3, "corners_for": 5.4, "corners_against": 4.2, "fouls": 11.0, "yellow_cards": 2.1, "red_cards": 0.1, "passes": 420.0, "pass_accuracy_pct": 84.1, "saves": 3.2, "save_rate": 0.71, "field_games": { /* per-field sample size, e.g. "possession_pct": 4 */ } }, "rates_season": { /* same fields; window: "season", no window_size */ } }, "away": { /* same shape as "home" */ } }, "windows": { "recent_form": 5, "rates_l5": 5, "rates_season": "season", "head_to_head": 10 }, "head_to_head": { "window": 10, "meetings": [ { "home_goals": 2, "away_goals": 1 }, { "home_goals": 0, "away_goals": 0 } ], "total": 2 }, "league_context": { "avg_goals_per_team": 1.35 } } ``` ### Response fields | Field | Type | Description | | --- | --- | --- | | available | boolean | false when both teams for this fixture haven't resolved yet. No charge in that case. | | profile | string | "world_cup" or "club" — which SoccerLeagueProfile this fixture uses, driving which source values appear below. | | windows | object | Explicit sample depths so agents never guess: recent_form (5), rates_l5 (5), rates_season ("season"), head_to_head (10). | | teams.{home,away}.recent_form | object | Up to the last window (5) completed results, most-recent-first. results is a list of "W"/"D"/"L"; goals_scored/goals_conceded align by index. | | teams.{home,away}.rest_days | integer \| null | Days since this team's last completed match in the same league. null if this is their first match of the competition. | | teams.{home,away}.team_strength | object | Club leagues (source: "league_table_ppg"): season table row — ppg, games, points, w/d/l, gf/ga/gd, and ordinal rank (points → GD → GF). World Cup (source: "fifa_rank"): the team's current FIFA World Ranking. | | teams.{home,away}.sos | object \| null | Club only: strength-of-schedule lite — average opponent PPG/rank across the last 5 completed league games (source: "opp_ppg_avg"). null for World Cup (no league table). | | teams.{home,away}.lineup | object | Formation + starters/bench from ESPN summary rosters when ingested (available: false until then). Each player has name, position, jersey, formation_place, espn_athlete_id. | | teams.{home,away}.venue | object | Club leagues (source: "home_away_ppg_split"): this team's own points-per-game when playing at home vs. away this season (each null until a minimum sample of completed games exists). World Cup (source: "confederation"): the team's confederation, used as a travel-distance proxy for neutral-site tournament venues. | | teams.{home,away}.rates_l5 | object | Per-game averages from the last 5 completed matches with ESPN team box scores in this league (source: "event_stats_avg"). Includes shots_for/shots_against, shots_on_target_*, possession_pct, corners_*, cards, saves, and save_rate (total saves ÷ total SoT faced). Always includes games (0 when no box scores yet; rate fields are then null) and field_games — the number of those games that actually had a value for each individual field, since a source's summary can omit one stat on an otherwise-complete game. | | teams.{home,away}.rates_season | object | Same shape as rates_l5 but averaged over the current season (club) or the full tournament (World Cup). window is "season" (no window_size). | | head_to_head | object | Up to the last window (10) meetings between these two teams in this league, most-recent-first from the current home team's perspective. | | league_context.avg_goals_per_team | number \| null | Season-to-date league-wide average goals scored per team per game. null for the World Cup (uses a fixed baseline instead) or before enough club-season games exist. | > **Tip:** Pairs with /intelligence: Every one of these raw fields is exactly what feeds the Core Signals in [GET /v1/events/{id}/intelligence](#event-intelligence) (Team Strength, Recent Form, Attack/Defense, Head-to-Head, Schedule/Rest, Venue) — that endpoint applies Lumify's scoring/weighting on top of these same numbers. Fetch both if you want the underlying data alongside Lumify's judgment. ## Get bet intelligence for an event `GET /v1/events/{id}/intelligence` Returns the full bet intelligence payload for an event. This endpoint is not cached — it always returns the latest computed data. Always returns 200 when the event exists — check available to determine whether intelligence has been computed yet. Returns 404 only if the event ID does not exist. > **Warning:** Two response shapes. bets[] comes in one of two shapes depending on the sport and league. Branch on the presence of probability versus confidence_score — never assume one shape. 1. Probability model — currently soccer / MLS only. Each bet carries a calibrated probability, a vig-free fair_price, and the components behind them (p_market, p_model, blend_w, edge, sufficiency, drivers). It carries no confidence_score, coverage, signals, or validator. See [Probability-model bet fields](#intelligence-probability-fields). 2. Points model — every other sport and league: MLB, ATP/WTA tennis, NFL, NCAAF, FIFA World Cup soccer, and club soccer leagues other than MLS. Each bet carries confidence_score, coverage, signals, validator, narrative, rationale, and attribution. See [Points-model bet fields](#intelligence-points-fields). Both shapes share bet_type, player_role, player_id, team_id, player_name, market, tier, and computed_at, plus every event-level field. > **Note:** Sports coverage: Available for MLB, ATP/WTA tennis, FIFA World Cup 2026 soccer, MLS, NFL, and NCAAF. Intelligence is computed by the Lumify analysis pipeline and updated after each run. has_recommend: null means the pipeline has not yet run for that event. NFL and NCAAF analysis is seasonal — the pipelines self-skip in the offseason, so intelligence is only present during the football season. MLB: Uses a 6-bet model. Bet tokens are ML_P1 (home), ML_P2 (away), SPREAD_P1, SPREAD_P2, OVER, UNDER. The model scores 9 core signals plus up to 4 optional supplementary signals (umpire factor, batting trend, travel fatigue, injury impact) returned in signals.signal_meta. OVER/UNDER confidence is capped at 0.80 — very_high tier is only reachable on moneyline and spread bets. Bets at extreme moneyline prices (≤ −400) are filtered out and will not appear in the response. Soccer — MLS (probability model): MLS returns the probability shape. Bet tokens are ML_HOME, ML_AWAY, ML_DRAW, SPREAD_HOME, SPREAD_AWAY, OVER, UNDER. Unlike the points model, the outcomes of a market are solved together, so ML_HOME + ML_DRAW + ML_AWAY sum to 1, as do SPREAD_HOME + SPREAD_AWAY and OVER + UNDER — they cannot contradict each other. There are no signals or confidence_score on MLS bets. Soccer — World Cup and other club leagues (points model): FIFA World Cup and club leagues other than MLS use the 3-way points model, with each token scored independently with its own confidence score, narrative, and tier. Signal keys are the same as tennis but map to soccer-specific concepts and different max points (see signal table below). Three signals are competition-aware: for the FIFA World Cup they measure FIFA Ranking Edge, Tournament Context (group vs. knockout), and Travel / Neutral site; for club leagues the same columns measure Team Strength (league table), Schedule & Rest, and Home Advantage. Human-readable labels for each signal — matching whichever competition type the fixture is — are included in the signals._labels object, so you never have to hardcode which column means what for soccer. Filter events with sport=soccer (optionally add &league=fifa_world_cup or a club-league slug such as mls). Draws are match-level. For any 3-way soccer market, ML_DRAW carries player_role, player_id, team_id, and player_name as null, exactly like OVER/UNDER — a draw is not a bet on either team. You can sum exposure by team_id across bets[] without double-counting the draw against the home side. NFL & NCAAF: Both use the same P1/P2 bet tokens as MLB — ML_P1 (home), ML_P2 (away), SPREAD_P1, SPREAD_P2, OVER, UNDER. Signals map to football-specific concepts. NFL: QB Edge, Offensive Efficiency, Defensive Strength, Situational/Weather, Recent Form, Market Odds, Research Alignment, Head-to-Head, Betting Splits. NCAAF: SP+ Power Rankings, Offensive/Defensive Efficiency, Market Odds, Research Alignment, Recent Form, Home Field/Weather, Head-to-Head, Betting Splits. Human-readable labels for each signal are included in the signals._labels object. ### Path parameters | Parameter | Type | | Description | | --- | --- | --- | --- | | id | integer | required | Lumify event ID. Returns 404 if the event doesn't exist. If the event exists but intelligence has not been computed yet, returns 200 with available: false and empty bets array. | **Request** ```bash curl https://lumify.ai/v1/events/4821/intelligence \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response** ```json { "event_id": 4821, "available": true, // false when the pipeline hasn't run yet "players": { // maps bet-token roles to players/teams "player_1": { "name": "Jacob Fearnley", "player_id": 44, "team_id": null }, "player_2": { "name": "Giovanni Mpetshi Perricard", "player_id": 51, "team_id": null } }, "has_recommend": true, // false = all bets avoided; null = pipeline hasn't run "analyst_take": "Expert consensus leans toward Fearnley given his dominant clay-court form and 3-1 H2H advantage. The market has installed him as a moderate favorite at -185, though prevailing sharp money has shown some interest on the underdog at +155. The narrow spread pricing suggests bookmakers expect a competitive match.", "match_overview": null, // populated instead of analyst_take when no bets are recommended "intelligence_updated_at": "2026-05-13T05:09:50Z", "bets": [ { "bet_type": "ML_P1", // MLB: ML_P1 (home) | ML_P2 (away) | SPREAD_P1 | SPREAD_P2 | OVER | UNDER // tennis: ML_P1 | ML_P2 | SPREAD_P1 | SPREAD_P2 | OVER | UNDER // soccer: ML_HOME | ML_AWAY | ML_DRAW | SPREAD_HOME | SPREAD_AWAY | OVER | UNDER "player_role": "player_1", // matches participants[].role in GET /v1/events/{id}; null for OVER/UNDER "player_id": 44, // permanent player ID — matches participants[].player.id; null for OVER/UNDER "team_id": null, // set for team sports (MLB etc.); null for player sports (tennis) "player_name": "Jacob Fearnley", "tier": "moderate", // very_high | strong | moderate | avoid "confidence_score": 0.646, // 0.0–1.0, after validator adjustment "confidence_score_pre_validator": 0.652, // pre-validator score; null if validator hasn't run "coverage": 1.0, // fraction of 100-pt model that could be scored "market": { "price": -185, // American odds; null if no market data "line": null // spread or total line; null for moneyline bets }, "signals": { "signal_research": 20, // Research Alignment — max 22 pts (all sports) "signal_surface": 12, // Surface Performance (tennis, max 15) | Tournament Context (soccer WC, max 8) | Schedule Context (soccer club, max 8) "signal_serve_rtn": 8, // Serve / Return Edge (tennis, max 12) | Attack / Defense Edge (soccer, max 12) "signal_form": 10, // Recent Form L10 (tennis, max 12) | Recent Form (soccer, max 15) "signal_surface_frm":6, // Surface Form L10 (tennis, max 8) | Travel / Neutral (soccer WC, max 5) | Home Advantage (soccer club, max 5) "signal_market_odds":4, // Market Odds Value — max 8 pts (all sports) "signal_fatigue": 6, // Fatigue / Context (tennis, max 8) | Goal Environment (soccer O/U only, max 8; null on ML/spread/draw) "signal_h2h": 5, // Head-to-Head (tennis, max 5) | Head-to-Head (soccer, max 10) "signal_ranking": 8, // Ranking Differential (tennis, max 10) — omitted for MLB; FIFA Ranking Edge (soccer WC, max 15) | Team Strength/Table (soccer club, max 15) // signal_splits omitted for tennis (Owls doesn't support tennis splits) "_earned_pts": 84, // sum of all non-null signal values "_max_pts": 108 // max possible given available data (differs by sport) }, "validator": { "stance": "neutral", // validate | neutral | invalidate "confidence": "medium", // conviction in the stance: high | medium | low "delta": -0.006, // adjustment applied to pre-validator score "validated_at": "2026-05-13T05:09:43Z" }, // null if validator hasn't run for this bet "narrative": "Fearnley enters this match having won 7 of his last 10 clay-court matches and holds a commanding H2H edge. The moneyline at -185 reflects his status as a significant favorite, and his superior surface form justifies the price.", "rationale": [// structured bullets derived from signal scores — agent-parseable "Strong Research Alignment (20/22)", "Strong Surface Performance (12/15)", "Moderate Serve / Return Edge (8/12)", "Strong Recent Form (10/12)", "Strong Head-to-Head (5/5)", "Deep Research returns no strong signal" ], "attribution": [// data sources that contributed meaningful signal "research_alignment", "surface_stats", "serve_return", "recent_form", "head_to_head", "deep_research" ], "computed_at": "2026-05-13T03:41:17Z" } // … ML_P2, SPREAD_P1, SPREAD_P2, OVER, UNDER entries follow (MLB / tennis) … // … ML_AWAY, ML_DRAW, SPREAD_HOME, SPREAD_AWAY, OVER, UNDER entries follow (soccer) … ] } ``` ### Top-level fields | Field | Type | Description | | --- | --- | --- | | available | boolean | false when the analysis pipeline has not yet run for this event. All other fields will be empty/null — check this first before reading bets. Not charged when false. | | sport | string \| null | Sport slug for this event. Together with league, tells you which of the two bets[] shapes to expect. | | league | string \| null | League slug for this event, if any — e.g. mls, fifa_world_cup. | | odds_source | string \| null | Bookmaker the bets[].market prices came from. For the probability model this is the book the assessment was actually priced against, not a live overlay — per-bet market.book is authoritative if the two ever differ. | | players | object | Participant identification keyed by role — {role: {name, player_id, team_id}}, where role is player_1/player_2 or home/away. Cross-reference with participants[] in GET /v1/events/{id}. | | has_recommend | boolean \| null | true if at least one bet meets the recommendation threshold; false if none do; null if the pipeline hasn't run yet. A recommendation requires an edge, so this is false for any probability-model event whose bets are all market-anchored (blend_w of 0). | | intelligence_updated_at | string \| null | ISO-8601 UTC timestamp of the most recent change anywhere in this payload — the maximum of the per-bet computed_at values. Individual markets are only rewritten when they move, so use the per-bet computed_at when reasoning about one specific bet. | | analyst_take | string \| null | Event-level narrative summarising expert consensus and market sentiment. Present when at least one bet is recommended. | | match_overview | string \| null | Short overview used when no bets are recommended. Explains why no clear edge exists. Mutually exclusive with analyst_take. | | bets | array | One entry per scored bet token. MLB / tennis order: ML_P1 → ML_P2 → SPREAD_P1 → SPREAD_P2 → OVER → UNDER. Soccer order: ML_HOME → ML_AWAY → ML_DRAW → SPREAD_HOME → SPREAD_AWAY → OVER → UNDER. | | matchup | object \| null | MLB only. Starting pitcher matchup derived from the research pipeline. Contains home_starter and away_starter objects, each with name (string), hand (pitching hand, if known), era (ERA, if available), and confirmed (boolean — whether the starter is confirmed vs. projected). null when research data is unavailable or for non-MLB events. | ### Probability-model bet fields Returned for **soccer / MLS**. These bets carry a calibrated probability and a vig-free fair price instead of a points score. The components are exposed deliberately, so you can rebuild the published number yourself, apply your own model weight, or ignore our model entirely and consume the de-vigged market alone. **Response (MLS, one bet shown)** ```json { "event_id": 11632, "available": true, "sport": "soccer", "league": "mls", "odds_source": "pinnacle", "players": { "home": { "name": "D.C. United", "player_id": null, "team_id": 858 }, "away": { "name": "Nashville SC", "player_id": null, "team_id": 843 } }, "has_recommend": false, // no edge yet on this league → nothing to recommend "bets": [ { "bet_type": "ML_HOME", "player_role": "home", "team_id": 858, "player_name": "D.C. United", "probability": 0.31903, // calibrated; ML_HOME + ML_DRAW + ML_AWAY sum to 1 "interval": [0.2631, 0.37496], "p_market": 0.31903, // de-vigged market price "p_model": null, // no fitted model cleared for this league yet "blend_w": 0, // 0 → probability is purely the de-vigged market "fair_price": 213, // vig-free line implied by probability "market": { "price": 199, "line": null, "book": "pinnacle" }, "edge": null, // null while blend_w is 0 — see note below "sufficiency": 0.6, "tier": null, // null whenever edge is null "phase": "quant", "model_version": "market_anchor", "drivers": [], "alignment": null, "computed_at": "2026-07-27T10:07:20Z" } // … ML_AWAY, ML_DRAW, SPREAD_HOME, SPREAD_AWAY, OVER, UNDER follow … ] } ``` | Field | Type | Description | | --- | --- | --- | | probability | number \| null | Published probability for this outcome, 0–1. Equals p_market when blend_w is 0; otherwise the blend of p_model and p_market at that weight. The outcomes of a market are solved together and sum to 1. | | p_market | number \| null | De-vigged market probability. The bookmaker's price with the margin removed across the whole market, so outcomes sum to 1. This is not the same as implied probability from a single price and cannot be recomputed from market.price alone. null when the fixture is unpriced. | | p_model | number \| null | Our fitted model's own probability, before blending. null for any league with no model yet cleared for publication — currently every soccer league. | | blend_w | number \| null | Weight applied to p_model when blending with p_market, 0–1. 0 means the published probability is purely the de-vigged market. Weight is enabled per league and per bet token, and only where the model beat the market out-of-sample — so tokens on the same event can carry different weights. | | fair_price | integer \| null | American-odds fair price implied by probability — the vig-free line. Comparing it to market.price gives the book's margin on this side (above: +213 fair vs +199 offered). | | edge | number \| null | Expected profit per 1 unit staked at market.price, i.e. probability × decimal_odds − 1. Positive means the price pays more than the probability justifies. null whenever blend_w is 0: a probability taken from the market has no honest edge against the price it came from, so reporting one would just restate the vig. | | interval | number[] \| null | [lo, hi] band around probability. Read it as how much evidence backs this number, not as a statistical confidence interval — its width is driven by sufficiency and its constants are calibrated against realised outcomes rather than derived analytically. While blend_w is 0 the band reflects how mature and well-traded the quoted line is, so a freshly-opened line gets a wider band than a heavily-traded one. | | sufficiency | number \| null | How much evidence backs this assessment, 0–1. Sets interval width and caps tier. Its inputs depend on what is being measured: while blend_w is 0 it measures the quoted line's maturity (how many times it has moved on the priced book, and hours since that book's first quote), because there is no model sample to be thin about. Once a model carries weight it reflects the model's own sample depth. Thin evidence widens the interval rather than removing the response. | | drivers | object[] | Named, signed contributions to probability: {id, input, effect, direction}, where effect is the probability shift attributed to that factor and direction is up/down/neutral. This is the probability-model equivalent of signals. Empty whenever no model contributed to the bet. | | phase | string \| null | quant when the assessment is purely deterministic (ratings, market, and model math). full once a qualitative overlay — lineups, injuries, research alignment — is attached, which is the only case where alignment is populated. | | alignment | object \| null | Qualitative-overlay agreement detail. Populated only when phase is full; null for every quant assessment. | | model_version | string \| null | Identifier of the parameter set that produced this assessment, so a published number can be traced to the version that made it. market_anchor means no fitted model contributed. Reported per bet, since model weight is enabled per token. | | tier | string \| null | very_high, strong, moderate, or avoid. null whenever edge is null — a tier ranks a bet against its price, so there is nothing to rank without an edge. | | market.book | string \| null | Bookmaker this bet's price/line came from, and the book probability, fair_price, and edge were computed against. Always set for the probability model. The ?bookmaker= parameter does not apply to this shape. | | computed_at | string \| null | ISO-8601 UTC time this bet's numbers last materially changed — not when they were last checked. The publisher runs on a schedule but only rewrites a bet when its price, line, or probability moves beyond a tolerance, so an older timestamp means "unchanged since", not "stale". Bets on the same event legitimately differ here because each market moves independently. | > **Tip:** Reading a market-anchored event. When blend_w is 0 across the payload, treat it as a fair-price reference, not a set of picks: probability and fair_price tell you what the market thinks with the vig stripped out, which is the input most agents want for their own expected-value math. edge, tier, and drivers populate per league and per token as models clear out-of-sample validation, so code against them now and they will fill in without a response-shape change. ### Points-model bet fields Returned for MLB, tennis, NFL, NCAAF, FIFA World Cup soccer, and club soccer leagues other than MLS. The response example above this table shows this shape. | Field | Type | Description | | --- | --- | --- | | bet_type | string | MLB: ML_P1 (home), ML_P2 (away), SPREAD_P1, SPREAD_P2, OVER, UNDER. Tennis: ML_P1, ML_P2, SPREAD_P1, SPREAD_P2, OVER, UNDER. Soccer: ML_HOME, ML_AWAY, ML_DRAW, SPREAD_HOME, SPREAD_AWAY, OVER, UNDER. Soccer moneyline is a 3-way market — ML_HOME, ML_AWAY, and ML_DRAW are all scored independently. | | player_role | string \| null | player_1/player_2 or home/away — matches participants[].role in GET /v1/events/{id}. null for match-level tokens: OVER, UNDER, and ML_DRAW. | | player_id | integer \| null | Permanent player identity — matches participants[].player.id in GET /v1/events/{id}. Stable across all events. null for team sports and for match-level tokens (OVER, UNDER, ML_DRAW). | | team_id | integer \| null | Permanent team identity for team-sport bets (MLB, soccer, NFL etc). null for player-sport bets (tennis) and for match-level tokens (OVER, UNDER, ML_DRAW) — a draw is not a bet on either team, so summing exposure by team_id never double-counts it. | | tier | string \| null | Recommendation tier: very_high, strong, moderate, avoid | | confidence_score | number | Final confidence (0.0–1.0) after validator adjustment | | confidence_score_pre_validator | number \| null | Confidence score before the deep-research validator ran. null if the validator has not yet processed this bet. Useful for measuring validator impact. | | coverage | number | Signal coverage ratio (0.0–1.0). 1.0 = all applicable signals had sufficient data. | | market.price | integer \| null | American odds, e.g. -185, +155 | | market.line | number \| null | Spread or total line, e.g. -3.5, 22.5. null for moneyline bets. | | signals.signal_* | integer \| null | Individual signal scores. null when data was unavailable for that signal. Signal keys are shared across sports but map to different concepts and max points depending on the sport. MLB max pts: signal_surface 25 (Starting Pitching), signal_serve_rtn 15 (Bullpen), signal_form 15 (Lineup/OPS), signal_market_odds 10 (Market Odds), signal_research 20 (Research Alignment), signal_splits 8 (Sharp Money), signal_fatigue 8 (Recent Form), signal_surface_frm 7 (Park & Weather), signal_h2h 5 (H2H). Tennis max pts: signal_research 22, signal_surface 15, signal_serve_rtn 12, signal_form 12, signal_surface_frm 8, signal_market_odds 8, signal_fatigue 8, signal_h2h 5, signal_ranking 10. (signal_splits is omitted from tennis responses — Owls Insight does not support tennis splits.) Soccer max pts: signal_research 22 (Research Alignment), signal_ranking 15 (World Cup: FIFA Ranking Edge; club leagues: Team Strength / league table), signal_form 15 (Recent Form), signal_serve_rtn 12 (Attack/Defense Edge), signal_h2h 10 (Head-to-Head), signal_surface 8 (World Cup: Tournament Context; club leagues: Schedule & Rest), signal_market_odds 8, signal_fatigue 8 (Goal Environment — OVER/UNDER only; null on ML/spread/draw), signal_surface_frm 5 (World Cup: Travel/Neutral; club leagues: Home Advantage), signal_splits 8. | | signals.signal_meta | object \| null | MLB only. JSON object containing optional supplementary signal scores that did not fit the shared signal columns. Keys: umpire (0–5, home plate umpire run-environment factor), batting_trend (0–3, L15 OPS vs season delta), travel (0–3, time-zone fatigue), injury (0–3, IL key-position player differential). Each key is omitted when data was unavailable. null for all non-MLB sports. | | signals._labels | object | Present for NFL, NCAAF, and soccer only — omitted entirely for tennis and MLB. Maps every signal_* key present on this bet to its human-readable, sport-specific label (e.g. signal_serve_rtn → "Attack / Defense Edge" for soccer). For soccer this already reflects the fixture's competition type (World Cup vs. club-league labels), so you can render or reason about signals without hardcoding the column-name → concept mapping yourself. | | signals._earned_pts | integer | Sum of all non-null signal values for this bet | | signals._max_pts | integer | Maximum possible points given available signal data. | | validator.stance | string \| null | validate, neutral, or invalidate | | validator.confidence | string \| null | Conviction level of the validator stance: high, medium, or low | | validator.delta | number \| null | Confidence adjustment applied by the validator. Positive = boost, negative = reduction. | | narrative | string \| null | LLM-generated bet rationale written for human consumption. Typically present for recommended bets; null for avoid tier. | | rationale | string[] | Structured list of signal-derived bullets, designed for agent consumption. Each entry describes a signal's contribution, e.g. "Strong Research Alignment (20/22)". Always present — contains at least one entry. | | attribution | string[] | List of data-source keys that contributed meaningful signal to this bet, e.g. ["research_alignment", "surface_stats", "deep_research"]. Useful for agents reasoning about signal provenance. | > **Tip:** Workflow tip: Call /v1/events to list events — use sport=mlb&status=scheduled for MLB, sport=tennis&status=scheduled for tennis, sport=soccer&league=fifa_world_cup&status=scheduled for World Cup matches, sport=soccer&league=mls for MLS, or sport=nfl / sport=ncaaf during football season. Then fetch /v1/events/{id}/intelligence for any game you want analysis on. Points-model events: check has_recommend first — if false, read match_overview; if true, look for bets where tier is not "avoid" and read narrative + analyst_take. For MLB, ML_P1 is always the home team. Probability-model events (MLS): ignore has_recommend until edge is populated. Compare fair_price to market.price to see the book's margin, use p_market as the de-vigged probability input to your own expected-value math, and use sufficiency to decide how much to trust a thin, freshly-opened line. All three moneyline outcomes sum to 1, so you can renormalise or compare across markets safely. ## Get betting splits for an event `GET /v1/events/{id}/splits` Returns public betting split data for an event — the percentage of bets and handle wagered on each side across moneyline, spread, and total markets. Includes a consensus (average across books) and a per-bookmaker breakdown. Updated every ~30 minutes. Always returns 200 when the event exists — check available to determine whether splits have been ingested yet. Returns 404 only if the event ID does not exist. > **Note:** Sports coverage: Available for MLB, NBA, NHL, and NFL (during their respective seasons). Tennis, soccer, and NCAAF splits are not available on the Owls Insight v1 API. Soccer odds are available via [/v1/events/{id}/odds](#event-odds). Splits are only ingested for pre-game events; data is not updated once a game starts. ### Path parameters | Name | Type | Description | | --- | --- | --- | | id | integer | Lumify event ID. | **Request** ```bash curl https://lumify.ai/v1/events/479/splits \ -H "Authorization: Bearer $LUMIFY_API_KEY" ``` **Response** ```json { "event_id": 479, "captured_at": "2026-05-17T14:15:12Z", "consensus": { "moneyline": { "home": { "bets_pct": 92, "handle_pct": 96, "price": -149 }, "away": { "bets_pct": 8, "handle_pct": 4, "price": 123 } }, "spread": { "home": { "bets_pct": 87, "handle_pct": 98, "line": -1.5 }, "away": { "bets_pct": 13, "handle_pct": 2, "line": 1.5 } }, "total": { "over": { "bets_pct": 78, "handle_pct": 81, "line": 7.0 }, "under": { "bets_pct": 22, "handle_pct": 19, "line": 7.0 } } }, "books": [ { "book": "dk", "name": "DraftKings", "moneyline": { "home": { "bets_pct": 83, "handle_pct": 93, "price": -149 }, "away": { "bets_pct": 17, "handle_pct": 7, "price": 123 } }, "spread": { "home": { "bets_pct": 74, "handle_pct": 95, "line": -1.5 }, "away": { "bets_pct": 26, "handle_pct": 5, "line": 1.5 } }, "total": { "over": { "bets_pct": 56, "handle_pct": 62, "line": 7.0 }, "under": { "bets_pct": 44, "handle_pct": 38, "line": 7.0 } } } ] } ``` ### Response fields | Field | Type | Description | | --- | --- | --- | | available | boolean | false when no splits have been ingested yet. consensus will be an empty object and books an empty array. | | captured_at | string \| null | UTC timestamp of the most recent ingest cycle for this event's splits data. null when available is false. | | consensus | object | Market averages across all available books. Contains moneyline, spread, and total objects. | | consensus[market][side].bets_pct | integer | Percentage of total bets placed on this side (0–100). Opposite sides sum to ~100. | | consensus[market][side].handle_pct | integer | Percentage of total money wagered on this side (0–100). A large gap between handle_pct and bets_pct indicates sharp (large-bet) money diverging from public action. | | consensus[market][side].price | integer \| null | American odds for this side. Present on moneyline; null on spread/total sides. | | consensus[market][side].line | number \| null | Spread or total line. Present on spread/total; null on moneyline. | | books | array | Per-bookmaker breakdown. Same structure as consensus but for a single book. book is the bookmaker key (e.g. dk); name is the display name. | > **Tip:** Sharp-money signal: When handle_pct significantly exceeds bets_pct on a side (typically ≥20pp gap), it indicates that a small number of large bets — characteristic of sharp bettors — are backing that side against the public. Use this in combination with /v1/events/{id}/intelligence for context on line movement. ### Teams Team profiles with league, conference, division, and home venue — first-class so agents do not need to derive teams from event participants. [MCP: list_teams, get_team →](/docs/guides#mcp) ## List teams `GET /v1/teams` Returns a paginated list of teams. Filter by sport, league, conference, division, country, name search, and active status. Results are sorted by ascending team ID. ### Query parameters | Parameter | Type | | Default | Description | | --- | --- | --- | --- | --- | | sport | string | optional | — | Sport slug, e.g. nba, nhl, nfl. | | league | string | optional | — | League slug, e.g. nba. | | conference | string | optional | — | Conference filter, e.g. Eastern. | | division | string | optional | — | Division filter, e.g. Atlantic. | | country | string | optional | — | ISO country code, e.g. USA. | | q | string | optional | — | Partial team-name search. | | active | boolean | optional | — | Filter by active status. Omit to return both active and inactive teams. | | after_id | integer | optional | — | Pagination cursor from next_after_id. | | limit | integer | optional | 25 | Page size, range 1–100. | **Request** ```bash # Eastern Conference NBA teams curl "https://lumify.ai/v1/teams?sport=nba&conference=Eastern" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response** ```json { "data": [ { "id": 1, "slug": "boston-celtics", "name": "Boston Celtics", "abbreviation": "BOS", "sport": "nba", "league": "nba", "conference": "Eastern", "division": "Atlantic", "venue": { "id": 1, "name": "TD Garden", "city": "Boston" }, "is_active": true } ], "has_more": false, "next_after_id": null } ``` ## Get a team `GET /v1/teams/{id}` Returns a single team's profile, including home venue when linked. Returns 404 if the team does not exist. ### Path parameters | Name | Type | Description | | --- | --- | --- | | id | integer | Lumify team ID. | **Request** ```bash curl "https://lumify.ai/v1/teams/1" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response** ```json { "id": 1, "slug": "boston-celtics", "name": "Boston Celtics", "short_name": "Celtics", "abbreviation": "BOS", "sport": "nba", "league": "nba", "city": "Boston", "state": "MA", "country_code": "USA", "conference": "Eastern", "division": "Atlantic", "venue": { "id": 1, "name": "TD Garden", "city": "Boston" }, "is_active": true } ``` ### Players Player profiles, rankings, and event history across all supported sports. [MCP: search_players, get_player, get_player_events →](/docs/guides#mcp) ## List players `GET /v1/players` Returns a paginated list of players. Supports filtering by sport, country, name search, active status, and ranking. Results are sorted by ascending player ID. ### Query parameters | Parameter | Type | | Default | Description | | --- | --- | --- | --- | --- | | sport | string | optional | — | Filter by sport slug: tennis, mlb, nfl, etc. | | q | string | optional | — | Partial name search. Case-insensitive match against full_name (e.g. ?q=sinner). | | country | string | optional | — | ISO 3166-1 alpha-3 country code (e.g. USA, ITA, GBR). Case-insensitive. | | active | boolean | optional | — | Pass true for active players only, false for retired. | | ranked | boolean | optional | — | If true, returns only players with a current ATP/WTA ranking. Useful for tennis leaderboard use cases. | | after_id | integer | optional | — | Pagination cursor. Pass next_after_id from the previous response. | | limit | integer | optional | 25 | Page size. Range: 1–100. | **Request** ```bash # Top-ranked tennis players curl "https://lumify.ai/v1/players?sport=tennis&ranked=true&limit=10" \ -H "Authorization: Bearer YOUR_API_KEY" # Search by name curl "https://lumify.ai/v1/players?q=sinner" \ -H "Authorization: Bearer YOUR_API_KEY" # All active MLB players curl "https://lumify.ai/v1/players?sport=mlb&active=true&limit=100" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response** ```json { "data": [ { "id": 1, "slug": "jannik-sinner", "full_name": "Jannik Sinner", "first_name": "Jannik", "last_name": "Sinner", "sport": "tennis", "country_code": "ITA", "birthdate": null, "position": null, // e.g. "P", "SS", "CF" for MLB "handedness": null, // left | right | switch (tennis) "height_cm": null, "weight_kg": null, "tennis_ranking": 1, "tennis_ranking_points": 14350, "current_team_id": null, "current_team_name": null, "is_active": true, "retired_at": null, "image_url": null // lumify.ai/media/players/tennis/{hash}.png when available } ], "has_more": true, "next_after_id": 25 // null on the last page } ``` ## Get a player `GET /v1/players/{id}` Returns a single player's full profile. Returns 404 if the player does not exist. ### Path parameters | Name | Type | Description | | --- | --- | --- | | id | integer | Lumify player ID. | **Request** ```bash curl "https://lumify.ai/v1/players/1" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response** ```json { "id": 1, "slug": "jannik-sinner", "full_name": "Jannik Sinner", "first_name": "Jannik", "last_name": "Sinner", "sport": "tennis", "country_code": "ITA", "birthdate": null, "position": null, "handedness": null, "height_cm": null, "weight_kg": null, "tennis_ranking": 1, "tennis_ranking_points": 14350, "current_team_id": null, "current_team_name": null, "is_active": true, "retired_at": null, "image_url": null // lumify.ai/media/players/tennis/{hash}.png when available } ``` ## List a player's events `GET /v1/players/{id}/events` Returns events a player has participated in or is scheduled to play. Defaults to a ±30-day window around today. Results are sorted by starts_at DESC (most recent first). ### Path parameters | Name | Type | Description | | --- | --- | --- | | id | integer | Lumify player ID. | ### Query parameters | Parameter | Type | | Default | Description | | --- | --- | --- | --- | --- | | status | string | optional | — | Filter by event status: scheduled, inprogress, final, etc. | | from | string | optional | today −30d | Start date (UTC, inclusive). Format: YYYY-MM-DD. | | to | string | optional | today +30d | End date (UTC, inclusive). Max range: 90 days. | | after_id | integer | optional | — | Pagination cursor. | | limit | integer | optional | 25 | Page size. Range: 1–100. | **Request** ```bash # Sinner's upcoming matches curl "https://lumify.ai/v1/players/1/events?status=scheduled" \ -H "Authorization: Bearer YOUR_API_KEY" # Recent results (last 30 days) curl "https://lumify.ai/v1/players/1/events?status=final" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response** ```json { "player_id": 1, "data": [ { "event_id": 1100, "starts_at": "2026-05-17T15:00:00Z", "status": "scheduled", "sport": "tennis", "competition": "ATP Rome", "venue": null, "venue_city": null, "role": "player_1", // player_1 | player_2 | home | away | single "result": null, // "win" | "loss" | null (pre-game) "score": null, "opponent": { "player_id": 1012, "name": "Casper Ruud" } } ], "has_more": false, "next_after_id": null } ``` ### Odds & Lines Current moneyline, spread, and total lines per bookmaker, plus line movement history. [MCP: get_odds, get_odds_history →](/docs/guides#mcp) [Guide: Track live odds movement →](/docs/guides#recipe-odds-movement) ## Get current odds for an event `GET /v1/events/{id}/odds` Returns the current moneyline, spread, and total lines for an event. Defaults to **Pinnacle only (1 credit)**. Use bookmaker=all or a comma-separated list to fetch multiple books at **2 credits**. Data is updated every ~30 minutes and cached for 2 minutes. Always returns 200 when the event exists — check available to determine whether odds have been ingested yet. Returns 404 only if the event ID does not exist. > **Note:** Soccer (FIFA World Cup): Filter events with league=fifa_world_cup. Soccer moneyline (h2h) is a 3-way market — outcomes include both team names plus Draw. Asian handicap spreads use goal lines (e.g. -0.5, +0.5). Odds are ingested for all known-team fixtures during the tournament window; TBD knockout placeholders (e.g. "Round of 32 Winner") will return available: false until teams are determined. ### Path parameters | Name | Type | Description | | --- | --- | --- | | id | integer | Lumify event ID. | ### Query parameters | Name | Type | Default | Description | | --- | --- | --- | --- | | bookmaker | string | pinnacle | Bookmaker filter. Accepted values: pinnacle, fanduel, draftkings, betmgm, caesars, bet365, circa, hardrock, betonline, all, or a comma-separated combination (e.g. fanduel,betmgm). 1 credit for a single bookmaker · 2 credits for multiple or all. | **Request — Pinnacle only (default, 1 credit)** ```bash curl https://lumify.ai/v1/events/4821/odds \ -H "Authorization: Bearer $LUMIFY_API_KEY" ``` **Request — all bookmakers (2 credits)** ```bash curl "https://lumify.ai/v1/events/4821/odds?bookmaker=all" \ -H "Authorization: Bearer $LUMIFY_API_KEY" ``` **Response** ```json { "event_id": 4821, "bookmakers": [ { "bookmaker": "pinnacle", "markets": [ { "key": "h2h", "label": "moneyline", "outcomes": [ { "outcome": "Jannik Sinner", "price": -280, "point": null }, { "outcome": "Carlos Alcaraz", "price": 230, "point": null } ] }, { "key": "spreads", "label": "spread", "outcomes": [ { "outcome": "Jannik Sinner", "price": -110, "point": -2.5 }, { "outcome": "Carlos Alcaraz", "price": -110, "point": 2.5 } ] }, { "key": "totals", "label": "totals", "outcomes": [ { "outcome": "Over", "price": -110, "point": 22.5 }, { "outcome": "Under", "price": -110, "point": 22.5 } ] } ], "captured_at": "2026-05-13T18:32:00Z" } ], "last_updated": "2026-05-13T18:32:00Z" } ``` ### Response fields | Field | Type | Description | | --- | --- | --- | | available | boolean | false when no odds have been ingested yet. bookmakers will be an empty array. | | bookmakers | array | One entry per bookmaker with odds data. | | bookmaker | string | Bookmaker key, e.g. pinnacle, draftkings, betmgm. Supported odds books: pinnacle, fanduel, draftkings, betmgm, caesars, bet365, circa, hardrock, betonline. Availability per event depends on what has been ingested. | | markets | array | Markets for this bookmaker, ordered: moneyline → spread → totals. | | key | string | Market key: h2h, spreads, or totals. | | label | string | Human-readable label: moneyline, spread, or totals. | | outcomes | array | Each outcome has outcome (name), price (American odds integer), and point (spread/total line; null for moneyline). Soccer h2h includes three outcomes: home team, away team, and Draw. | | captured_at | string | UTC timestamp when this bookmaker's odds were last ingested. | | last_updated | string | Most recent captured_at across all bookmakers. | ## Get odds movement history `GET /v1/events/{id}/odds/history` Returns all recorded line movements for an event — any time a price or point changed between ingest cycles. Defaults to **Pinnacle only (1 credit)**. Use bookmaker=all or a comma-separated list for multiple books at **2 credits**. Ordered newest-first. Useful for detecting sharp line movement. Not cached. Returns 404 if no movement has been recorded. ### Path parameters | Name | Type | Description | | --- | --- | --- | | id | integer | Lumify event ID. | ### Query parameters | Name | Type | Default | Description | | --- | --- | --- | --- | | bookmaker | string | pinnacle | Bookmaker filter. Accepted values: pinnacle, fanduel, draftkings, betmgm, caesars, bet365, circa, hardrock, betonline, all, or a comma-separated combination. 1 credit for a single bookmaker · 2 credits for multiple or all. | | limit | integer | 50 | Max movements to return (1–200). | **Response** ```json { "event_id": 4821, "movements": [ { "bookmaker": "pinnacle", "market": "moneyline", "market_key": "h2h", "outcome": "Jannik Sinner", "price_from": -250, "price_to": -280, "point_from": null, "point_to": null, "moved_at": "2026-05-13T15:02:00Z" } ], "total": 1 } ``` ### Push & Streaming Server-Sent Events and webhook subscriptions — remove the need to poll for live score changes. [Guide: Track live odds movement →](/docs/guides#recipe-odds-movement) ## Stream live score updates (SSE) `GET /v1/events/{id}/stream` Opens a text/event-stream connection that emits an event: score message only when the score, status, or clock changes — plus a keep-alive comment every 15 seconds. The stream closes when the event finishes (event: done) or after 5 minutes, whichever comes first. Use this instead of polling [/v1/events/{id}/score](#event-score) when you want push-based updates. > **Note:** Auth for EventSource clients. Browser EventSource cannot set custom headers, so this endpoint also accepts the key as ?api_key=lmfy-... in addition to the standard Authorization: Bearer header. > **Tip:** Reconnecting across the 5-minute cap. If the connection closes because the max duration elapsed (not because the game finished), the server sends event: reconnect first so you can tell the two apart — open a fresh connection to the same URL to keep watching. Both SDKs' stream helpers (streamScores() in TypeScript, client.events.stream() in Python) already do this automatically, so a long game looks like one continuous stream — no reconnect logic to write yourself. ### Path parameters | Parameter | Type | | Description | | --- | --- | --- | --- | | id | integer | required | Lumify event ID. Returns an event: error message if the event does not exist. | **Request** ```bash curl -N "https://lumify.ai/v1/events/4812/stream" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response** ```text event: score data: {"event_id": 4812, "status": "inprogress", "period": "3", "clock": "8:42", "scores": [...] , "updated_at": "2026-05-10T01:18:44Z"} : keep-alive event: done data: {"event_id": 4812} // or, if the 5-minute cap is hit before the game finishes: event: reconnect data: {"event_id": 4812, "reason": "max_stream_duration", "max_seconds": 300} ``` > **Warning:** Concurrency limit. Each API key may hold a limited number of concurrent streams. Exceeding it returns 429 with error.code: "stream_limit_exceeded" — close an existing stream and retry. ## Manage webhook subscriptions Webhooks push score changes, status transitions, and line moves to your own endpoint — no polling or open connections required. Delivery is performed by the ingest pipeline; each payload is signed with the subscription's signing_secret (HMAC-SHA256) so you can verify authenticity. Deliveries that fail transiently (5xx, 429, or a timeout) are automatically retried with exponential backoff (30s / 5m / 30m / 2h / 6h) — see [delivery history](#webhook-deliveries) below. ### Create a subscription `POST /v1/webhooks` | Field | Type | | Description | | --- | --- | --- | --- | | url | string | required | HTTPS endpoint to receive deliveries. Rejected if it resolves to a private/internal address. | | event_types | string[] | optional | One or more of score, status, line_move, intelligence. Defaults to ["score", "status"]. | | sport | string | optional | Restrict to one sport slug. Omit to subscribe across sports. | | event_id | integer | optional | Restrict to a single event. Omit to subscribe to all matching events. | ```bash curl -X POST https://lumify.ai/v1/webhooks \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/hooks/lumify", "event_types": ["score", "line_move"], "sport": "mlb"}' ``` ```json { "id": 42, "url": "https://example.com/hooks/lumify", "event_types": ["score", "line_move"], "sport": "mlb", "event_id": null, "signing_secret": "whsec_...", // shown once — store it to verify delivery signatures "is_active": true } ``` ### List & delete subscriptions | Method & path | Description | | --- | --- | | GET /v1/webhooks | List the caller's webhook subscriptions. | | DELETE /v1/webhooks/{id} | Delete a subscription. Returns 404 if it does not belong to the caller. | ```bash # List curl https://lumify.ai/v1/webhooks -H "Authorization: Bearer YOUR_API_KEY" # Delete curl -X DELETE https://lumify.ai/v1/webhooks/42 -H "Authorization: Bearer YOUR_API_KEY" ``` ### Delivery history & retries Every delivery attempt — including retries — is recorded and queryable per subscription, newest first. A failed attempt whose failure looks transient (5xx, 429, or a connection timeout) gets automatically retried with exponential backoff (30s → 5m → 30m → 2h → 6h, 5 retries max); other 4xx failures are not retried since the receiver is rejecting the request itself. Retries appear as their own rows linked to the attempt they retried via `parent_delivery_id`, so you can reconstruct the full chain for any event. `GET /v1/webhooks/{id}/deliveries` | Param | Type | | Description | | --- | --- | --- | --- | | after_id | integer | optional | Cursor: return deliveries with id < after_id (list is newest-first). | | limit | integer | optional | Page size, default 25, max 100. | | success | boolean | optional | Filter to successful (true, 2xx) or failed (false) deliveries. | | given_up | boolean | optional | Filter to deliveries that exhausted retries (true) or still have / had a retry path (false). | | event_type | string | optional | Filter by event type: score, status, line_move, or intelligence. | ```bash curl https://lumify.ai/v1/webhooks/42/deliveries -H "Authorization: Bearer YOUR_API_KEY" ``` ```json { "data": [ { "id": 1002, "event_type": "score", "event_id": 555, "attempt": 2, "parent_delivery_id": 1001, // the attempt this one retried "status_code": 200, "success": true, "error": null, "given_up": false, "next_retry_at": null, "delivered_at": "2026-07-23T18:05:30Z" }, { "id": 1001, "event_type": "score", "event_id": 555, "attempt": 1, "parent_delivery_id": null, "status_code": 503, "success": false, "error": null, "given_up": false, "next_retry_at": null, // cleared once the retry above ran "delivered_at": "2026-07-23T18:05:00Z" } ], "next_after_id": null } ``` ### Agent Onboarding Programmatic key and credit management under /api/agent — provision access without the browser dashboard. [Guide: Provision API access programmatically →](/docs/guides#recipe-agent-onboarding) [agent.json →](/.well-known/agent.json) ## Manage API keys Lets a builder or agent provision and manage keys without the dashboard. Authenticate with a browser session **or** an existing Lumify API key — so an agent that already has one key can mint, list, and revoke others on its own. | Method & path | Description | | --- | --- | | POST /api/agent/keys | Create a new API key. The secret value is returned only once. | | GET /api/agent/keys | List the caller's API keys (metadata only — secrets are never re-shown). | | DELETE /api/agent/keys/{id} | Revoke an API key. Returns 404 if it doesn't belong to the caller. | **Request — create a key** ```bash curl -X POST https://lumify.ai/api/agent/keys \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "prod-worker-1", "scopes": ["all"]}' ``` ```json { "id": 17, "name": "prod-worker-1", "key": "lmfy-abc123.def456...", // shown once — store it now "scopes": ["all"], "created_at": "2026-06-01T12:00:00Z" } ``` > **Warning:** Key limits are tier-based. Creating past your plan's max_api_keys returns 403 with error.code: "key_limit_reached" and an upgrade_url. ## Credits & credit packs Check balance and buy additional credits programmatically — the same prepaid rails as the dashboard, over the existing Stripe integration, with no crypto or new payment flow required. | Method & path | Description | | --- | --- | | GET /api/agent/credits | Current tier, credits used, credit limit, bonus credits, and billing period. | | GET /api/agent/credit-packs | List purchasable one-time credit packs. | | POST /api/agent/credits/topup | Purchase a credit pack by pack_id — charged off-session to the account's Stripe payment method on file. | **Request — check balance** ```bash curl https://lumify.ai/api/agent/credits \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```json { "tier": "growth", "credits_used": 4210, "credit_limit": 10000, "bonus_credits": 0, "total_remaining": 5790, "is_trial": false, "is_trial_expired": false } ``` ### Purchase a credit pack ```bash curl -X POST https://lumify.ai/api/agent/credits/topup \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"pack_id": 3}' ``` > **Warning:** Payment failures return 402 for card/payment issues (card_declined, payment_method_required) or 400 for an invalid pack_id. ### Planning Pre-call credit-cost estimates — plan spend before you spend it. [MCP: estimate_cost →](/docs/guides#mcp) ## Estimate call cost `POST /v1/estimate` Returns a credit-cost range for one or more planned calls **without making them** — for agents that want to budget before spending. Costs are data-dependent (e.g. odds/intelligence/splits not yet ingested are free), so each result is a min_credits/max_credits range, not a single number, computed by the exact same pricing rules the real endpoints use. This call itself is always free. See GET /v1/estimate/tools for the full list of supported tool names, grouped by how their cost varies. ### Request body | Field | Type | | Description | | --- | --- | --- | --- | | calls | object[] | required | 1 or more {"tool": "...", "arguments": {...}} entries — the same tool name and arguments you'd pass to the matching MCP tool or SDK method. | **Request** ```bash curl -X POST https://lumify.ai/v1/estimate \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"calls":[{"tool":"get_event","arguments":{"event_id":12345,"include_odds":true}}]}' ``` **Response** ```json { "estimates": [ { "tool": "get_event", "min_credits": 1, "max_credits": 2, "note": "Base lookup is always 1 credit; each include_* add-on only bills (+1) if that data is actually available for the event." } ], "total_min_credits": 1, "total_max_credits": 2 } ``` > **Tip:** Always free. /v1/estimate (and GET /v1/estimate/tools) report X-Credits-Used: 0 — estimating never costs a credit, even in a loop while an agent explores options. ## Error Codes All machine-facing errors (/v1/*, /mcp, /api/agent/*) use one JSON envelope. Switch on error.code. The top-level detail field mirrors error.message for backward compatibility. | Status | error.code | When it occurs | | --- | --- | --- | | 400 | bad_request | Invalid parameter — unknown status value, bad date format, date+from conflict, or date range > 90 days. | | 401 | unauthorized | Missing, malformed, invalid, inactive, or expired API key. Credit exhaustion is not a 401 — see 402. | | 402 | insufficient_credits / daily_credit_cap_exceeded | Valid key, but credits block access. Envelope includes upgrade_url and often topup_url. The free-tier daily cap is a rolling 24-hour window; daily_credit_cap_exceeded includes resets_at and window_hours. | | 403 | forbidden / sport_scope_denied | Valid key denied for this resource (e.g. sport not in key scopes). Structured extras may include sport, granted_scopes, and upgrade_url. | | 404 | not_found | The requested resource does not exist (e.g. unknown event ID). Sub-resources such as /odds, /splits, and /intelligence return 200 with available: false when data hasn't been ingested yet — 404 on those paths means the parent event ID is invalid. | | 422 | validation_error | Type validation failed — non-integer id, or limit outside 1–100. Field errors are listed under error.errors. | | 429 | rate_limit_exceeded | Rate limit exceeded. See error.retry_after and the Retry-After header. | | 500 | internal_error | Unexpected error. Retry with exponential backoff. | ### Error response shape ```json { "error": { "code": "bad_request", "message": "Invalid status 'live'. Valid values: ['cancelled', 'delayed', 'final', ...]", "status": 400, "doc_url": "https://lumify.ai/docs/reference#error-codes" }, "detail": "Invalid status 'live'. Valid values: ['cancelled', 'delayed', 'final', ...]" } ``` ## Event Status Values The status field describes the lifecycle state of an event. Passing an unrecognised value to the ?status filter returns 400. | Value | Phase | Description | | --- | --- | --- | | scheduled | Pre-game | Confirmed and scheduled; not yet started | | inprogress | Live | Currently being played | | delayed | Pre-game hold | Start pushed back but game has not begun (e.g. weather delay before first pitch) | | suspended | Mid-game halt | Play stopped after the game began (e.g. rain delay mid-inning) | | postponed | Pre-game | Moved to a different date entirely | | cancelled | Terminal | Will not be played | | final | Terminal | Concluded. Check result_type for how it ended. | | walkover | Terminal | Tennis — opponent withdrew before the match. Winner is set; no score recorded. | ### Result type values Present on final events. Describes how the outcome was reached. | Value | Sports | Description | | --- | --- | --- | | regulation | All | Decided in normal time | | overtime | NHL, NBA, NFL, Soccer (AET) | Decided in extra time or OT period | | shootout | NHL, Soccer (PEN) | Decided by penalty shootout | | retired | Tennis | Opponent retired mid-match due to injury | | walkover | Tennis | Opponent withdrew before the match | ## Sports Coverage | Sport | League slug | Type | Live scores | | --- | --- | --- | --- | | NFL | nfl | Team league | Yes | | NBA | nba | Team league | Yes | | MLB | mlb | Team league | Yes | | NHL | nhl | Team league | Yes | | NCAAF | ncaaf | Team league | Yes (in season) | | NCAAB | ncaab | Team league | Yes (in season) | | Tennis | atp, wta | Individual tour | Yes | | Soccer | fifa_world_cup | Tournament | Yes (tournament dates only) | | Soccer | mls | Team league | Yes (in season) | | Soccer | epl, la_liga, serie_a, bundesliga, ligue_1 | Team league | Yes (in season) | | Soccer | ucl | Tournament | Yes (in season) | > **Note:** All timestamps are stored and returned in UTC. Use the venue timezone field to convert to local time for display. --- # Cheat sheet Cite-back: https://lumify.ai/docs/cheat-sheet.md # Lumify API Cheat Sheet > Canonical URL: https://lumify.ai/docs/cheat-sheet > HTML twin: https://lumify.ai/docs/cheat-sheet One-page re-entry for humans and a compact context block for LLMs. ## Base URL & auth ``` Base URL: https://lumify.ai Auth: Authorization: Bearer lmfy-... Instant key (no signup): https://lumify.ai/docs/ai Persistent free tier: https://lumify.ai/register (1,000 credits) ``` ## Response envelope & headers - JSON everywhere. Cursor pagination: `?after_id=&limit=` (max 100) → `next_after_id`. - Timestamps are ISO-8601 UTC with trailing `Z`. - Every response: `X-RateLimit-{Limit,Remaining,Reset}`, `X-Credits-Used`, `X-Credits-Remaining` (when resolvable). - Odds / splits / intelligence not ready yet → `200` with `available: false` and **0 credits**. ## Credits (1-credit default) | Call | Cost | |---|---| | Standard (events, scores, odds, splits, intelligence, players) | **1 credit** | | Multi-bookmaker odds (`bookmaker=all` or a list) | **2 credits** | | Compound `include_odds` / `include_intelligence` | **+1–2 credits** | | SSE stream open | **1 credit** | | `POST /v1/estimate` | **Free** | | Errors (4xx/5xx) and `available: false` | **Free** | Rate limits (sliding 60s): Free **20**/min · PAYG **60**/min · Growth **120**/min. ## Hero query ```bash curl -s "https://lumify.ai/v1/events?sport=mlb&status=scheduled&limit=5" \ -H "Authorization: Bearer lmfy-YOUR_KEY" curl -s "https://lumify.ai/v1/events/EVENT_ID?include_odds=true&include_intelligence=true" \ -H "Authorization: Bearer lmfy-YOUR_KEY" curl -s -X POST "https://lumify.ai/v1/estimate" \ -H "Authorization: Bearer lmfy-YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"calls":[{"tool":"get_event","arguments":{"event_id":12345,"include_odds":true,"include_intelligence":true}}]}' ``` Intelligence sports today: **MLB, NFL, NCAA Football, ATP/WTA tennis, FIFA World Cup soccer, MLS**. Splits: **MLB, NBA, NHL, NFL**. ## Error shape ```json { "error": { "code": "rate_limit_exceeded", "message": "Rate limit exceeded", "status": 429, "doc_url": "https://lumify.ai/docs/reference#error-codes", "retry_after": 23 }, "detail": "Rate limit exceeded" } ``` Switch on `error.code`, not `message`. ## MCP ``` URL: https://lumify.ai/mcp Auth: Authorization: Bearer lmfy-... 18 tools — initialize / tools/list / ping free; tools/call metered like REST. ``` ## Go deeper - https://lumify.ai/llms.txt (~4.9k tokens, measured) - https://lumify.ai/llms-full.txt (~11k tokens GEO, measured) - https://lumify.ai/docs/llms-full.txt (~54k tokens technical, measured) - https://lumify.ai/openapi-llms.txt (~5.9k tokens endpoint dump, measured) - https://lumify.ai/docs/ai - https://lumify.ai/docs/best-practices - https://lumify.ai/docs/rate-limits - https://lumify.ai/changelog.json --- # Best practices Cite-back: https://lumify.ai/docs/best-practices.md # Lumify Best Practices > Canonical URL: https://lumify.ai/docs/best-practices.md > HTML twin: https://lumify.ai/docs/best-practices Patterns and anti-patterns for building on Lumify — credit budgeting with `/v1/estimate`, polling, MCP, and agent hygiene. ## Patterns (do) 1. **Estimate before you spend.** `POST /v1/estimate` (MCP `estimate_cost`) is always free and returns min/max credit ranges. 2. **Compound when you need the full picture.** `GET /v1/events/{id}?include_odds=true&include_intelligence=true` is cheaper than three separate calls. 3. **Default to Pinnacle for a single book.** Use `bookmaker=all` only when you need cross-book comparison (2 credits). 4. **Treat `available: false` as success-with-no-data.** It is free — do not retry-storm; back off until the next ingest cycle (~30 min for odds/intel). 5. **Prefer MCP tools when the client supports them.** Hosted at `https://lumify.ai/mcp` — no npx for remote clients; `_meta.credits_used` mirrors REST. 6. **Read rate-limit and credit headers every call.** `X-RateLimit-*`, `X-Credits-Used`, `X-Credits-Remaining`. 7. **Use webhooks or SSE instead of tight polling** for score/status/line_move/intelligence changes. 8. **Cursor-paginate with `after_id`** and stop when `next_after_id` is null. Max `limit=100`. 9. **Branch intelligence on shape.** Presence of `probability` → MLS probability model; `confidence_score` → points model. 10. **Filter with `has_recommend=true`** when you only want events with actionable intelligence. 11. **Keep keys server-side.** Never expose `lmfy-...` in browsers or public repos. 12. **Provision keys/credits via `/api/agent/*`** for agent-autonomous loops after the first human-issued key. ## Anti-patterns (don't) 1. **Don't put the API key in the query string** except for SSE (`?api_key=` is required there because EventSource cannot set headers). Prefer the `Authorization` header everywhere else. 2. **Don't invent endpoints, fields, or credit costs.** Read llms.txt / OpenAPI; ask the user to paste docs if you cannot fetch them. 3. **Don't poll odds faster than the ingest cadence** (~30 minutes). You will burn credits for identical payloads. 4. **Don't assume every sport has intelligence or splits.** Intelligence: MLB/NFL/NCAAF/tennis/FIFA WC/MLS. Splits: MLB/NBA/NHL/NFL. 5. **Don't combine `sort=status` with `after_id`.** It returns 400 — fetch in one page or use `sort=time`. 6. **Don't treat MLS `edges_by_book` / `best.edge` as EV.** It is a line-shopping gap vs sharp consensus, not expected value. 7. **Don't call Lumify from the end-user's browser** with a real key — proxy through your backend. 8. **Don't ignore 429 `retry_after`.** Back off; rate-limited calls cost 0 credits but still waste wall time. 9. **Don't expect ChatGPT/Claude.ai web connectors to work yet** — OAuth is not shipped; use desktop/IDE MCP clients. 10. **Don't document footguns as features.** If an API behavior surprises you, open an issue — we fix APIs rather than warn forever. ## Credit budgeting with `/v1/estimate` ```bash curl -s -X POST "https://lumify.ai/v1/estimate" \ -H "Authorization: Bearer lmfy-YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "calls": [ {"tool": "list_events", "arguments": {"sport": "mlb", "status": "scheduled", "limit": 25}}, {"tool": "get_event", "arguments": {"event_id": 12345, "include_odds": true, "include_intelligence": true}} ] }' ``` ``` daily_credits ≈ (events_tracked × polls_per_day × cost_per_poll) + webhook_or_sse_opens + intelligence_refreshes ``` Prefer webhooks/SSE for scores so polls collapse to the odds ingest cadence. Cap loops with `estimate_cost` before a burst. ## Adaptive polling - **Live scores:** ~1 minute freshness — or open SSE / register a webhook. - **Odds / splits / intelligence:** ~30 minute ingest — polling faster usually returns identical data. - Respect `updated_at` / `intelligence_updated_at` / `computed_at`. - On 429: sleep `retry_after` seconds. Full limits: https://lumify.ai/docs/rate-limits.md ## Agent hygiene - Start from https://lumify.ai/docs/ai#context - Disambiguate: Lumify (lumify.ai) ≠ LUMIFY eye drops ≠ Philips Lumify ultrasound - Poll https://lumify.ai/changelog.json for breaking changes (≥90 days notice within a major version) --- # Rate limits Cite-back: https://lumify.ai/docs/rate-limits.md # Lumify Rate Limits & Pagination > Canonical URL: https://lumify.ai/docs/rate-limits.md > HTML twin: https://lumify.ai/docs/rate-limits One page an agent can fetch for the complete picture — per-tier limits, sliding-window rules, cursor semantics, and stream caps. ## Per-tier limits Limits are enforced **per API key** on a **sliding 60-second window**. There is no separate hourly quota. | Plan | Requests / minute | Notes | |---|---|---| | Free Tier | 20 | Plus a rolling 24h anti-abuse credit spend cap | | Pay As You Go | 60 | Metered credits; no monthly credit cap | | Growth | 120 | 10,000 credits included / month | | Enterprise | Custom | Negotiated | Instant trial keys (no signup) use a tighter anonymous limit. Persistent free-tier keys use the Free row above. ## Headers on every response | Header | Meaning | |---|---| | `X-RateLimit-Limit` | Max requests in the current 60s window | | `X-RateLimit-Remaining` | Requests left in the window | | `X-RateLimit-Reset` | Unix timestamp when the window resets | | `X-Credits-Used` | Credits charged for this call (0 on errors / available:false) | | `X-Credits-Remaining` | Best-effort balance after the call (omitted when unlimited / unknown) | | `Retry-After` | Seconds to wait (on 429 responses) | ## 429 behavior ```json { "error": { "code": "rate_limit_exceeded", "message": "Rate limit exceeded", "status": 429, "doc_url": "https://lumify.ai/docs/reference#error-codes", "retry_after": 23 }, "detail": "Rate limit exceeded" } ``` No credits are consumed on a 429. Switch on `error.code` and sleep `retry_after` before retrying. ## Cursor pagination - Query params: `?after_id=&limit=` - `limit` range: **1–100** (defaults vary by endpoint; events default 25) - Response field: `next_after_id` — pass it back as `after_id`; `null` means last page - Cursor is stable even if new rows are ingested between pages - **Caveat:** `sort=status` on events does **not** support `after_id` — combining them returns **400**. Use `sort=time` (default) for cursor pagination. ```python import requests after_id = None while True: params = {"sport": "nba", "limit": 100} if after_id: params["after_id"] = after_id r = requests.get( "https://lumify.ai/v1/events", headers={"Authorization": "Bearer lmfy-YOUR_KEY"}, params=params, timeout=30, ) r.raise_for_status() body = r.json() for event in body["events"]: process(event) after_id = body.get("next_after_id") if not after_id: break ``` ## SSE concurrency - Max **5 concurrent streams** per API key - Each connection costs **1 credit** to open - Connections close after **5 minutes**; the server sends `event: reconnect` first - Both SDKs' stream helpers reopen automatically Related: https://lumify.ai/docs/best-practices.md · https://lumify.ai/docs/cheat-sheet.md --- # Agent cookbook Cite-back: https://lumify.ai/docs/agent-cookbook.md # Lumify Agent Cookbook Copy-paste recipes for wiring Lumify into AI agents and apps. Every request needs an API key (`Authorization: Bearer lmfy-...`). **Bootstrap:** the fastest start is the free **instant trial key** — no signup, email, or card — at (100 credits, 14-day expiry). For a persistent account plus 1,000 starter credits, create a key in the dashboard at . After that, agents can create additional keys and top up credits via `/api/agent/*` using an existing key. - Base URL: `https://lumify.ai` - GEO orientation (coverage/pricing/FAQ): - Full technical reference: - OpenAPI: - MCP endpoint: `https://lumify.ai/mcp` - Postman collection: `docs/external/lumify.postman_collection.json` --- ## 1. Quick tasks (curl) Get today's best MLB bets (recommended only): ```bash curl "https://lumify.ai/v1/events?sport=mlb&status=scheduled&has_recommend=true" \ -H "Authorization: Bearer $LUMIFY_API_KEY" ``` Find a team's upcoming games (resolve the id first — NL query does not map team names): ```bash TEAM_ID=$(curl -sS "https://lumify.ai/v1/teams?q=bruins&sport=nhl" \ -H "Authorization: Bearer $LUMIFY_API_KEY" | jq '.data[0].id') curl "https://lumify.ai/v1/events?team_id=$TEAM_ID&status=scheduled" \ -H "Authorization: Bearer $LUMIFY_API_KEY" ``` Then fetch the intelligence for one event: ```bash curl "https://lumify.ai/v1/events/12345/intelligence" \ -H "Authorization: Bearer $LUMIFY_API_KEY" ``` Poll a live score (or stream it — see §6): ```bash curl "https://lumify.ai/v1/events/12345/score" \ -H "Authorization: Bearer $LUMIFY_API_KEY" ``` --- ## 2. Cursor / Claude Desktop (MCP) Lumify hosts a remote MCP server at `https://lumify.ai/mcp` (Streamable HTTP, JSON mode). Authenticate with your Lumify API key as a Bearer token. The server is stateless — no session negotiation is required. Human-oriented install + prompts: **Cursor (one-click)** — open this deeplink, then replace the placeholder key: ``` cursor://anysphere.cursor-deeplink/mcp/install?name=lumify&config=eyJ1cmwiOiJodHRwczovL2x1bWlmeS5haS9tY3AiLCJoZWFkZXJzIjp7IkF1dGhvcml6YXRpb24iOiJCZWFyZXIgWU9VUl9BUElfS0VZIn19 ``` **Cursor (remote config)** — add to `~/.cursor/mcp.json` (or a project `.cursor/mcp.json`): ```json { "mcpServers": { "lumify": { "url": "https://lumify.ai/mcp", "headers": { "Authorization": "Bearer lmfy-YOUR_KEY" } } } } ``` **Claude Desktop / stdio** — prefer the published bridge (`@lumifyai/mcp`): ```json { "mcpServers": { "lumify": { "command": "npx", "args": ["-y", "@lumifyai/mcp"], "env": { "LUMIFY_API_KEY": "lmfy-YOUR_KEY" } } } } ``` Alternatively, bridge with `mcp-remote`: ```json { "mcpServers": { "lumify": { "command": "npx", "args": [ "-y", "mcp-remote", "https://lumify.ai/mcp", "--header", "Authorization: Bearer lmfy-YOUR_KEY" ] } } } ``` Verify any client interactively with the MCP Inspector: ```bash npx @modelcontextprotocol/inspector # Transport: Streamable HTTP · URL: https://lumify.ai/mcp # Header: Authorization: Bearer lmfy-YOUR_KEY ``` Tools exposed: `list_sports`, `list_seasons`, `list_events`, `get_event`, `batch_get_events`, `query_events`, `get_live_score`, `get_odds`, `get_odds_history`, `get_splits`, `get_intelligence`, `list_teams`, `get_team`, `search_players`, `get_player`, `get_player_events`, `estimate_cost` (free — pre-call credit-cost estimate for planned calls). ### Recipe — natural-language search then batch detail ```bash # 1) Parse free text into list filters (1 credit) curl -sS https://lumify.ai/v1/query \ -H "Authorization: Bearer $LUMIFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query":"live nhl games today","limit":5}' # 2) Fetch full detail for the returned ids in one round-trip curl -sS https://lumify.ai/v1/events/batch \ -H "Authorization: Bearer $LUMIFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"event_ids":[101,102],"include_odds":true}' ``` `query_events` is rule-based (not an LLM). Always inspect `interpreted`, `equivalent_request`, and `unrecognized_terms` before acting — bare `football` is ambiguous and left unrecognized on purpose. ### Recipe — MLS soccer line-shopping (price gap, not EV) MLS `/intelligence` publishes a cross-book **price-gap** surface under each bet: `fair` (sharp reference), `edges_by_book` (gap per soft book), and `best` (highest-gap book). Top-level `bets[].edge` stays null while `blend_w` is 0 — that field is model-vs-priced-book EV; the line-shopping product is `edges_by_book` / `best`. ```bash # 1) Upcoming MLS fixtures curl -sS "https://lumify.ai/v1/events?sport=soccer&league=mls&status=scheduled&limit=5" \ -H "Authorization: Bearer $LUMIFY_API_KEY" # 2) Intelligence for one event (look at bets[].fair / edges_by_book / best) curl -sS "https://lumify.ai/v1/events/$EVENT_ID/intelligence" \ -H "Authorization: Bearer $LUMIFY_API_KEY" \ | jq '.bets[] | {bet_type, fair, edges_by_book, best}' ``` How to use it: rank soft books by `edges_by_book` (or take `best.book` / `best.price`) to find the quote furthest above the sharp reference. Do **not** read a positive gap as expected value or badge a pick as "+EV" — at N=2 soft-book coverage (FanDuel/Hard Rock) the measured winner's-curse of a max-of-N pick is ~171% of the mean positive gap, and a closing-time N=2 backfill realized ≈−2.5% on the positive-gap population. `best.quote_age_seconds` is the soft book's age at publish; quotes older than 30 minutes are excluded. ### Recipe — estimate cost before spending (always free) Budget a planned call (or batch of calls) without executing it. Same tool names and arguments as MCP / the SDKs. The estimate itself never costs credits. ```bash curl -sS https://lumify.ai/v1/estimate \ -H "Authorization: Bearer $LUMIFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"calls":[ {"tool":"get_event","arguments":{"event_id":12345,"include_odds":true}}, {"tool":"get_intelligence","arguments":{"event_id":12345}} ]}' # → estimates[], total_min_credits, total_max_credits ``` Over MCP, call `estimate_cost` with the same `calls` payload. Supported tool names: `GET /v1/estimate/tools`. Costs are ranges because add-ons only bill when data is actually available. Billing: `initialize`, `tools/list`, and `ping` are free; each `tools/call` costs the same credits as the equivalent REST call (variable-cost tools like `get_event` with `include_odds` are metered accordingly). Calls that return no usable data because it isn't available yet — `get_odds`, `get_odds_history`, `get_splits`, or `get_intelligence` for a match that hasn't been priced/computed — are **free**: they report `_meta.credits_used: 0` (REST: `X-Credits-Used: 0`). `get_splits` is only ingested for **MLB, NBA, NHL, and NFL**; tennis, soccer, and NCAAF always return `available: false` (also free). > Local stdio bridge (for clients that only speak stdio): > `npx -y @lumifyai/mcp` (forwards to the hosted `/mcp` endpoint; set > `LUMIFY_API_KEY`). Cursor can also hit the remote endpoint directly as shown > above. Note: ChatGPT and Claude.ai *web* connectors require OAuth, which is > not yet supported — use the API-key configs above with desktop/IDE clients. --- ## 3. OpenAI tool calling (Python) ```python import os from openai import OpenAI import requests, json client = OpenAI() LUMIFY = "https://lumify.ai" LUMIFY_API_KEY = os.environ["LUMIFY_API_KEY"] HEADERS = {"Authorization": "Bearer " + LUMIFY_API_KEY} tools = [{ "type": "function", "function": { "name": "get_intelligence", "description": "Get Lumify bet intelligence for an event.", "parameters": { "type": "object", "properties": {"event_id": {"type": "integer"}}, "required": ["event_id"], }, }, }] def get_intelligence(event_id: int) -> dict: r = requests.get(f"{LUMIFY}/v1/events/{event_id}/intelligence", headers=HEADERS) return r.json() resp = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "What's the best bet for event 12345?"}], tools=tools, ) call = resp.choices[0].message.tool_calls[0] args = json.loads(call.function.arguments) result = get_intelligence(**args) ``` --- ## 4. Anthropic tool use (Python) ```python import os import anthropic, requests LUMIFY_API_KEY = os.environ["LUMIFY_API_KEY"] client = anthropic.Anthropic() tools = [{ "name": "list_events", "description": "List Lumify events (schedules and live scores).", "input_schema": { "type": "object", "properties": { "sport": {"type": "string"}, "status": {"type": "string"}, }, }, }] def list_events(sport=None, status=None): r = requests.get( "https://lumify.ai/v1/events", params={"sport": sport, "status": status}, headers={"Authorization": "Bearer " + LUMIFY_API_KEY}, ) return r.json() msg = client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, tools=tools, messages=[{"role": "user", "content": "List today's scheduled MLB games."}], ) ``` --- ## 5. LangChain / LangGraph (MCP adapters) Point LangChain at the hosted MCP server and **all 17 Lumify tools load automatically** — no per-tool wrappers to write or keep in sync. `get_tools()` (a `tools/list` call) needs no key; tool execution uses the Bearer key you pass. **Python** — `pip install langchain-mcp-adapters langchain`: ```python import asyncio import os from langchain_mcp_adapters.client import MultiServerMCPClient from langchain.agents import create_agent async def main(): client = MultiServerMCPClient({ "lumify": { "transport": "streamable_http", # Python adapter name "url": "https://lumify.ai/mcp", "headers": { "Authorization": f"Bearer {os.environ['LUMIFY_API_KEY']}", }, } }) tools = await client.get_tools() # list_events, get_intelligence, … agent = create_agent("openai:gpt-4.1", tools) return await agent.ainvoke( {"messages": "What's the best MLB bet today?"} ) asyncio.run(main()) ``` **JavaScript / TypeScript** — `npm i @langchain/mcp-adapters langchain`: ```typescript import { MultiServerMCPClient } from "@langchain/mcp-adapters"; import { createAgent } from "langchain"; const client = new MultiServerMCPClient({ mcpServers: { lumify: { transport: "http", // JS adapter name for Streamable HTTP url: "https://lumify.ai/mcp", headers: { Authorization: `Bearer ${process.env.LUMIFY_API_KEY}` }, }, }, }); const tools = await client.getTools(); const agent = createAgent({ model: "openai:gpt-4.1", tools }); ``` Prefer a one-liner? The [`langchain-lumify`](https://pypi.org/project/langchain-lumify/) package wraps the above: ```bash pip install langchain-lumify ``` ```python import asyncio from langchain_lumify import get_lumify_tools from langchain.agents import create_agent async def main(): tools = await get_lumify_tools() # requires LUMIFY_API_KEY agent = create_agent("openai:gpt-4.1", tools) return await agent.ainvoke({"messages": "Best MLB bet today?"}) asyncio.run(main()) ``` No key yet? Grab a free instant key (no signup) at . --- ## 6. Live scores without polling (SSE + webhooks) `EventSource` cannot set headers, so pass the key as a query param: ```javascript const es = new EventSource( "https://lumify.ai/v1/events/12345/stream?api_key=lmfy-YOUR_KEY" ); es.addEventListener("score", (e) => console.log(JSON.parse(e.data))); es.addEventListener("done", () => es.close()); // Streams close after 5 minutes. Distinguish a timed close from game-over: es.addEventListener("reconnect", () => { es.close(); // open a fresh EventSource to the same URL }); ``` Prefer the SDK helpers — they reconnect automatically across the 5-minute cap (`streamScores()` in TypeScript, `client.events.stream()` in Python), so a long game looks like one continuous stream. Or subscribe a webhook to be pushed score/status/line-move events: ```bash curl -X POST "https://lumify.ai/v1/webhooks" \ -H "Authorization: Bearer $LUMIFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://your.app/hooks/lumify","event_types":["score","line_move"],"sport":"mlb"}' ``` Callback URLs must be public `https` endpoints (localhost, private RFC1918, and cloud metadata IPs are rejected). Verify deliveries with the `Lumify-Signature: t=,v1=` header (HMAC-SHA256 of `"."` using your subscription's `signing_secret`). Transient delivery failures (5xx, 429, timeout) are retried with exponential backoff (30s / 5m / 30m / 2h / 6h). Inspect history (including retries linked via `parent_delivery_id` and whether Lumify has given up): ```bash curl "https://lumify.ai/v1/webhooks/42/deliveries?success=false" \ -H "Authorization: Bearer $LUMIFY_API_KEY" ``` Opening an SSE stream costs 1 credit, is metered against the same API key as Bearer calls, and is capped at 5 concurrent streams per key (`error.code = stream_limit_exceeded` on 429). Event types: `score`, `status`, `line_move`, `intelligence`. --- ## 7. Handling errors All errors share one envelope: ```json { "error": { "code": "rate_limit_exceeded", "message": "Rate limit exceeded", "status": 429, "doc_url": "https://lumify.ai/docs/reference#error-codes", "retry_after": 42 }, "detail": "Rate limit exceeded" } ``` Switch on `error.code`. Respect `Retry-After` on 429. Budget with the `X-Credits-Used` / `X-Credits-Remaining` response headers. Errors (4xx/5xx) are never charged. Requests for odds, line-movement history, splits, or intelligence on a match where that data isn't available yet succeed with `200` + `available: false` (or an empty list) and are **not** charged (`X-Credits-Used: 0`). The same free `available: false` response applies when splits are requested for an unsupported sport (tennis, soccer, NCAAF — only MLB/NBA/NHL/NFL are ingested). Read the header rather than assuming a fixed per-call cost. --- # AI-assisted setup Cite-back: https://lumify.ai/docs/ai AI-assisted development Use Cursor, Claude, Copilot, or any coding agent to build on Lumify — with MCP tools, machine-readable docs, and copy-paste prompts that prevent hallucinated endpoints. Overview Lumify is built for agents. You can connect in two ways: - MCP tools — the agent calls schedules, odds, splits, and intelligence directly (no wrapper code). - REST + SDKs — the agent reads llms.txt / OpenAPI and writes correct client code. | Resource | URL | Measured size | Use when | MCP server | https://lumify.ai/mcp | 18 tools | Agent needs live sports intelligence as tools | Cheat sheet | /docs/cheat-sheet | ~1 page | Human re-entry + compact LLM context | llms.txt | /llms.txt | ~4.9k tokens (measured) | Lookup / answer-engine overview | llms-full.txt (GEO) | /llms-full.txt | ~11k tokens (measured) | Orientation: FAQ, pricing, coverage, comparisons | docs/llms-full.txt | /docs/llms-full.txt | ~54k tokens (measured) | Full technical docs + endpoint dump | openapi-llms.txt | /openapi-llms.txt | ~5.9k tokens (measured) | OpenAPI-derived endpoint dump alone | OpenAPI | /openapi.json | ~45k tokens (measured) | Exact schemas for clients and validators | Agent manifest | /.well-known/agent.json | — | Discovery of transport + MCP endpoint | Agent cookbook | /docs/agent-cookbook.md | — | Copy-paste REST + MCP recipes | Changelog | /changelog · JSON | — | Date-stamped changes agents can poll Token budgets are measured (UTF-8 bytes ÷ 4), not estimated. Re-measure after regenerating llms-full.txt or OpenAPI. One-click install Install the hosted MCP server into Cursor, then replace the placeholder API key: Create a free API key → Cursor (remote HTTP) ~/.cursor/mcp.json { "mcpServers": { "lumify": { "url": "https://lumify.ai/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } Cursor / Claude Desktop (stdio via npm) Use the published bridge when the client only speaks local stdio: npx npx -y @lumifyai/mcp mcp.json { "mcpServers": { "lumify": { "command": "npx", "args": ["-y", "@lumifyai/mcp"], "env": { "LUMIFY_API_KEY": "YOUR_API_KEY" } } } } VS Code / Copilot .vscode/mcp.json { "servers": { "lumify": { "type": "http", "url": "https://lumify.ai/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } CLI one-liner: code --add-mcp '{"name":"lumify","type":"http","url":"https://lumify.ai/mcp","headers":{"Authorization":"Bearer YOUR_API_KEY"}}' Web connectors: ChatGPT and Claude.ai browser connectors need OAuth, which Lumify does not implement yet. Use Cursor, Claude Desktop, VS Code, or any Bearer-header MCP client. Give your agent context Paste this into CLAUDE.md, .cursorrules, or a project rule file. It is the API's essence compressed for agents (~2.5k tokens of guidance + links to measured artifacts): agent context You are integrating with Lumify (also: Lumify AI, lumify.ai) — the agent-ready sports intelligence API at https://lumify.ai. NOT affiliated with LUMIFY eye drops, Philips Lumify ultrasound, lumifyai.com, or the archived lumifyio/lumify project. ## Read these first (measured token budgets) - https://lumify.ai/llms.txt (~4.9k tokens) — overview + pricing + limitations - https://lumify.ai/docs/cheat-sheet — base URL, auth, credits, hero query, errors - https://lumify.ai/llms-full.txt (~11k tokens) — GEO orientation (FAQ, coverage) - https://lumify.ai/docs/llms-full.txt (~54k tokens) — full technical docs + dump - https://lumify.ai/openapi-llms.txt (~5.9k tokens) — endpoint dump alone - https://lumify.ai/openapi.json (~45k tokens) — exact schemas - https://lumify.ai/docs/agent-cookbook.md — copy-paste recipes - https://lumify.ai/changelog.json — date-stamped changes ## Auth Authorization: Bearer lmfy-... Instant trial key (no signup): https://lumify.ai/docs/ai Never invent an API key. If you cannot access URLs, ask the user to paste the relevant resource instead of guessing. ## MCP (preferred when available) URL: https://lumify.ai/mcp (Streamable HTTP, JSON mode, stateless) 18 tools: list_sports, list_seasons, list_events, get_event, batch_get_events, query_events, get_live_score, get_odds, get_odds_history, get_stats, get_splits, get_intelligence, list_teams, get_team, search_players, get_player, get_player_events, estimate_cost. initialize / tools/list / ping are free; tools/call metered like REST. _meta.credits_used reports the charge. Prefer MCP tools over hand-rolled REST. ## Billing rule (two budgets) - Data plane: schedules, scores, odds, splits, stats — typically 1 credit. - Intelligence plane: /intelligence — 1 credit when available. - Richest-payload-wins: include_odds (+1 or +2 for multi-book) and include_intelligence (+1) on GET /v1/events/{id} compound into one call. - Errors and available:false responses are NEVER charged. - Always estimate first with POST /v1/estimate or MCP estimate_cost (free). ## Boundary litmus - /stats and raw odds = deterministic data. No scoring, no tiers. - /intelligence = judgment (confidence/tier/rationale OR MLS probability model). - Branch on presence of `probability` vs `confidence_score` in bets[]. ## Hero endpoints GET /v1/events?sport=mlb&status=scheduled GET /v1/events/{id}?include_odds=true&include_intelligence=true GET /v1/events/{id}/odds?bookmaker=all GET /v1/events/{id}/splits GET /v1/events/{id}/intelligence POST /v1/estimate POST /v1/trial-key (human Turnstile-gated; prefer /docs/ai button) ## Coverage (keep in sync with llms.txt) Intelligence live: MLB, NFL, NCAAF, tennis, FIFA WC soccer, MLS. Splits: MLB, NBA, NHL, NFL. Books: pinnacle (default), fanduel, draftkings, betmgm, caesars, bet365, circa, hardrock, betonline. No player props / futures / alternates yet. Odds cadence ~30 min. ## Model behavior - Do not guess or invent endpoints, fields, sport IDs, or credit costs. - Help the user choose filters (sport, status, date, has_recommend). - When data is unavailable, explain available:false rather than retrying forever. - Gate volume, not capability existence — streaming/webhooks are self-serve. In Cursor, you can also add https://lumify.ai/llms.txt as a docs/@ reference. Starter prompts Try these after MCP is connected (or with the context block above): Live slate + intelligence prompt Using Lumify MCP, list today's MLB games that are scheduled or live. For the top 3 by start time, pull get_intelligence and summarize confidence, recommended bets, and the key rationale bullets. Splits vs public prompt Find NFL games this week where betting splits show a clear ticket% vs handle% divergence. Use list_events then get_splits. Rank by the largest handle/ticket gap and explain what it implies. Line movement watcher prompt For a given event_id, call get_odds and get_odds_history. Show opening vs current moneyline/spread/total across supported books (Pinnacle, FanDuel, DraftKings, BetMGM, Caesars, Bet365, Circa, Hard Rock, BetOnline), and flag any reverse line moves. Scaffold a small agent prompt Read https://lumify.ai/openapi.json and scaffold a TypeScript script that: (1) lists today's NBA events, (2) fetches intelligence for each, (3) prints games with has_recommend=true. Use @lumifyai/sdk if helpful. Do not invent fields. SDKs When you want typed REST clients instead of (or alongside) MCP: npm / pip npm install @lumifyai/sdk pip install lumify-sdk Docs: @lumifyai/sdk · lumify-sdk · MCP bridge @lumifyai/mcp Next steps - Full MCP tool catalogue & billing - Build an MCP betting-splits agent - REST API reference - Pricing & free credits --- # FAQ Cite-back: https://lumify.ai/faq # FAQ Cite-back: https://lumify.ai/faq ## Product Overview ### What is the Lumify Sports Intelligence API? Lumify is a real-time sports intelligence API designed for AI agents and autonomous systems. It delivers structured, explainable data — including live scores, player signals, market intelligence, and change attribution — in a format that agents can reason over and act on without additional processing. Unlike traditional sports data feeds designed for dashboards and displays, every Lumify response includes confidence scores, rationale, and change signals that make it native to agentic workflows. ### How is Lumify different from other sports data providers? Most sports data APIs return raw numbers. Lumify returns intelligence — structured data enriched with rationale, confidence scoring, and change attribution that tells an agent not just what happened, but what it means and how certain the system is. Key differences: - Agent-native format — responses are structured for reasoning, not rendering - Confidence scoring — every signal includes a reliability score - Change attribution — know why a value changed, not just that it did - Credit-based pricing — pay for intelligence consumed, not data volume ### What sports and leagues are currently supported? Lumify covers NFL, NBA, MLB, NHL, NCAA Football, NCAA Basketball, ATP/WTA tennis, and soccer — the FIFA World Cup plus club leagues MLS, Premier League, La Liga, Serie A, Bundesliga, Ligue 1, and UEFA Champions League, with schedules, live scores, and odds across all of them. Bet intelligence is available for MLB, NFL, NCAA Football, ATP/WTA tennis, FIFA World Cup soccer, and MLS. Those come in two response shapes: MLB, NFL, NCAA Football, tennis, and World Cup soccer return the points model (confidence scoring, signal breakdowns, rationale, and attribution), while MLS returns the probability model (a calibrated probability, a vig-free fair price, and the components behind them). NBA, NHL, NCAA Basketball, and the remaining club soccer leagues have live data and odds today, with intelligence coming to those next. See the API reference for per-field definitions of both shapes. Contact us if you need coverage for a specific league or sport not listed. ### What does 'agent-ready' mean? Agent-ready means the API is designed specifically for consumption by AI agents, LLM pipelines, and autonomous systems — not human eyes on a dashboard. This includes: structured JSON with consistent schemas, confidence scores on every signal, natural-language rationale fields, change-detection webhooks, and predictable credit costs per call that make it easy to budget agentic workloads. ## Getting Started ### How do I get an API key? Sign up for a free account at lumify.ai/register. Once your account is created, navigate to API Keys in the sidebar and click Create key. Your key is shown once — copy it immediately. Free Tier accounts receive 1,000 credits that never expire, with no credit card required. ### What's the fastest way to make my first call? All requests require a Bearer token header: Authorization: Bearer lmfy-xxxxxx.your_key_here Then hit any endpoint — for example, a live match lookup costs 1 credit and returns a structured response within ~200ms. The API reference has curl examples for every endpoint. ### Is there an SDK or just REST? Yes — npm install @lumifyai/sdk for TypeScript/JavaScript, and pip install lumify-sdk for Python. The REST API itself works with any HTTP client too, and is simple enough to integrate directly in under 10 minutes. The API Docs include examples in curl, Python, and Node. ### What does the Free Trial include? The Free Tier gives you: - 1,000 credits that never expire - Full access to all API endpoints - Up to 2 active API keys - Rate limit of 20 requests/minute No credit card is required. Your credits don't expire, so you can explore at your own pace — when you're ready to scale beyond the free credits, upgrade to a paid plan. A rolling 24-hour spend cap applies to the free tier as an anti-abuse safeguard. ### Can I self-host Lumify? Yes — Lumify can run in your own environment for organizations that need to keep data on-premises or in a private cloud. The full setup guide, including required services and configuration, is at docs/getting-started/self-hosting.md. Most customers use the hosted lumify.ai API instead — self-hosting is typically an Enterprise conversation. Contact us if you're evaluating it. ## API & Integration ### What authentication method does the API use? All API requests are authenticated via a Bearer token in the Authorization header: Authorization: Bearer lmfy-xxxxxx.your_key_here Keys are scoped to your account and can be revoked at any time from the API Keys page. Never share your key or commit it to source control. ### What response format does the API return? All responses are JSON. The exact shape depends on the endpoint — for example: - GET /v1/events → { events: [...], total, next_after_id } - GET /v1/events/{id}/intelligence → { available, has_recommend, analyst_take, bets: [...] }. Each bet uses one of two shapes: most sports include confidence_score, tier, rationale, and attribution, while MLS includes probability, fair_price, p_market, and edge instead — branch on which of probability / confidence_score is present Every response includes rate-limit headers (X-RateLimit-*) and credit headers (X-Credits-Used, X-Credits-Remaining when the balance is resolvable). Usage history is also available in your dashboard. See the API reference for the full schema of each endpoint. ### Do you have a Postman collection or OpenAPI spec? Full API documentation with curl, Python, and JavaScript examples is at /docs, with the complete endpoint contract at /docs/reference. An auto-generated OpenAPI schema is served at /openapi.json, and a ready-to-import Postman collection is available for download. ### Can I use the API from a serverless function or edge runtime? Yes. The REST API works from any environment that can make HTTPS requests — AWS Lambda, Vercel Edge Functions, Cloudflare Workers, Google Cloud Functions, etc. Keep your API key in environment variables / secrets, not in client-side code — calls should always go through your own backend or edge function, never directly from a user's browser. Contact us if your integration has a use case that needs direct browser access. ### Does Lumify support MCP (Model Context Protocol)? Yes. Lumify runs a hosted MCP server at https://lumify.ai/mcp exposing 18 tools — event schedules and live scores, odds and line-movement history, betting splits, bet intelligence, stats, team/player/season lookups, and a free pre-call cost estimator — so agents in Cursor, Claude, and other MCP clients can query Lumify directly without writing REST calls. Install in Cursor with one click from the MCP guide, or run npx -y @lumifyai/mcp for Claude Desktop and other stdio clients. See the AI-assisted setup guide for prompts and context files. Only tool calls are metered — discovery and connection are free. ### How do real-time updates work — webhooks or streaming? Both. For push-based updates, register a webhook with POST /v1/webhooks — supply a public HTTPS URL and, optionally, which event types to receive (score, status, line_move, intelligence; defaults to score and status). Every delivery is signed with a Lumify-Signature header so you can verify authenticity. For pull-based streaming, GET /v1/events/{id}/stream opens a Server-Sent Events connection that pushes score and status changes as they happen. Each API key can hold up to 5 concurrent streams, and a single connection closes automatically after 5 minutes — the server sends an event: reconnect frame first so you know to open a fresh connection (both SDKs' stream helpers do this for you automatically). ### What happens if a webhook delivery fails? Transient failures — HTTP 5xx, 429, or a timeout — are retried automatically with exponential backoff (30 seconds, then 5 minutes, 30 minutes, 2 hours, and 6 hours). Each attempt is recorded; retries are linked via parent_delivery_id. Inspect history with GET /v1/webhooks/{id}/deliveries (filters: success, given_up, event_type). When retries are exhausted, given_up is true. Permanent client errors (most 4xx) are not retried. See the delivery history reference. ### What does an error response look like? Every error on /v1/*, /mcp, and /api/agent/* returns the same JSON envelope, so agents can parse failures deterministically: { "error": { "code": "not_found", "message": "Event not found.", "status": 404, "doc_url": "https://lumify.ai/docs/reference#error-codes" }, "detail": "Event not found." } error.code is a stable, machine-readable slug (e.g. rate_limit_exceeded, validation_error, unauthorized) — switch on that rather than parsing message, which is meant for humans. detail is kept as a backward-compatible mirror of message for older integrations. ## Data & Coverage ### How fresh is the data? Live scores refresh approximately every minute during active games. Odds and betting splits are ingested roughly every 30 minutes, and bet intelligence is recomputed on a similar ~30-minute cadence (with a validator pass shortly after). Completed events are stable once finalized. Use the updated_at and intelligence_updated_at timestamps in responses to detect when data last changed. ### Which sportsbooks are supported for odds? Odds and line history cover Pinnacle (default sharp/reference book), FanDuel, DraftKings, BetMGM, Caesars, Bet365, Circa, Hard Rock, and BetOnline. Use bookmaker=pinnacle (default, 1 credit), a comma-separated list, or bookmaker=all (2 credits). Public betting splits use a separate key namespace (e.g. dk for DraftKings) and cover DraftKings, Circa, FanDuel, BetMGM, and Caesars for MLB/NBA/NHL/NFL. ### What data points are included per event? A standard event response includes: - Live score and match status - Team and player signal scores with confidence - Market intelligence invocations (where applicable) - Rationale and change attribution for key signals - Structured metadata (venue, officials, weather) Specific fields vary by sport and endpoint. See the API reference for full schemas. ### How is confidence scoring calculated? Confidence scores (0.0–1.0) come from a weighted, sport-specific signal model. Each bet is scored across multiple signals (e.g. for MLB: starting pitching, bullpen, lineup/OPS, park & weather, market odds, recent form, splits, research alignment). The earned points are normalized against the maximum available points (coverage), then adjusted by a Deep Research validator pass. Scores map to tiers: very_high (≥ 0.85), strong (0.70–0.84), moderate (0.55–0.69), and avoid (< 0.55). Per-signal breakdowns and a human-readable rationale are included in every points-model intelligence response. MLS is different. It uses the probability model, which returns a calibrated probability rather than a points score — the outcomes of a market are solved together and sum to 1, so they cannot contradict each other. Instead of coverage it reports sufficiency (how much evidence backs the number, which widens the interval rather than suppressing the response), and instead of signals it reports drivers (signed contributions in probability units). Where no fitted model has cleared out-of-sample validation for a league yet, probability is the de-vigged market price and edge/tier are null — a probability taken from the market has no honest edge against the price it came from. ### Can I access historical data? Yes. Completed events — final scores, per-period breakdowns, and captured odds — are retained and queryable through the standard endpoints using date filters (?from/?to), up to a 90-day range per request. Historical queries are billed at the same credit rate as live queries. For large or multi-season bulk historical access, contact us about an Enterprise plan and volume pricing. ### Can I resell or redistribute data I get from Lumify? Our Terms of Service govern acceptable use of the API, but redistribution and resale rights aren't a one-size-fits-all answer — they depend on your use case and plan. If you're building a product that displays or resells Lumify data to your own end users, contact us to discuss licensing terms before you launch. ## Credits & Billing ### How do credits work? Credits are the unit of consumption on Lumify. Most API calls cost 1 credit. A few endpoints can cost more when you request extra data in a single call: - Standard call (events, scores, odds, splits, intelligence, players) — 1 credit - GET /v1/events/{id}?include_odds=true — +1 credit for a single book (default Pinnacle); +2 credits when bookmaker=all or a comma-separated list - ?include_intelligence=true — +1 credit when intelligence is available - Standalone multi-bookmaker odds (bookmaker=all or a comma-separated list) — 2 credits Failed requests (4xx/5xx) never consume credits — and neither do odds or intelligence calls that return available: false (the market or analysis isn't ready yet). You're only charged once there's real data in the response. Your plan allocates a monthly credit budget that resets on your billing anniversary. ### Can I estimate credit cost before making a call? Yes — and estimating is always free. POST /v1/estimate (MCP tool estimate_cost) returns a min/max credit range for one or more planned calls without executing them. Costs are ranges because add-ons like odds or intelligence only bill when that data is actually available. See GET /v1/estimate/tools for supported tool names, or the estimate reference. Both SDKs expose client.estimate.cost(...). ### What counts as one credit? One credit = one successful standard API call. Compound calls that bundle extra data (embedding odds or intelligence, or requesting multiple bookmakers) cost more, as described above. Failed requests (4xx/5xx) do not consume credits. You can review exactly what was consumed on your usage dashboard. ### Do unused credits roll over to the next month? No. Credits reset on your billing anniversary each month. Unused credits do not roll over. If you regularly have leftover credits, consider a smaller plan. If you're consistently hitting your limit, upgrade to a higher tier or switch to Pay As You Go, which is metered and never blocks on a monthly credit cap. ### What happens when I run out of credits? Once your credit balance is exhausted, API requests are rejected with a 402 Payment Required response (error.code of insufficient_credits) until you add credits or upgrade. Free Tier credits themselves never expire. To avoid interruptions, upgrade to a higher tier or switch to Pay As You Go, which is metered and continues serving requests without a monthly credit cap. ### Can I upgrade, downgrade, or cancel my plan? Yes, you can change your plan at any time from the Billing page. - Upgrades take effect immediately and are prorated - Downgrades take effect at the start of your next billing cycle - Cancellation stops renewal; you retain access until the end of the current period ## Rate Limits ### What are the rate limits per plan? Rate limits are enforced per API key on a sliding 60-second window: - Free Tier — 20 req/min - Pay As You Go — 60 req/min - Growth — 120 req/min - Enterprise — custom / negotiated Rate limit headers (X-RateLimit-Remaining, X-RateLimit-Reset) are included in every response so you can track consumption in real time. ### What happens if I exceed my rate limit? Requests that exceed your rate limit receive a 429 Too Many Requests response with a Retry-After header indicating when the window resets. No credits are consumed for rate-limited requests. We recommend implementing exponential backoff in your client for resilience. ### What is the expected API response time? Median response time is under 200ms for live data endpoints. Market intelligence endpoints that involve additional computation typically respond within 500ms. Response times are monitored continuously on our end. If you're seeing unexpected latency, contact us and we'll investigate. ## Security & Privacy ### How are API keys stored? API keys are never stored in plaintext. Lumify stores only a SHA-256 hash of each key for validation. The plaintext key is shown exactly once at creation time — we cannot retrieve it after that. If you lose a key, revoke it immediately from the API Keys page and generate a new one. ### Is data encrypted in transit and at rest? Yes. All API traffic is encrypted via TLS 1.2+. Data at rest is encrypted using AES-256. We do not store your query content beyond what is needed for credit metering and rate limiting. ### Where is data hosted? Lumify infrastructure runs on Google Cloud Platform in the us-west1 region. Enterprise customers can request dedicated deployments in alternate regions. If you need to complete a security questionnaire or compliance review as part of your evaluation, contact us — we're happy to work through it directly. ## Support ### How do I report a bug or unexpected API behaviour? Use the Contact page and select Technical support as the subject. Include your API key prefix (not the full key), the endpoint, and an example request/response. Growth and Enterprise customers have access to a dedicated support channel with a guaranteed response SLA. ### Do you offer uptime or response SLA guarantees? Our target uptime is 99.9% for all paid plans. Formal SLA guarantees with credit remedies are available on Enterprise plans. We don't yet have a public status page — if you suspect an outage or degraded service, contact us directly and we'll respond quickly. ### How do I contact the team? The fastest way to reach us is via the Contact page. We read every message and respond within one business day. For enterprise inquiries, partnership opportunities, or press requests, select the relevant subject from the contact form and we'll route you to the right person. --- # Pricing (from /pricing) Cite-back: https://lumify.ai/pricing Pricing Pay only for what you use. Every plan includes the full Intelligence API. Start free, scale to production without re-architecting. Best value For evaluation and testing For prototypes and variable workloads For production applications and high-volume usage $0 free to start $ /month credits included $/cr effective Save % vs Pay As You Go $ per credit · no commitment Custom contact us for pricing Credits included Expiration None Best for Under 7,000 cr/mo Best for 7,000+ cr/mo Overage credits $0.035/cr - Rate limit / min Max API keys ✓ Current plan Not available Start Free ✓ Current plan Get Started Enterprise Built for production scale Custom credit volumes, dedicated endpoints, SLA guarantees, and a direct integration partner — tailored to your workload. Custom Monthly credits Unlimited API keys 99.9% Uptime SLA Dedicated Support channel Contact Sales How credits work Standard call (events, scores, odds, splits, intelligence, players) 1 credit Multi-bookmaker odds (bookmaker=all or a list) 2 credits Compound event fetch (include_odds / include_intelligence) +1–2 credits SSE stream open / pre-call cost estimate 1 credit / free Errors and available: false responses are never charged. Free tier credits never expire — explore at your own pace. Estimate any call for free with POST /v1/estimate. --- # OpenAPI schema field catalog Cite-back: https://lumify.ai/openapi.json Derived from clients/lumify-sdk/openapi/openapi.sdk.json — the filtered public SDK slice of /openapi.json. Prefer /openapi.json for exact schemas. ### AgentApiKey - id (integer | null) - name (string | null) - key_prefix (string | null) - scopes (array | null) - created_at (string | null) - last_used_at (string | null) - expires_at (string | null) - is_active (boolean | null) - key (string | null): Full API key — returned once on create. ### AgentApiKeyListResponse - data (array) - total (integer) ### AgentApiKeyRevokeResponse - revoked (boolean) - id (integer | null) ### AgentCreditsResponse - tier (string | null) - credits_used (integer | null) - credit_limit (integer | null) - bonus_credits (integer | null) - total_remaining (integer | null) - period_start (string | null) - period_end (string | null) - is_trial (boolean | null) - is_trial_expired (boolean | null) ### BatchEventsRequest - event_ids (array required): Event ids to fetch (max 25). Duplicates are billed once. - include_odds (boolean): Inline current odds on each event. Scoped by bookmaker (default: pinnacle). +1 credit per event for a single book when available; +2 for bookmaker=all or a comma-separated list. - include_intelligence (boolean): Inline bet intelligence on each event (+1 credit per event when available). - bookmaker (string | null): Bookmaker for inlined odds and intelligence market prices. Defaults to pinnacle; 'all' or a comma-separated list for multiple books. ### BatchEventsResponse - events (array) - not_found (array): Requested event_ids that don't exist. Never billed. - total (integer) ### BetIntelligence - bet_type (string | null): Canonical bet token, e.g. ML_HOME/ML_AWAY/ML_DRAW (soccer), ML_P1/ML_P2 (head-to-head sports), SPREAD_HOME/SPREAD_AWAY, OVER, UNDER. - player_role (string | null): 'home'/'away' for team sports or 'p1'/'p2' for head-to-head sports. Null for match-level tokens (OVER, UNDER, ML_DRAW), which are not about either side. - player_id (integer | null): Player ID when this bet is about an individual player; null for team bets and match-level tokens (OVER, UNDER, ML_DRAW). - team_id (integer | null): Team ID when this bet is about a team; null for individual-player bets and match-level tokens (OVER, UNDER, ML_DRAW). A draw is not a bet on either team, so ML_DRAW carries no team_id — safe to sum exposure by team_id across bets without double-counting the draw. - player_name (string | null): Display name of the player or team this bet is about; null for match-level tokens (OVER, UNDER, ML_DRAW). Duplicates `players[player_role].name` for convenience when flattening bets[]. - market (object | null): Market quote used for this bet: {price: American odds, line: handicap/total line or null for moneyline, book: bookmaker slug when known}. For predictive-framework sports `book` is always populated from the assessment; for legacy sports it may be omitted. Present for every sport. - computed_at (string | null): ISO-8601 UTC timestamp of when this bet's numbers last materially changed — not when they were last checked. The publisher re-runs on a schedule but only rewrites a row when the price, line, or probability moves beyond a tolerance, so an older `computed_at` means 'unchanged since', not 'stale'. Values legitimately differ between bets on the same event because each market moves independently. - tier (string | null): Confidence tier: 'very_high', 'strong', 'moderate', or 'avoid'. Present for both legacy and predictive-framework sports. For predictive-framework sports it is null whenever `edge` is null — a tier ranks a bet against its price, so there is nothing to rank without an edge. - confidence_score (number | null): Legacy points-engine confidence score, 0-1. A points fraction, not a calibrated probability. Legacy sports only — see `probability` for predictive-framework sports. - confidence_score_pre_validator (number | null): Legacy confidence_score before the qualitative validator applied its delta. Legacy sports only. - coverage (number | null): Legacy signal-coverage fraction, 0-1 — how many of the expected signal_* columns were populated for this bet. Legacy sports only — superseded by the continuous `sufficiency` for predictive-framework sports, which widens the interval instead of gating the response. - signals (object | null): Legacy per-signal point breakdown (signal_research, signal_form, etc.); the same DB columns mean different things per sport — see `signals._labels` when present. Legacy sports only — superseded by `drivers` for predictive-framework sports. - validator (object | null): Legacy qualitative-validator verdict: {stance, confidence, delta, validated_at}. Legacy sports only. - narrative (string | null): Legacy per-bet narrative sentence generated alongside the validator pass. Legacy sports only. - rationale (array): Legacy human-readable rationale bullets built from `signals`. Legacy sports only. - attribution (array): Attribution keys backing `rationale`, in the same order. Legacy sports only. - probability (number | null): Published probability for this outcome, 0-1. Equals `p_market` (the de-vigged market price) whenever `blend_w` is 0, which is the case for every league that does not yet have a fitted model that beat the market out-of-sample; otherwise it is the log-space blend of `p_model` and `p_market` at weight `blend_w`. Outcomes of the same market (e.g. ML_HOME + ML_DRAW + ML_AWAY) are blended jointly and sum to 1. Predictive-framework sports only. - interval (array | null): [lo, hi] band around `probability`, 0-1. Width is driven by `sufficiency`, so read it as 'how much evidence backs this number', not as a statistical confidence interval — the constants are calibrated against realised outcomes, not derived analytically. When `blend_w` is 0 the band reflects how mature and well-traded the quoted line is, so a freshly-opened, unmoved line gets a wider band than a heavily-traded one. Thin evidence widens this band rather than removing the response. Predictive-framework sports only. - p_model (number | null): The fitted model's own probability for this outcome, before blending with the market. Null for any league that does not yet have a model cleared for publication — currently every soccer league. Predictive-framework sports only. - p_market (number | null): De-vigged market probability for this outcome, before blending with the model. This is the bookmaker's price with the vig removed across the whole market, so the outcomes of a market sum to 1 — it is not recoverable from a single price by implied-probability arithmetic. Null when the fixture is unpriced. Predictive-framework sports only. - blend_w (number | null): Weight given to `p_model` when blending it with `p_market` to produce `probability`, 0-1. 0 means the published probability is purely the de-vigged market. Weight is enabled per league and per bet token, and only where the model beat the market out-of-sample, so tokens on the same event can carry different weights. Predictive-framework sports only. - fair_price (integer | null): American-odds fair price implied by `probability` — the vig-free line. Comparing it to `market.price` gives the bookmaker's margin on this side. Predictive-framework sports only. - edge (number | null): Expected profit per 1 unit staked at `market.price`, i.e. `probability x decimal_odds - 1`. Positive means the price pays more than the probability justifies. Null whenever `blend_w` is 0: a probability taken from the market has no honest edge against the price it came from, so reporting one would just be restating the vig. Predictive-framework sports only. - sufficiency (number | null): How much evidence backs this assessment, 0-1, and the input that sets `interval` width and caps `tier`. Its inputs depend on what is actually being measured: while `blend_w` is 0 it measures the quoted line's maturity (movement count on the priced book/market and hours since that book's first quote), because there is no model sample to be thin about; once a model carries weight it reflects the model's own sample depth. Predictive-framework sports only. - phase (string | null): 'quant' when the assessment is purely deterministic (ratings, market, and model math). 'full' once a qualitative overlay — lineups, injuries, research alignment — is attached; `alignment` is populated only in that case. Predictive-framework sports only. - model_version (string | null): Identifier of the parameter set that produced this assessment, so a published number can be traced to the version that made it. 'market_anchor' means no fitted model contributed. Per bet, since model weight is enabled per token. Predictive-framework sports only. - drivers (array): Named, signed contributions to `probability` in probability units: {id, input, effect, direction, evidence}. `effect` is the shift in probability attributed to that factor and `direction` is 'up'/'down'/'neutral'. `evidence` is null for quantitative drivers (rating edge, HFA, rest) and, once populated, a list of `{fact_id, as_of}` citations into the Fact Ledger (`lumify_fixture_research`) for evidence-derived drivers — never an embedded quote/URL, which would duplicate that archive. Replaces `signals` for predictive-framework sports. Normally empty when no model contributed (`blend_w` 0); the exception is Stage 6 Match Context drivers (`soccer.match_context.*`) that cite the Fact Ledger — those carry `effect: 0` and never move probability (numeric channel closed). Predictive-framework sports only. - alignment (object | null): Qualitative-overlay agreement detail. Populated only when `phase` is 'full'; null for every 'quant' assessment. Predictive-framework sports only. - fair (object | null): Sharp-consensus fair price for this outcome: {probability, books, n_books, is_consensus}. `probability` is distinct from `p_market` (the *priced* book's own de-vigged probability) and from `fair_price` (American odds implied by the published `probability`, above) — it is the sharp-book reference (`SOCCER_SHARP_BOOKS`) that a cross-book edge is measured against. `is_consensus` is false whenever `n_books` < 2 — a single sharp book is a de-vig, not a consensus (true for every soccer row today: Pinnacle is the only soccer sharp book on our feed). Populated for MLS as of Stage 5 v1 (2026-07-29); null for every other league until its own book-independence screen and multi-book fetch are wired. Predictive-framework sports only. - edges_by_book (object | null): Price gap versus the sharp-reference fair probability (`fair.probability`), keyed by bookmaker, for every independently-eligible soft book quoting this outcome — `fair.probability × decimal_odds − 1` at each book's posted price. This is a **price-gap / line-shopping metric**, not an expected-value recommendation: at the current N=2 soft-book coverage (FanDuel/Hard Rock on MLS), the measured winner's-curse component of a max-of-N pick is ~171% of the mean positive gap, and a closing-time backfill simulation of N=2 realized approximately −2.5% on the positive-gap population (see SOCCER_PROGRAM_PLAN.md §5.0 steps E / tenth pass). Rank soft books by these gaps for line-shopping (largest gap = furthest above the sharp reference); do not treat a positive gap as EV or as a bet recommendation, and do not badge picks as '+EV'. Populated for MLS as of Stage 5 v1; null for every other league until its own gate and fetch are wired, and null for any market with no eligible soft book quoting it. Predictive-framework sports only. - best (object | null): Highest price-gap pick across `edges_by_book`: {book, price, edge, quote_age_seconds}. `edge` is the field's raw maximum (`shrinkage=0.0` — winner's-curse correction closed as won't-fit at N=2). Same caveats as `edges_by_book`: this is a price gap versus a de-vigged sharp reference, **not** an expected-value claim; at N=2 the winner's-curse magnitude is ~171% of the mean positive gap and a closing-time N=2 backfill simulation realized ≈−2.5% on positive-gap picks. Use `best.book`/`best.price` as the line-shopping target for this outcome; do not read `best.edge` as EV or as a bet recommendation. `quote_age_seconds` is `best.book`'s posted-price age at publish time — a stale soft-book price is excluded before selection (30-minute freshness threshold). Populated for MLS as of Stage 5 v1 (2026-07-29); null for every other league or whenever `edges_by_book` is empty. Predictive-framework sports only. ### CreditPack - id (integer | null) - name (string | null) - credits (integer | null) - price_cents (integer | null) - bonus_credits (integer | null) ### CreditPackListResponse - data (array) - total (integer) ### CreditTopupResponse - success (boolean | null) - credits_added (integer | null) - bonus_credits (integer | null) - pack_id (integer | null) ### EstimateCall - tool (string required): The MCP tool / SDK call to estimate, e.g. 'get_event', 'batch_get_events', 'get_odds'. See GET /v1/estimate/tools for the full supported list. - arguments (object): Same arguments you'd pass to that tool/endpoint, e.g. {"event_id": 123, "include_odds": true}. ### EstimateRequest - calls (array required): One or more planned calls to estimate in a single round-trip. ### EstimateResponse - estimates (array required) - total_min_credits (integer required) - total_max_credits (integer required) ### EstimateResult - tool (string required) - min_credits (integer required): Cheapest realistic outcome (e.g. requested data not yet available). - max_credits (integer required): Priciest realistic outcome (e.g. all requested data is available). - note (string | null): Why the cost varies, when it does. ### EventDetail - id (integer required) - name (string | null) - sport (string | null) - league (string | null) - season_id (integer | null) - starts_at (string | null): ISO-8601 UTC start time. - status (string | null) - period (string | null) - clock (string | null) - venue (object | null) - participants (array) - updated_at (string | null) ### EventListResponse - events (array) - total (integer) - next_after_id (integer | null) ### EventSummary - id (integer required) - name (string | null) - sport (string | null) - league (string | null) - season_id (integer | null) - starts_at (string | null): ISO-8601 UTC start time. - status (string | null) - period (string | null) - clock (string | null) - venue (object | null) ### HTTPValidationError - detail (array) ### IntelligenceResponse - event_id (integer | null): Lumify event ID this intelligence describes. Present on the standalone endpoint; omitted when inlined under an event via include_intelligence. - available (boolean): False when no intelligence has been computed for this event yet — every other field is then null/empty and the request is not charged. - odds_source (string | null): Bookmaker `bets[].market` prices were sourced from. For predictive-framework sports this is the assessment's market_book (not a live `?bookmaker=` overlay). For legacy sports it follows the system default or `?bookmaker=` when that override is used. - sport (string | null): Sport slug for this event. - league (string | null): League slug for this event, if any. - players (object): Home/away (or p1/p2) participant identification, keyed by role: {role: {name, player_id, team_id}}. - has_recommend (boolean | null): True when at least one bet meets the recommendation threshold; false when none do; null when intelligence has not been computed for this event. For predictive-framework sports a recommendation requires an `edge`, so this is false for any event whose bets are all market-anchored (`blend_w` 0). - analyst_take (string | null): Short natural-language read on the event as a whole (as opposed to a single bet). Null when no narrative has been generated. - match_overview (string | null): Longer natural-language preview of the matchup — form, context, and what to watch. Null when no narrative has been generated. - intelligence_updated_at (string | null): ISO-8601 UTC timestamp of the most recent change anywhere in this payload — the maximum of the per-bet `computed_at` values for predictive-framework sports. Because individual markets are only rewritten when they move, use the per-bet `computed_at` to reason about a specific bet rather than this event-level maximum. - bets (array): One entry per priced bet token. Which fields are populated depends on whether the sport/league is on the predictive framework (probability/edge fields) or the legacy points engine (confidence_score/signals fields) — see BetIntelligence. Outcomes of the same market sum to 1 for predictive-framework sports. - matchup (object | null): MLB-only probable-starter context (home_starter/away_starter with name/hand/era/confirmed), extracted from research context. Absent for every other sport. ### LeagueSummary - id (integer | null) - slug (string | null) - name (string | null) - abbreviation (string | null) - league_type (string | null) - country_code (string | null) - current_season (ref:SeasonSummary | null) ### NLQueryFilters - sport (string | null) - status (string | null) - date (string | null) - from (string | null) - to (string | null) - limit (integer | null) ### NLQueryRequest - query (string required): Free text, e.g. 'live nfl games today' or 'college basketball this week'. - limit (integer | null): Overrides any limit parsed from the query text. Max 100. ### NLQueryResponse - query (string required): The original natural-language query text. - interpreted (ref:NLQueryFilters required): The GET /v1/events filters parsed from the query text. - unrecognized_terms (array): Query words that didn't map to a known filter. - equivalent_request (string required): The literal GET /v1/events request this query was translated to. - events (array) - total (integer) - next_after_id (integer | null) ### OddsHistoryResponse - event_id (integer required) - movements (array) - total (integer) ### OddsResponse - event_id (integer | null) - available (boolean) - bookmakers (array) - last_updated (string | null) ### Participant - participant_id (integer | null) - role (string | null) - team (object | null) - player (object | null) - score (string | null) - is_winner (boolean | null) ### Player - id (integer | null) - slug (string | null) - full_name (string | null) - first_name (string | null) - last_name (string | null) - sport (string | null) - country_code (string | null) - birthdate (string | null): YYYY-MM-DD date of birth. - position (string | null) - handedness (string | null) - height_cm (integer | null) - weight_kg (number | null) - tennis_ranking (integer | null) - tennis_ranking_points (integer | null) - current_team_id (integer | null) - current_team_name (string | null) - is_active (boolean | null) - retired_at (string | null) - image_url (string | null) ### PlayerEventsResponse - player_id (integer required) - data (array) - has_more (boolean) - next_after_id (integer | null) ### PlayersListResponse - data (array) - has_more (boolean) - next_after_id (integer | null) ### ScoreEntry - role (string | null) - name (string | null) - abbreviation (string | null) - score (string | null) - is_winner (boolean | null) - period_scores (array) ### ScoreResponse - event_id (integer required) - status (string | null) - finished (boolean | null) - period (string | null) - clock (string | null) - scores (array) - updated_at (string | null) ### Season - id (integer | null) - year (integer | null) - name (string | null) - phase (string | null) - start_date (string | null) - end_date (string | null) - is_current (boolean | null) - sport (object | null) - league (object | null) ### SeasonSummary - id (integer | null) - year (integer | null) - name (string | null) - phase (string | null) - start_date (string | null) - end_date (string | null) ### SeasonsListResponse - seasons (array) - total (integer) ### SplitsResponse - event_id (integer required) - available (boolean) - captured_at (string | null) - consensus (object) - books (array) ### Sport - id (integer | null) - slug (string | null) - name (string | null) - is_team_sport (boolean | null) - leagues (array) ### SportsListResponse - sports (array) - total (integer) ### StatsHeadToHead - window (integer required): Max trailing meetings considered (see `windows.head_to_head`). - meetings (array): Most-recent-first past meetings between these two teams, up to `window`. - total (integer): Number of meetings returned in `meetings`. ### StatsHeadToHeadMeeting - home_goals (integer | null): Goals scored by this event's home team in that past meeting. - away_goals (integer | null): Goals scored by this event's away team in that past meeting. ### StatsLeagueContext - avg_goals_per_team (number | null): League-wide average goals scored per team per game this season, used as the baseline the /intelligence layer compares each team's attack/defense against. Null for competitions that use a fixed baseline instead (e.g. World Cup). ### StatsLineup - available (boolean required): False when no lineup has been ingested yet for this side. - formation (string | null): e.g. '4-3-3'. Null when unavailable. - starters (array) - bench (array) - captured_at (string | null): When this lineup was captured from the source, 'YYYY-MM-DD HH:MM:SS'. ### StatsLineupPlayer - name (string | null) - position (string | null): Position abbreviation, e.g. 'GK', 'CB'. - jersey (string | null) - formation_place (integer | null): Slot index within the formation, when ESPN provides one. - espn_athlete_id (string | null) - starter (boolean): True in `starters`, false in `bench`. ### StatsRatesBlock - source (string | null): Provenance tag, e.g. 'event_stats_avg' (ESPN box-score averages). - window (string | integer | null): Which rolling window this block covers: 'l5' (see `window_size` for the fixed game count) or 'season' (an intentionally variable-length window — see `games` for how many games it actually covers for this team). - window_size (integer | null): Fixed sample depth for this window, only present when the window has one (e.g. 5 for 'l5'). Omitted for 'season', since season length varies by team — use `games` instead. - games (integer): Games with box-score data included in this average. - shots_for (number | null): Avg. shots taken per game. - shots_against (number | null): Avg. shots conceded per game. - shots_on_target_for (number | null): Avg. shots on target taken per game. - shots_on_target_against (number | null): Avg. shots on target conceded per game. - possession_pct (number | null): Avg. possession percentage (0-100). - corners_for (number | null): Avg. corners won per game. - corners_against (number | null): Avg. corners conceded per game. - fouls (number | null): Avg. fouls committed per game. - yellow_cards (number | null): Avg. yellow cards received per game. - red_cards (number | null): Avg. red cards received per game. - passes (number | null): Avg. total passes per game (ESPN totalPasses; see pass_accuracy_pct for completion rate). - pass_accuracy_pct (number | null): Avg. pass completion percentage (0-100). - saves (number | null): Avg. goalkeeper saves per game. - save_rate (number | null): Saves / shots on target faced across the window (not an average of per-game rates). - field_games (object): Per-field sample size, keyed by the metric names above. `games` is how many games are in this window overall, but an individual field can have a smaller count when a source's box score omits that stat for some fixtures — lets agents tell 'averaged over all N games' apart from 'averaged over fewer'. ### StatsRecentForm - window (integer required): Number of trailing completed games this form line covers. - results (array): Most-recent-first results over `window`: 'W', 'D', or 'L'. - goals_scored (array): Goals this team scored in each game, same order as `results`. - goals_conceded (array): Goals this team conceded in each game, same order as `results`. ### StatsResponse - event_id (integer required): Lumify event ID. - available (boolean required): False when either team hasn't resolved for this fixture yet — every other field is omitted in that case and the call isn't billed. - league_slug (string | null): League slug, e.g. 'mls'. - profile (string | null): 'club' or 'world_cup'. Determines the shape of `teams.*.team_strength`, `teams.*.venue`, and whether `teams.*.sos` is populated. - neutral_site (boolean | null): Whether this fixture is at a neutral venue. Informational here — not used in any of this endpoint's own aggregates, but factored into home-advantage scoring on GET /v1/events/{id}/intelligence. - windows (ref:StatsWindows | null) - teams (ref:StatsTeams | null) - head_to_head (ref:StatsHeadToHead | null) - league_context (ref:StatsLeagueContext | null) ### StatsSos - source (string | null): Provenance tag, e.g. 'opp_ppg_avg'. - window (integer required): Number of trailing opponents this strength-of-schedule average covers. - games (integer): Opponent games actually available within `window`. - avg_opp_ppg (number | null): Average points-per-game of the opponents faced in `window`. - avg_opp_rank (number | null): Average league-table rank of the opponents faced in `window`. ### StatsTeam - team_id (integer | null) - name (string | null) - abbreviation (string | null) - rest_days (integer | null): Days since this team's previous completed game. - recent_form (ref:StatsRecentForm | null) - rates_l5 (ref:StatsRatesBlock | null): Boxscore rate averages over the trailing 5 games. - rates_season (ref:StatsRatesBlock | null): Boxscore rate averages over the current season/edition. - lineup (ref:StatsLineup | null) - team_strength (object | null): Club fixtures: {source: 'league_table_ppg', ppg, games, points, w, d, l, gf, ga, gd, rank} from the current league table. World Cup fixtures: {source: 'fifa_rank', fifa_rank} since there's no in-tournament table yet. - venue (object | null): Club fixtures: {source: 'home_away_ppg_split', home_ppg, home_games, away_ppg, away_games}. World Cup fixtures: {source: 'confederation', confederation}, since home/away splits don't apply at a neutral-site tournament. - sos (ref:StatsSos | null): Strength of schedule (avg. opponent PPG/rank over recent games). Null for World Cup fixtures — there's no league table to compute it from. ### StatsTeams - home (ref:StatsTeam | null) - away (ref:StatsTeam | null) ### StatsWindows - recent_form (integer required): Trailing games covered by each team's `recent_form`. - rates_l5 (integer required): Trailing games covered by each team's `rates_l5`. - rates_season (string | integer required): Always the literal string 'season' — a deliberately variable-length window (see each team's `rates_season.games`), unlike the fixed integer windows above. - head_to_head (integer required): Trailing meetings between these two teams considered for `head_to_head`. - sos (integer required): Trailing opponents covered by each team's `sos`. ### Team - id (integer | null) - slug (string | null) - name (string | null) - short_name (string | null) - abbreviation (string | null) - sport (string | null) - league (string | null) - city (string | null) - state (string | null) - country_code (string | null) - conference (string | null) - division (string | null) - venue (object | null) - is_active (boolean | null) ### TeamsListResponse - data (array) - has_more (boolean) - next_after_id (integer | null) ### TrialKeyRequest - cf-turnstile-response (string): Cloudflare Turnstile response token from the widget on /docs/ai. ### ValidationError - loc (array required) - msg (string required) - type (string required) - input (any) - ctx (object) ### WebhookCreateResponse - id (integer | null) - url (string | null) - event_types (array) - sport (string | null) - event_id (integer | null) - is_active (boolean | null) - created_at (string | null) - signing_secret (string | null): Returned only on create — store it to verify Lumify-Signature. ### WebhookDeleteResponse - deleted (boolean) - id (integer | null) ### WebhookDeliveryItem - id (integer | null) - event_type (string | null) - event_id (integer | null) - attempt (integer | null) - parent_delivery_id (integer | null) - status_code (integer | null) - success (boolean | null) - error (string | null) - given_up (boolean | null) - next_retry_at (string | null) - delivered_at (string | null) ### WebhookDeliveryListResponse - data (array) - next_after_id (integer | null) ### WebhookListResponse - data (array) - total (integer) ### WebhookSubscription - id (integer | null) - url (string | null) - event_types (array) - sport (string | null) - event_id (integer | null) - is_active (boolean | null) - created_at (string | null) - signing_secret (string | null): Returned only on create — store it to verify Lumify-Signature. --- # OpenAPI-derived endpoint dump Cite-back: https://lumify.ai/openapi-llms.txt # Lumify — OpenAPI-derived endpoint dump for AI Agents # https://lumify.ai/openapi-llms.txt # # Auto-generated from the OpenAPI schema by scripts/gen_llms_full.py — do not edit # by hand. Embedded in /docs/llms-full.txt. For GEO orientation see /llms-full.txt; # for the short overview see /llms.txt. > Lumify is an agent-ready sports intelligence API: schedules, live scores, odds, > public betting splits, and AI-powered bet intelligence (confidence scores, > signal breakdowns, and LLM-generated narratives) across MLB, tennis, soccer > (FIFA World Cup + MLS, EPL, La Liga, Serie A, Bundesliga, Ligue 1, and UEFA > Champions League), NFL, NCAAF, NCAAB, NBA, and NHL. ## Base URL https://lumify.ai ## Authentication Every /v1/* request requires a Bearer API key: Authorization: Bearer lmfy-xxxxxx.yyyyyyyy... Create keys at https://lumify.ai/api-keys or programmatically via POST /api/agent/keys. ## Conventions - Timestamps: ISO-8601 UTC with trailing Z (2026-06-23T23:40:00Z). - Errors: {"error": {"code","message","status","doc_url"}, "detail": "..."} — switch on error.code. - Headers: X-RateLimit-{Limit,Remaining,Reset}, X-Credits-Used, X-Credits-Remaining. - Pagination: cursor-based ?after_id=&limit= (max 100); responses carry next_after_id. - Credits: most calls cost 1 credit; compound/multi-book calls cost 2-3 (see per-endpoint notes). ## Model Context Protocol (MCP) Connect an MCP client to https://lumify.ai/mcp (streamable HTTP), authenticating with your Lumify API key. Tools mirror the endpoints below. ## Endpoints ### GET /api/agent/credit-packs List purchasable credit packs Returns: CreditPackListResponse Fields: data: array, total: integer CreditPack fields: id: integer | null, name: string | null, credits: integer | null, price_cents: integer | null, bonus_credits: integer | null ### GET /api/agent/credits Get credit balance and usage Returns: AgentCreditsResponse Fields: tier: string | null, credits_used: integer | null, credit_limit: integer | null, bonus_credits: integer | null, total_remaining: integer | null, period_start: string | null, period_end: string | null, is_trial: boolean | null, is_trial_expired: boolean | null ### POST /api/agent/credits/topup Purchase a credit pack Request body: application/json object (see /openapi.json for schema). Returns: CreditTopupResponse Fields: success: boolean | null, credits_added: integer | null, bonus_credits: integer | null, pack_id: integer | null ### GET /api/agent/keys List API keys Returns: AgentApiKeyListResponse Fields: data: array, total: integer AgentApiKey fields: id: integer | null, name: string | null, key_prefix: string | null, scopes: array | null, created_at: string | null, last_used_at: string | null, expires_at: string | null, is_active: boolean | null, key: string | null ### POST /api/agent/keys Create an API key Request body: application/json object (see /openapi.json for schema). Returns: AgentApiKey Fields: id: integer | null, name: string | null, key_prefix: string | null, scopes: array | null, created_at: string | null, last_used_at: string | null, expires_at: string | null, is_active: boolean | null, key: string | null ### DELETE /api/agent/keys/{key_id} Revoke an API key Parameters: - key_id (integer, required, in=path): Returns: AgentApiKeyRevokeResponse Fields: revoked: boolean, id: integer | null ### POST /mcp MCP JSON-RPC endpoint (streamable HTTP) ### POST /v1/estimate Estimate the credit cost of a planned call (or batch of calls) Request body: application/json object (see /openapi.json for schema). Returns: EstimateResponse Fields: estimates: array, total_min_credits: integer, total_max_credits: integer EstimateResult fields: tool: string, min_credits: integer, max_credits: integer, note: string | null ### GET /v1/estimate/tools List the tools /v1/estimate understands Returns: object ### GET /v1/events List events Parameters: - sport (string | null, optional, in=query): Sport slug: nfl, nba, mlb, nhl, tennis, soccer… - league (string | null, optional, in=query): League slug: nfl, nba, atp, fifa_world_cup… - status (string | null, optional, in=query): Event status: scheduled|inprogress|final|… - date (string | null, optional, in=query): UTC date YYYY-MM-DD (single day) - from (string | null, optional, in=query): UTC start date YYYY-MM-DD - to (string | null, optional, in=query): UTC end date YYYY-MM-DD (inclusive) - season_id (integer | null, optional, in=query): Filter by season ID - team_id (integer | null, optional, in=query): Filter to events where this team participates. Resolve ids via GET /v1/teams?q=… - after_id (integer | null, optional, in=query): Cursor: return events with id > after_id - limit (integer, optional, in=query): - include_scores (boolean, optional, in=query): Inline participants + scores in each event. Intended for small result sets (≤ 200 events). - has_recommend (boolean | null, optional, in=query): When true, return only events with at least one recommended bet (has_recommend=1 in intelligence). Requires intelligence pipeline to have run. - sort (string, optional, in=query): Sort order: 'time' (chronological, default) or 'status' (Live → Delayed/Upcoming → Final → Cancelled/Postponed). Cursor pagination (after_id) is not supported with sort=status. Returns: EventListResponse Fields: events: array, total: integer, next_after_id: integer | null EventSummary fields: id: integer, name: string | null, sport: string | null, league: string | null, season_id: integer | null, starts_at: string | null, status: string | null, period: string | null, clock: string | null, venue: object | null ### POST /v1/events/batch Get multiple events in one call Fetch multiple events by id in a single round-trip — for agents that already have a list of ids (e.g. from GET /v1/events) and want full detail for each without issuing one GET per event. Body: {"event_ids": [1, 2, 3], "include_odds": bool, "include_intelligence": bool, "bookmaker": str} Max 25 ids per call. Duplicate ids are billed once. Credits are the sum of each event's normal GET /v1/events/{id} cost — billing fairness applies per event (unavailable odds/intelligence are free; ids that don't exist cost nothing and are returned under "not_found"). Request body: application/json object (see /openapi.json for schema). Returns: BatchEventsResponse Fields: events: array, not_found: array, total: integer EventDetail fields: id: integer, name: string | null, sport: string | null, league: string | null, season_id: integer | null, starts_at: string | null, status: string | null, period: string | null, clock: string | null, venue: object | null, participants: array, updated_at: string | null ### GET /v1/events/{event_id} Get an event Parameters: - event_id (integer, required, in=path): Lumify event ID - include_odds (boolean, optional, in=query): Inline current odds into the response under the 'odds' key. Scoped by bookmaker (default: pinnacle). +1 credit for a single book; +2 credits for bookmaker=all or a comma-separated list. - include_intelligence (boolean, optional, in=query): Inline bet intelligence (signals, bets, narratives) into the response under the 'intelligence' key. Costs +1 credit (total 2 for this call). - bookmaker (string | null, optional, in=query): Bookmaker filter for inlined odds (when include_odds=true) and for intelligence.bets[].market prices (when include_intelligence=true). Valid values: pinnacle, fanduel, draftkings, betmgm, caesars, bet365, circa, hardrock, betonline, all. Defaults to pinnacle. Returns: EventDetail Fields: id: integer, name: string | null, sport: string | null, league: string | null, season_id: integer | null, starts_at: string | null, status: string | null, period: string | null, clock: string | null, venue: object | null, participants: array, updated_at: string | null Participant fields: participant_id: integer | null, role: string | null, team: object | null, player: object | null, score: string | null, is_winner: boolean | null ### GET /v1/events/{event_id}/intelligence Get bet intelligence for an event Parameters: - event_id (integer, required, in=path): Lumify event ID - bookmaker (string | null, optional, in=query): Bookmaker to use for market.price and market.line in the response. Overrides the system default (pinnacle). Valid values: pinnacle, fanduel, draftkings, betmgm, caesars, bet365, circa, hardrock, betonline. Applies to the points-model shape only — probability-model sports (soccer/MLS) always report the book their assessment was priced against, since quoting a different book's price beside a probability computed from another would misstate fair_price and edge. Returns: IntelligenceResponse Fields: event_id: integer | null, available: boolean, odds_source: string | null, sport: string | null, league: string | null, players: object, has_recommend: boolean | null, analyst_take: string | null, match_overview: string | null, intelligence_updated_at: string | null, bets: array, matchup: object | null BetIntelligence fields: bet_type: string | null, player_role: string | null, player_id: integer | null, team_id: integer | null, player_name: string | null, market: object | null, computed_at: string | null, tier: string | null, confidence_score: number | null, confidence_score_pre_validator: number | null, coverage: number | null, signals: object | null, validator: object | null, narrative: string | null, rationale: array, attribution: array, probability: number | null, interval: array | null, p_model: number | null, p_market: number | null, blend_w: number | null, fair_price: integer | null, edge: number | null, sufficiency: number | null, phase: string | null, model_version: string | null, drivers: array, alignment: object | null, fair: object | null, edges_by_book: object | null, best: object | null ### GET /v1/events/{event_id}/odds Get current betting odds for an event Parameters: - event_id (integer, required, in=path): Lumify event ID - bookmaker (string | null, optional, in=query): Bookmaker filter. Default: pinnacle. Accepted values: pinnacle, fanduel, draftkings, betmgm, caesars, bet365, circa, hardrock, betonline, all, or a comma-separated list (e.g. 'fanduel,betmgm'). Single book = 1 credit; multiple or 'all' = 2 credits. Returns: OddsResponse Fields: event_id: integer | null, available: boolean, bookmakers: array, last_updated: string | null ### GET /v1/events/{event_id}/odds/history Get line movement history for an event Parameters: - event_id (integer, required, in=path): Lumify event ID - bookmaker (string | null, optional, in=query): Bookmaker filter. Default: pinnacle. Accepted values: pinnacle, fanduel, draftkings, betmgm, caesars, bet365, circa, hardrock, betonline, all, or a comma-separated list. Single book = 1 credit; multiple or 'all' = 2 credits. - limit (integer, optional, in=query): Max movements to return Returns: OddsHistoryResponse Fields: event_id: integer, movements: array, total: integer ### GET /v1/events/{event_id}/score Get an event's score Parameters: - event_id (integer, required, in=path): Lumify event ID Returns: ScoreResponse Fields: event_id: integer, status: string | null, finished: boolean | null, period: string | null, clock: string | null, scores: array, updated_at: string | null ScoreEntry fields: role: string | null, name: string | null, abbreviation: string | null, score: string | null, is_winner: boolean | null, period_scores: array ### GET /v1/events/{event_id}/splits Get public betting splits for an event Public bets% and handle% per side (moneyline, spread, total), with a cross-book consensus and per-bookmaker breakdown. Available for MLB, NBA, NHL, and NFL. Not available for tennis, soccer, or NCAAF (upstream Owls Insight v1 does not expose splits for these). available:false is free. Parameters: - event_id (integer, required, in=path): Lumify event ID Returns: SplitsResponse Fields: event_id: integer, available: boolean, captured_at: string | null, consensus: object, books: array ### GET /v1/events/{event_id}/stats Get raw team/match statistics for an event (Data layer) Soccer only. Returns deterministic aggregates from completed results + ESPN team box scores: team strength (club: rank/GD/W-D-L; WC: fifa_rank), recent form, H2H, rest days, home/away splits, boxscore rates (shots/SoT for&against, possession, save_rate), strength-of-schedule (sos), and lineups/formation. No market/odds data — see /odds for that. Explicit windows: rates_l5, rates_season, sos. No scoring/confidence — see /intelligence for that. available:false is free. Parameters: - event_id (integer, required, in=path): Lumify event ID Returns: StatsResponse Fields: event_id: integer, available: boolean, league_slug: string | null, profile: string | null, neutral_site: boolean | null, windows: StatsWindows | null, teams: StatsTeams | null, head_to_head: StatsHeadToHead | null, league_context: StatsLeagueContext | null StatsWindows fields: recent_form: integer, rates_l5: integer, rates_season: string | integer, head_to_head: integer, sos: integer StatsTeams fields: home: StatsTeam | null, away: StatsTeam | null StatsHeadToHead fields: window: integer, meetings: array, total: integer StatsLeagueContext fields: avg_goals_per_team: number | null ### GET /v1/events/{event_id}/stream Stream live score updates (SSE) Parameters: - event_id (integer, required, in=path): Lumify event ID ### GET /v1/health Health ### GET /v1/players List players Parameters: - sport (string | null, optional, in=query): Sport slug, e.g. 'tennis' or 'mlb' - q (string | null, optional, in=query): Name search (partial match) - country (string | null, optional, in=query): ISO 3166-1 alpha-3 country code, e.g. 'USA' - active (boolean | null, optional, in=query): Filter by active status - ranked (boolean | null, optional, in=query): If true, only players with a tennis ranking - after_id (integer, optional, in=query): Cursor: last player id from previous page - limit (integer, optional, in=query): Returns: PlayersListResponse Fields: data: array, has_more: boolean, next_after_id: integer | null Player fields: id: integer | null, slug: string | null, full_name: string | null, first_name: string | null, last_name: string | null, sport: string | null, country_code: string | null, birthdate: string | null, position: string | null, handedness: string | null, height_cm: integer | null, weight_kg: number | null, tennis_ranking: integer | null, tennis_ranking_points: integer | null, current_team_id: integer | null, current_team_name: string | null, is_active: boolean | null, retired_at: string | null, image_url: string | null ### GET /v1/players/{player_id} Get a player Parameters: - player_id (integer, required, in=path): Returns: Player Fields: id: integer | null, slug: string | null, full_name: string | null, first_name: string | null, last_name: string | null, sport: string | null, country_code: string | null, birthdate: string | null, position: string | null, handedness: string | null, height_cm: integer | null, weight_kg: number | null, tennis_ranking: integer | null, tennis_ranking_points: integer | null, current_team_id: integer | null, current_team_name: string | null, is_active: boolean | null, retired_at: string | null, image_url: string | null ### GET /v1/players/{player_id}/events List a player's events Parameters: - player_id (integer, required, in=path): - status (string | null, optional, in=query): Event status filter - from (string | null, optional, in=query): Start date YYYY-MM-DD - to (string | null, optional, in=query): End date YYYY-MM-DD - after_id (integer, optional, in=query): Cursor: last event id from previous page - limit (integer, optional, in=query): Returns: PlayerEventsResponse Fields: player_id: integer, data: array, has_more: boolean, next_after_id: integer | null ### POST /v1/query Natural-language event search Map a natural-language query to the same filters GET /v1/events accepts, then run that exact list query — results are identical to, and as fresh as, calling /v1/events directly. This is a small rule-based mapper, not an LLM call — see the module-level `_parse_nl_query` docstring for exactly what it recognizes. Body: {"query": "live nfl games today", "limit": 25} An explicit top-level "limit" overrides one parsed from the text. Costs 1 credit, same as GET /v1/events — interpreting the query text is free. Request body: application/json object (see /openapi.json for schema). Returns: NLQueryResponse Fields: query: string, interpreted: NLQueryFilters, unrecognized_terms: array, equivalent_request: string, events: array, total: integer, next_after_id: integer | null NLQueryFilters fields: sport: string | null, status: string | null, date: string | null, from: string | null, to: string | null, limit: integer | null EventSummary fields: id: integer, name: string | null, sport: string | null, league: string | null, season_id: integer | null, starts_at: string | null, status: string | null, period: string | null, clock: string | null, venue: object | null ### GET /v1/seasons List seasons Parameters: - sport (string | null, optional, in=query): Filter by sport slug (e.g. nhl, nba, soccer) - current_only (boolean, optional, in=query): Return only currently active seasons (default true). Pass false to include historical/past seasons. Returns: SeasonsListResponse Fields: seasons: array, total: integer Season fields: id: integer | null, year: integer | null, name: string | null, phase: string | null, start_date: string | null, end_date: string | null, is_current: boolean | null, sport: object | null, league: object | null ### GET /v1/sports List sports Parameters: - active_only (boolean, optional, in=query): Return only active sports (default true) Returns: SportsListResponse Fields: sports: array, total: integer Sport fields: id: integer | null, slug: string | null, name: string | null, is_team_sport: boolean | null, leagues: array ### GET /v1/teams List teams Parameters: - sport (string | null, optional, in=query): Sport slug, e.g. 'nba' or 'nhl' - league (string | null, optional, in=query): League slug, e.g. 'nba' - conference (string | null, optional, in=query): Conference filter - division (string | null, optional, in=query): Division filter - country (string | null, optional, in=query): ISO country code, e.g. 'USA' - q (string | null, optional, in=query): Team name search (partial match) - active (boolean | null, optional, in=query): Filter by active status - after_id (integer, optional, in=query): Cursor: last team id from previous page - limit (integer, optional, in=query): Returns: TeamsListResponse Fields: data: array, has_more: boolean, next_after_id: integer | null Team fields: id: integer | null, slug: string | null, name: string | null, short_name: string | null, abbreviation: string | null, sport: string | null, league: string | null, city: string | null, state: string | null, country_code: string | null, conference: string | null, division: string | null, venue: object | null, is_active: boolean | null ### GET /v1/teams/{team_id} Get a team Parameters: - team_id (integer, required, in=path): Lumify team ID Returns: Team Fields: id: integer | null, slug: string | null, name: string | null, short_name: string | null, abbreviation: string | null, sport: string | null, league: string | null, city: string | null, state: string | null, country_code: string | null, conference: string | null, division: string | null, venue: object | null, is_active: boolean | null ### POST /v1/trial-key Issue an instant, unauthenticated trial API key Issues a throwaway API key with no signup or email verification — 100 lifetime credits, 14-day expiry, one per network per 7 days. Requires a valid Cloudflare Turnstile token. Intended for the 'Get instant trial key' button on /docs/ai, not for building a persistent integration — use /register for that. Request body: application/json object (see /openapi.json for schema). ### GET /v1/webhooks List webhook subscriptions Returns: WebhookListResponse Fields: data: array, total: integer WebhookSubscription fields: id: integer | null, url: string | null, event_types: array, sport: string | null, event_id: integer | null, is_active: boolean | null, created_at: string | null, signing_secret: string | null ### POST /v1/webhooks Create a webhook subscription Request body: application/json object (see /openapi.json for schema). Returns: WebhookCreateResponse Fields: id: integer | null, url: string | null, event_types: array, sport: string | null, event_id: integer | null, is_active: boolean | null, created_at: string | null, signing_secret: string | null ### DELETE /v1/webhooks/{subscription_id} Delete a webhook subscription Parameters: - subscription_id (integer, required, in=path): Returns: WebhookDeleteResponse Fields: deleted: boolean, id: integer | null ### GET /v1/webhooks/{subscription_id}/deliveries List delivery history for a webhook subscription Parameters: - subscription_id (integer, required, in=path): - after_id (integer | null, optional, in=query): Cursor: return deliveries with id < after_id (list is newest-first) - limit (integer, optional, in=query): - success (boolean | null, optional, in=query): Filter to successful (2xx) or failed deliveries - given_up (boolean | null, optional, in=query): Filter to deliveries that exhausted retries (or not) - event_type (string | null, optional, in=query): Filter by event type (score, status, line_move, intelligence) Returns: WebhookDeliveryListResponse Fields: data: array, next_after_id: integer | null WebhookDeliveryItem fields: id: integer | null, event_type: string | null, event_id: integer | null, attempt: integer | null, parent_delivery_id: integer | null, status_code: integer | null, success: boolean | null, error: string | null, given_up: boolean | null, next_retry_at: string | null, delivered_at: string | null ## Error codes - bad_request (400): malformed request or conflicting parameters - unauthorized (401): missing or invalid API key - payment_required (402): credit top-up or payment method required — specific codes: insufficient_credits, daily_credit_cap_exceeded (see error.code; includes upgrade_url / topup_url; daily_credit_cap_exceeded includes resets_at for the rolling 24h free-tier window) - forbidden (403): API key lacks scope for the requested sport (see error.upgrade_url) - not_found (404): resource does not exist - validation_error (422): parameter failed validation (see error.errors[]) - rate_limit_exceeded (429): slow down; see Retry-After and error.retry_after - internal_error (500) / service_unavailable (503): transient server error — retry with backoff ## SDKs - Python: pip install lumify-sdk (import lumify) pip install 'lumify-sdk[asyncio]' (from lumify.aio import AsyncLumify) - TypeScript: npm install @lumifyai/sdk ## Support - Docs: https://lumify.ai/docs - Contact: https://lumify.ai/contact-us # Measured size: ~52873 tokens (UTF-8 chars÷4, generated 2026-08-02).