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 overview for auth and rate limits.
For agents: the machine-readable twin of this entire reference is llms-full.txt (generated from the OpenAPI schema), or explore interactively via ReDoc / /openapi.json. Discovery/manifest for autonomous agents lives at /.well-known/agent.json.
List 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
curl https://lumify.ai/v1/sports \
-H "Authorization: Bearer YOUR_API_KEY"
resp = requests.get( "https://lumify.ai/v1/sports", headers={"Authorization": "Bearer YOUR_API_KEY"}, ) sports = resp.json()["sports"]
const { sports } = await fetch("https://lumify.ai/v1/sports", { headers: { "Authorization": "Bearer YOUR_API_KEY" }, }).then(r => r.json());
Response
{
"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
Returns the full season calendar across all leagues. Use this endpoint to look up a season_id before filtering events by season, or to find historical seasons for a sport.
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 | false |
When true, return only seasons currently in progress (is_current = true). |
Request
curl "https://lumify.ai/v1/seasons?sport=nhl¤t_only=true" \
-H "Authorization: Bearer YOUR_API_KEY"
resp = requests.get( "https://lumify.ai/v1/seasons", params={"sport": "nhl", "current_only": "true"}, headers={"Authorization": "Bearer YOUR_API_KEY"}, ) seasons = resp.json()["seasons"]
const { seasons } = await fetch( "https://lumify.ai/v1/seasons?sport=nhl¤t_only=true", { headers: { "Authorization": "Bearer YOUR_API_KEY" } } ).then(r => r.json());
Response
{
"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
}
List 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 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
# 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"
# All live NHL games today resp = requests.get( "https://lumify.ai/v1/events", params={"sport": "nhl", "status": "inprogress"}, headers={"Authorization": "Bearer YOUR_API_KEY"}, ) events = resp.json()["events"] # Iterate all pages after_id = None while True: params = {"sport": "nba", "from": "2026-05-10", "to": "2026-05-17", "limit": 50} if after_id: params["after_id"] = after_id resp = requests.get("https://lumify.ai/v1/events", params=params, headers={"Authorization": "Bearer YOUR_API_KEY"}) body = resp.json() # … process body["events"] … after_id = body.get("next_after_id") if not after_id: break
// All live NHL games today const { events } = await fetch( "https://lumify.ai/v1/events?sport=nhl&status=inprogress", { headers: { "Authorization": "Bearer YOUR_API_KEY" } } ).then(r => r.json()); // Iterate all pages async function fetchAllEvents(sport) { const all = [], headers = { "Authorization": "Bearer YOUR_API_KEY" }; let afterId = null; do { const qs = new URLSearchParams({ sport, limit: 100, ...(afterId && { after_id: afterId }) }); const body = await fetch(`https://lumify.ai/v1/events?${qs}`, { headers }).then(r => r.json()); all.push(...body.events); afterId = body.next_after_id; } while (afterId); return all; }
Response
{
"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
}
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.
Date filter note: ?date and ?from / ?to are mutually exclusive. Combining them returns 400.
Natural-language event search
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
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
{
"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
}
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
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 up to 2 extra round trips. Each include parameter adds +1 credit to the call cost. Use /v1/events/{id}/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. Adds +1 credit (total 2 credits). |
include_intelligence | boolean | optional | false |
When true, embeds the bet intelligence payload under an intelligence key — same shape as GET /v1/events/{id}/intelligence. Adds +1 credit (total 2 credits). |
bookmaker | string | optional | system default | Bookmaker for intelligence.bets[].market prices when include_intelligence=true. Valid values: pinnacle, fanduel, draftkings. Defaults to the system-configured book. |
Credit costs: A plain call costs 1 credit. Adding include_odds=true costs 2 credits. Adding include_intelligence=true costs 2 credits. Using both costs 3 credits — cheaper than fetching event + odds + intelligence as three separate calls (3 credits each).
Request
# 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"
# Basic — 1 credit resp = requests.get( "https://lumify.ai/v1/events/4812", headers={"Authorization": "Bearer YOUR_API_KEY"}, ) event = resp.json() # Compound — event + odds + intelligence in one call (3 credits) resp = requests.get( "https://lumify.ai/v1/events/4812", headers={"Authorization": "Bearer YOUR_API_KEY"}, params={"include_odds": "true", "include_intelligence": "true"}, ) event = resp.json() # event.odds and event.intelligence are now populated
// Basic — 1 credit const event = await fetch("https://lumify.ai/v1/events/4812", { headers: { "Authorization": "Bearer YOUR_API_KEY" }, }).then(r => r.json()); // Compound — event + odds + intelligence in one call (3 credits) const eventFull = await fetch( "https://lumify.ai/v1/events/4812?include_odds=true&include_intelligence=true", { headers: { "Authorization": "Bearer YOUR_API_KEY" } } ).then(r => r.json()); // eventFull.odds and eventFull.intelligence are populated
Response
// 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
// 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 */ ] } }
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.
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
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}.
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 on each event (+1 credit per event when odds are available). |
include_intelligence | boolean | optional | Inline bet intelligence on each event (+1 credit per event when available). |
bookmaker | string | optional | Bookmaker for inlined odds; defaults to pinnacle. Use all for every book. |
Request
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
{
"events": [ /* EventDetail objects, same shape as GET /v1/events/{id} */ ],
"not_found": [999],
"total": 2
}
Get an event's 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
curl https://lumify.ai/v1/events/4812/score \
-H "Authorization: Bearer YOUR_API_KEY"
import time # Poll every 30 s while the game is live while True: resp = requests.get( "https://lumify.ai/v1/events/4812/score", headers={"Authorization": "Bearer YOUR_API_KEY"}, ) score = resp.json() print(score["period"], score["scores"]) if score["status"] != "inprogress": break time.sleep(30)
// Poll every 30 s while the game is live async function pollScore(eventId) { const headers = { "Authorization": "Bearer YOUR_API_KEY" }; while (true) { const score = await fetch(`https://lumify.ai/v1/events/${eventId}/score`, { headers }) .then(r => r.json()); renderScore(score); if (score.status !== "inprogress") break; await new Promise(res => setTimeout(res, 30_000)); } }
Response
{
"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'" |
Get bet intelligence for an event
Returns the full bet intelligence payload for an event: confidence scores for each bet type, individual signal breakdowns, validator output, and LLM-generated narratives. 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.
Sports coverage: Available for MLB, ATP/WTA tennis, FIFA World Cup 2026 soccer, 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 (FIFA World Cup): Soccer uses a 3-way moneyline model. Bet tokens are ML_HOME, ML_AWAY, and ML_DRAW — each 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). Filter events with sport=soccer&league=fifa_world_cup.
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
curl https://lumify.ai/v1/events/4821/intelligence \
-H "Authorization: Bearer YOUR_API_KEY"
resp = requests.get( "https://lumify.ai/v1/events/4821/intelligence", headers={"Authorization": "Bearer YOUR_API_KEY"}, ) intel = resp.json()
const intel = await fetch("https://lumify.ai/v1/events/4821/intelligence", { headers: { "Authorization": "Bearer YOUR_API_KEY" }, }).then(r => r.json());
Response
{
"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, 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, 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, max 8)
"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, 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. |
players | object | Maps player_1 and player_2 to {name, player_id}. Use player_id to cross-reference with participants[].player.id in GET /v1/events/{id}. |
has_recommend | boolean | null | true if at least one bet meets the recommendation threshold; false if all avoided; null if the pipeline hasn't run yet |
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. |
Bet object fields
| 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 or player_2 — matches participants[].role in GET /v1/events/{id}. null for OVER/UNDER. |
player_id | integer | null | Permanent player identity — matches participants[].player.id in GET /v1/events/{id}. Stable across all events. null for OVER/UNDER and team sports. |
team_id | integer | null | Permanent team identity for team-sport bets (MLB, NHL etc). null for player-sport bets (tennis) and OVER/UNDER. |
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 (FIFA Ranking Edge), signal_form 15 (Recent Form), signal_serve_rtn 12 (Attack/Defense Edge), signal_h2h 10 (Head-to-Head), signal_surface 8 (Tournament Context), signal_market_odds 8, signal_fatigue 8 (Goal Environment), signal_surface_frm 5 (Travel/Neutral), 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._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. |
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, or sport=nfl / sport=ncaaf during football season. Then fetch /v1/events/{id}/intelligence for any game you want bet analysis on. 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. For soccer, all three moneyline outcomes (ML_HOME, ML_AWAY, ML_DRAW) are scored independently — check each one.
Get betting splits for an event
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.
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. 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
curl https://lumify.ai/v1/events/479/splits \ -H "Authorization: Bearer $LUMIFY_API_KEY"
Response
{
"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. |
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.
List 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
# Eastern Conference NBA teams curl "https://lumify.ai/v1/teams?sport=nba&conference=Eastern" \ -H "Authorization: Bearer YOUR_API_KEY"
Response
{
"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
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
curl "https://lumify.ai/v1/teams/1" \
-H "Authorization: Bearer YOUR_API_KEY"
Response
{
"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
}
List 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
# 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"
# Top-ranked tennis players resp = requests.get( "https://lumify.ai/v1/players", params={"sport": "tennis", "ranked": "true", "limit": 10}, headers={"Authorization": "Bearer YOUR_API_KEY"}, ) players = resp.json()["data"]
const { data: players } = await fetch( "https://lumify.ai/v1/players?sport=tennis&ranked=true&limit=10", { headers: { "Authorization": "Bearer YOUR_API_KEY" } } ).then(r => r.json());
Response
{
"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
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
curl "https://lumify.ai/v1/players/1" \
-H "Authorization: Bearer YOUR_API_KEY"
player = requests.get( "https://lumify.ai/v1/players/1", headers={"Authorization": "Bearer YOUR_API_KEY"}, ).json()
const player = await fetch("https://lumify.ai/v1/players/1", { headers: { "Authorization": "Bearer YOUR_API_KEY" }, }).then(r => r.json());
Response
{
"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
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
# 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"
events = requests.get( "https://lumify.ai/v1/players/1/events", params={"status": "scheduled"}, headers={"Authorization": "Bearer YOUR_API_KEY"}, ).json()["data"]
const { data: events } = await fetch( "https://lumify.ai/v1/players/1/events?status=scheduled", { headers: { "Authorization": "Bearer YOUR_API_KEY" } } ).then(r => r.json());
Response
{
"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
}
Get current odds for an event
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.
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, all, or a comma-separated combination (e.g. fanduel,draftkings). 1 credit for a single bookmaker · 2 credits for multiple or all. |
Request — Pinnacle only (default, 1 credit)
curl https://lumify.ai/v1/events/4821/odds \ -H "Authorization: Bearer $LUMIFY_API_KEY"
Request — all bookmakers (2 credits)
curl "https://lumify.ai/v1/events/4821/odds?bookmaker=all" \ -H "Authorization: Bearer $LUMIFY_API_KEY"
Response
{
"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, fanduel. The set of available bookmakers depends on what odds have been ingested for that event. |
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
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, 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
{
"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
}
Stream live score updates (SSE)
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 automatically when the event finishes or after 5 minutes, whichever comes first. Use this instead of polling /v1/events/{id}/score when you want push-based updates.
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.
Path parameters
| Parameter | Type | Description | |
|---|---|---|---|
id | integer | required | Lumify event ID. Returns an event: error message if the event does not exist. |
Request
curl -N "https://lumify.ai/v1/events/4812/stream" \
-H "Authorization: Bearer YOUR_API_KEY"
// EventSource cannot set headers, so pass the key as a query param const es = new EventSource(`https://lumify.ai/v1/events/4812/stream?api_key=YOUR_API_KEY`); es.addEventListener("score", (e) => { const snap = JSON.parse(e.data); renderScore(snap); }); es.addEventListener("done", () => es.close());
Response
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}
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.
Create a subscription
| 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. |
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"}'
{
"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. |
# 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"
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
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"]}'
{
"id": 17,
"name": "prod-worker-1",
"key": "lmfy-abc123.def456...", // shown once — store it now
"scopes": ["all"],
"created_at": "2026-06-01T12:00:00Z"
}
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
curl https://lumify.ai/api/agent/credits \
-H "Authorization: Bearer YOUR_API_KEY"
{
"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
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}'
Payment failures return 402 for card/payment issues (card_declined, payment_method_required) or 400 for an invalid pack_id.
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
{
"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) |
All timestamps are stored and returned in UTC. Use the venue timezone field to convert to local time for display.