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 Markdown twin of this page is /docs/reference.md. Full concatenated technical payload: /docs/llms-full.txt. Endpoint dump alone: /openapi-llms.txt. GEO orientation (FAQ/pricing/coverage): /llms-full.txt. Explore interactively via ReDoc / /openapi.json. Discovery/manifest for autonomous agents lives at /.well-known/agent.json.

API key Sign in, or get an instant trial key — no signup required.
Reference Data Sports catalogue and season lookup — stable metadata used to filter all other endpoints. MCP: list_sports, list_seasons →

List sports

GET /v1/sports Open in Playground

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

ParameterTypeDefaultDescription
active_onlyboolean optionaltrue When true, exclude sports marked inactive. Pass false to include all sports regardless of status.

Request

curl
curl https://lumify.ai/v1/sports \
  -H "Authorization: Bearer YOUR_API_KEY"
Python
resp = requests.get(
    "https://lumify.ai/v1/sports",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
sports = resp.json()["sports"]
JavaScript
const { sports } = await fetch("https://lumify.ai/v1/sports", {
  headers: { "Authorization": "Bearer YOUR_API_KEY" },
}).then(r => r.json());

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 Open in Playground

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

ParameterTypeDefaultDescription
sportstring optional— Filter to seasons for a single sport slug (e.g. nhl, tennis). Returns an empty list for unknown slugs.
current_onlyboolean optionaltrue When true (default), return only seasons currently in progress (is_current = true). Pass false to include historical seasons.

Request

curl
curl "https://lumify.ai/v1/seasons?sport=nhl&current_only=true" \
  -H "Authorization: Bearer YOUR_API_KEY"
Python
resp = requests.get(
    "https://lumify.ai/v1/seasons",
    params={"sport": "nhl", "current_only": "true"},
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
seasons = resp.json()["seasons"]
JavaScript
const { seasons } = await fetch(
  "https://lumify.ai/v1/seasons?sport=nhl&current_only=true",
  { headers: { "Authorization": "Bearer YOUR_API_KEY" } }
).then(r => r.json());

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 → Guide: Track live odds movement →

List events

GET /v1/events Open in Playground

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

ParameterTypeDefaultDescription
sportstringoptional— Filter by sport slug: nfl, nba, mlb, nhl, tennis, soccer, ncaaf, ncaab. Unknown slugs return an empty list.
leaguestringoptional— Narrow to a specific league slug (e.g. atp, mls). More specific than sport.
statusstringoptional— Filter by lifecycle status. See the Status Values table. Returns 400 for unrecognised values.
datestringoptional— Single-day filter. Format: YYYY-MM-DD (UTC). Mutually exclusive with from / to — combining them returns 400.
fromstringoptional— Range start date (UTC, inclusive). Pair with to. Format: YYYY-MM-DD.
tostringoptional— Range end date (UTC, inclusive). Max range: 90 days. Returns 400 if exceeded.
season_idintegeroptional— Restrict to a single season. Obtain the ID from /v1/seasons.
team_idintegeroptional— 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_idintegeroptional— Pagination cursor. Pass the next_after_id from the previous response to retrieve the next page.
limitintegeroptional25 Page size. Range: 1–100.
include_scoresbooleanoptionalfalse 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). Shares the 20/min score-poll budget with GET /events/{id}/score.
has_recommendbooleanoptional— 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.
sortstringoptionaltime 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

curl
# 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"
Python
# 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
JavaScript
// 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

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
      "inprogress_since":     null,                      // first live ingest; stays set after final
      "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

POST /v1/query Open in Playground

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

FieldTypeDescription
querystringrequired Free text, max 500 characters. Example: live nfl games today.
limitintegeroptional Overrides any limit parsed from the text. Range: 1–100.

What the mapper recognizes

FilterExamples
sportnfl, nba, mlb, nhl, tennis, soccer, ncaaf, ncaab; aliases hockey, basketball, baseball, american football, college football, college basketball. Bare football is ambiguous and left unrecognized.
statuslive / in progress / in-progress → inprogress; final, upcoming, postponed, cancelled, delayed, suspended, walkover.
date / rangetoday, 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.
limitA bare integer 1–100 in the text (e.g. 5 nhl games), overridden by the body field when present.

Request

curl
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
}

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} Open in Playground

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). Includes do not add credits — one request is 1 credit. Use /v1/events/{id}/score when you only need live score data.

Path parameters

ParameterTypeDescription
idintegerrequired Lumify event ID. Non-integer values return 422. Unknown IDs return 404.

Query parameters

ParameterTypeDefaultDescription
include_oddsbooleanoptionalfalse When true, embeds the current odds payload under an odds key — same shape as GET /v1/events/{id}/odds, scoped by bookmaker (default: pinnacle). Does not add credits — the event call stays 1 credit.
include_intelligencebooleanoptionalfalse When true, embeds the bet intelligence payload under an intelligence key — same shape as GET /v1/events/{id}/intelligence. Does not add credits — the event call stays 1 credit.
bookmakerstringoptionalsystem 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, westgate, wynn, south_point, stations, hardrock, betonline, betr, betrivers, lowvig, bovada, all. Defaults to pinnacle.
include_altsbooleanoptionalfalse When include_odds=true, include alternate spread/total rungs. Default is mains only.
CallCredits
Event only1
include_odds=true and/or include_intelligence=true1 — includes do not add credits

Request

curl
# Basic — 1 credit
curl https://lumify.ai/v1/events/4812 \
  -H "Authorization: Bearer YOUR_API_KEY"

# Compound — event + odds + intelligence in one call (1 credit)
curl "https://lumify.ai/v1/events/4812?include_odds=true&include_intelligence=true" \
  -H "Authorization: Bearer YOUR_API_KEY"
Python
# 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 (1 credit)
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
JavaScript
// 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 (1 credit)
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

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
  "inprogress_since":     "2026-05-10T23:05:12Z", // first live ingest; stays set after final
  "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", "image_url": "https://lumify.ai/media/teams/nhl/1.png"
      },
      "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", "image_url": "https://lumify.ai/media/teams/nhl/2.png" },
      "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",
  "inprogress_since":     "2026-05-08T22:14:03Z", // first ball — not starts_at (session floor)
  "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,
    "match_overview": null,
    "rationale": 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. inprogress_since is first ball (first live ingest) — null until the match actually starts, and it stays set after final. Use court and order_of_play to understand draw position.

Batch get events

POST /v1/events/batch Open in Playground

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

FieldTypeDescription
event_idsinteger[]required 1–25 event ids. Order is preserved in the response.
include_oddsbooleanoptional Inline current odds scoped by bookmaker (default: pinnacle). Does not add credits.
include_intelligencebooleanoptional Inline bet intelligence on each event. Does not add credits.
bookmakerstringoptional Bookmaker for inlined odds and intelligence market prices. Defaults to pinnacle. Valid: pinnacle, fanduel, draftkings, betmgm, caesars, bet365, circa, westgate, wynn, south_point, stations, hardrock, betonline, betr, betrivers, lowvig, bovada, all.

Request

curl
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 Open in Playground

Lightweight score snapshot. Returns status, period, clock, and per-participant scores. Live cache is 15 seconds (status=inprogress / suspended). Capped at 20 calls/min per key, shared with include_scores=true list polls — tighter than the plan rpm so a score firehose cannot starve other customers. For live updates use GET /v1/events/{id}/stream.

Path parameters

ParameterTypeDescription
idintegerrequired Lumify event ID. Non-integer values return 422. Unknown IDs return 404.

Request

curl
curl https://lumify.ai/v1/events/4812/score \
  -H "Authorization: Bearer YOUR_API_KEY"
Python
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)
JavaScript
// 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

json — in-progress game
{
  "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.

SportExample periodExample 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), predictive probability / fair price / Price overlay (Intelligence), and public betting splits per event. MCP: get_stats, get_player_props, get_intelligence, get_splits, get_injuries → Guide: Build an MCP betting-splits agent → Guide: Pull a full intelligence report →

Get raw match statistics for an event

GET /v1/events/{id}/stats Open in Playground

Returns the deterministic, reproducible team and match statistics behind an event — computed directly from completed results and box scores 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.

Eight sports today. Other sports return 400. The payload is sport-specific — branch on the event's sport / league_slug, not on whether a given key is present.

SportShapeNotes
Soccerteams.home/awayClub leagues. Fields
MLBteams.home/awayIncludes this-event player_box (final and in-progress). Fields
Tennisplayers.player_1/player_2Main-draw singles. Doubles / qualifying → available: false (free). Fields
NFLteams.home/awayTeam rates + this-event player_box (final and in-progress). Player settleables: /player-props. Fields
NCAAFteams.home/awaySame football catalog as NFL (incl. player_box), scoped to ncaaf. Fields
NBAteams.home/awayTeam rates + this-event player_box (final and in-progress). Player settleables: /player-props. Fields
NCAABteams.home/awaySame basketball catalog as NBA (incl. player_box), scoped to ncaab. Fields
NHLteams.home/awayTeam rates + this-event player_box (skaters/goalies, final and in-progress). Player settleables: /player-props. Fields
  • Tennis form / record / rest / H2H count final / complete only — walkovers excluded, retirements included.
  • NFL / NCAAF / NBA / NCAAB exclude season_type=preseason from form, rates, record, and H2H.

Path parameters

NameTypeDescription
idintegerLumify event ID.

Request

cURL
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",
      "image_url":    "https://lumify.ai/media/teams/soccer/210.png",
      "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,
        "offsides": 1.8,
        "tackles": 14.0,
        "interceptions": 9.2,
        "blocked_shots": 3.4,
        "assists": 1.6,
        "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,
    "sos": 5
  },
  "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 — Soccer

FieldTypeDescription
availablebooleanfalse when both teams for this fixture haven't resolved yet. No charge in that case.
profilestring"world_cup" or "club" — which SoccerLeagueProfile this fixture uses, driving which source values appear below.
windowsobjectExplicit sample depths so agents never guess: recent_form (5), rates_l5 (5), rates_season ("season"), head_to_head (10), sos (5).
teams.{home,away}.recent_formobjectUp 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_daysinteger | nullDays 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_strengthobjectClub leagues (source: "league_table_ppg"): season table row — ppg, games, points, w/d/l, gf/ga/gd, and ordinal rank (points → GD → GF).
teams.{home,away}.sosobject | nullStrength-of-schedule lite — average opponent PPG/rank across the last 5 completed league games (source: "opp_ppg_avg"). null until enough league games exist.
teams.{home,away}.lineupobjectFormation + starters/bench from ingested rosters (available: false until then). Each player has name, position, jersey, formation_place, espn_athlete_id.
teams.{home,away}.venueobjectClub 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).
teams.{home,away}.rates_l5objectPer-game averages from the last 5 completed matches with team box scores (source: "event_stats_avg"): shots / SoT, possession, corners, cards, saves / save_rate, plus offsides / tackles / interceptions / blocked_shots / assists (team assists summed from per-player goalAssists). Shot location is not exposed. games is 0 (rates null) until boxes land; field_games is how many of those games had each field.
teams.{home,away}.rates_seasonobjectSame shape as rates_l5 but averaged over the current season. window is "season" (no window_size).
head_to_headobjectUp 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_teamnumber | nullSeason-to-date league-wide average goals scored per team per game. null before enough club-season games exist.

Response — MLB

JSON
{
  "event_id":     88410,
  "available":    true,
  "league_slug":  "mlb",
  "teams": {
    "home": {
      "team_id":      120,
      "name":         "Washington Nationals",
      "abbreviation": "WSH",
      "rest_days":    1,
      "recent_form": {
        "window":        5,
        "results":       ["W", "L"],
        "runs_scored":   [11, 3],
        "runs_allowed":  [4, 6]
      },
      "record": {
        "source":           "season_to_date",
        "wins":             55,
        "losses":           53,
        "games":            108,
        "win_pct":          0.5093,
        "run_differential": 14
      },
      "rates_l5": {
        "source": "player_game_stats_sum",
        "window": "l5",
        "window_size": 5,
        "games": 5,
        "batting_avg": 0.244,
        "obp": 0.318,
        "slg": 0.392,
        "ops": 0.710,
        "era": 4.72,
        "whip": 1.38,
        /* + at_bats, hits, runs, home_runs, walks, strikeouts, doubles, triples, rbi, hit_by_pitch, stolen_bases, total_bases, innings_pitched, earned_runs, hits_allowed, walks_allowed, strikeouts_pitched, home_runs_allowed, k9, bb9 */
        "field_games": { /* per-field sample size, e.g. "hits": 5 */ }
      },
      "rates_season": { /* same fields; window: "season", no window_size */ },
      "starting_pitcher": {
        "available": true,
        "player_id": 674841,
        "name": "Home Starter",
        "games_started": 22,
        "innings_pitched": 128.1,
        "era": 3.92,
        "whip": 1.21,
        "k9": 8.4,
        "bb9": 2.6
      },
      "lineup": {
        "available": true,
        "batting_order": [ /* 9 batters, ordered, each {name, position, player_id, role, batting_order, starter} */ ],
        "starting_pitcher": { "name": "Home Starter", "player_id": 674841, "role": "pitcher", "starter": true },
        "bench": [ /* pinch hitters / relievers / subs who appeared */ ],
        "captured_at": "2026-08-03 22:15:00"
      },
      "player_box": {
        "available": true,
        "as_of": "2026-08-03T22:15:00",
        "batters": [ /* {player_id, name, position, at_bats, runs, hits, rbi, doubles, triples, home_runs, walks, strikeouts, stolen_bases, caught_stealing, hit_by_pitch, left_on_base} */ ],
        "pitchers": [ /* {player_id, name, position, innings_pitched, outs, games_started, earned_runs, hits_allowed, runs_allowed, walks_allowed, strikeouts_pitched, home_runs_allowed, hit_batsmen, pitches_thrown, strikes_thrown, batters_faced} */ ]
      }
    },
    "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_runs": 3, "away_runs": 1 }
    ],
    "total": 1
  }
}

Response fields — MLB

FieldTypeDescription
windowsobjectrecent_form (5), rates_l5 (5), rates_season ("season"), head_to_head (10) — no sos, unlike soccer.
teams.{home,away}.recent_formobjectUp to the last window (5) completed results, most-recent-first. results is "W"/"L" (no ties in MLB); runs_scored/runs_allowed align by index.
teams.{home,away}.rest_daysinteger or nullDays since this team's last completed game in the same league. null if this is their first game of the season.
teams.{home,away}.recordobjectSeason-to-date source: "season_to_date" record: wins, losses, games, win_pct, runs_for, runs_against, run_differential.
teams.{home,away}.rates_l5 / rates_seasonobjectTeam batting/pitching rate aggregates (source: "player_game_stats_sum") — every player's per-game box-score line summed to the team-game level, then averaged/derived across the window. batting_avg/obp/slg/ops/era/whip/k9/bb9 are derived from the sum of their component totals, not an average of per-game ratios. Counting fields include rbi, triples, total_bases, and hit_by_pitch. obp uses (H+BB+HBP)/(AB+BB+HBP) — sacrifice flies are not in Path A ingest. Includes field_games per field, same convention as soccer.
teams.{home,away}.starting_pitcherobjectThe game's own starting pitcher's season-to-date era/whip/k9/bb9, derived via outs-based ratio-of-sums from his own box-score rows. available: false until a post-final box score with lineup lands — pregame probable pitchers stay on /intelligence, not /stats.
teams.{home,away}.lineupobjectbatting_order (9 batters, ordered) + starting_pitcher + bench (pinch hitters/relievers/subs) from the ingested final box score. No formation field — not a baseball concept. available: false until post-game ingest.
teams.{home,away}.player_box.battersobject[]This event's batting lines (sorted by name): at_bats, runs, hits, rbi, doubles, triples, home_runs, walks, strikeouts, stolen_bases, caught_stealing, hit_by_pitch, left_on_base. No per-player AVG/OBP. Two-way players also appear under pitchers.
teams.{home,away}.player_box.pitchersobject[]This event's pitching lines (starter first, then outs desc): innings_pitched (baseball notation), outs, games_started, earned_runs, hits_allowed, runs_allowed, walks_allowed, strikeouts_pitched, home_runs_allowed, hit_batsmen, pitches_thrown, strikes_thrown, batters_faced. No per-player ERA. player_box.available is false until this game has ingested player lines (live during in-progress once rows land). player_box.as_of is the newest row timestamp. Player settleables: /player-props.
head_to_headobjectUp to the last window (10) meetings between these two teams, most-recent-first. meetings use home_runs/away_runs — not home_goals/away_goals.

Response — NFL

JSON
{
  "event_id":    12001,
  "available":   true,
  "league_slug": "nfl",
  "windows": {
    "recent_form": 5,
    "rates_l5": 5,
    "rates_season": "season",
    "head_to_head": 10
  },
  "teams": {
    "home": {
      "team_id": 12,
      "name": "Kansas City Chiefs",
      "abbreviation": "KC",
      "rest_days": 7,
      "recent_form": {
        "window": 5,
        "results": ["W", "L"],
        "points_scored": [27, 17],
        "points_allowed": [20, 24]
      },
      "record": {
        "source": "season_to_date",
        "wins": 10, "losses": 3, "games": 13,
        "win_pct": 0.7692,
        "points_for": 312, "points_against": 245,
        "point_differential": 67
      },
      "rates_l5": {
        "source": "event_stats_avg", "window": "l5",
        "window_size": 5, "games": 5,
        "total_yards": 368.2, "yards_allowed": 312.4,
        "passing_yards": 241.0, "rushing_yards": 127.2,
        "turnovers": 1.2, "third_down_pct": 42.5
      },
      "rates_season": { /* same keys as rates_l5; window "season" */ }
    },
    "away": { /* same shape as "home" */ }
  },
  "head_to_head": {
    "window": 10,
    "meetings": [
      { "home_points": 27, "away_points": 20 }
    ],
    "total": 1
  }
}

Response fields — NFL

FieldTypeDescription
windowsobjectrecent_form (5), rates_l5 (5), rates_season ("season"), head_to_head (10).
teams.{home,away}.recent_formobjectUp to the last window (5) completed results, most-recent-first. results is "W"/"L" (ties omitted); points_scored/points_allowed align by index.
teams.{home,away}.rest_daysinteger or nullDays since this team's last completed game in the same league (NFL or NCAAF). null if this is their first game of the season. Preseason is excluded from the lookback.
teams.{home,away}.recordobjectSeason-to-date record: wins, losses, games, win_pct, points_for, points_against, point_differential.
teams.{home,away}.rates_l5 / rates_seasonobjectTeam box averages (source: "event_stats_avg"): total_yards, yards_allowed, passing_yards (net), rushing_yards, attempts, turnovers, third_down_pct (ratio-of-sums).
teams.{home,away}.player_box.playersobject[]This event's per-player counting stats (final, in-progress, and delayed when rows exist; available: false pregame). Passing/rushing/receiving/defense/kicking fields on one line per athlete. Sorted passing_yards desc. as_of is the newest row timestamp. NCAAF uses the same football shape. Settleables: /player-props.
head_to_headobjectUp to the last window (10) meetings. meetings use home_points/away_points.

NCAAF uses the same football-native field catalog as NFL above (league_slug: "ncaaf"). Aggregates are scoped to the NCAAF league so college rates never mix with NFL. Judgment stays on /intelligence.

Response — NBA

JSON
{
  "event_id":    8579,
  "available":   true,
  "league_slug": "nba",
  "windows": {
    "recent_form": 5,
    "rates_l5": 5,
    "rates_season": "season",
    "head_to_head": 10
  },
  "teams": {
    "home": {
      "team_id": 35,
      "name": "New York Knicks",
      "abbreviation": "NYK",
      "rest_days": 2,
      "recent_form": {
        "window": 5,
        "results": ["L", "W", "W"],
        "points_scored": [111, 105, 105],
        "points_allowed": [115, 104, 95]
      },
      "record": {
        "source": "season_to_date",
        "wins": 13, "losses": 1, "games": 14,
        "win_pct": 0.9286,
        "points_for": 1673, "points_against": 1404,
        "point_differential": 269
      },
      "rates_l5": {
        "source": "event_stats_avg", "window": "l5",
        "window_size": 5, "games": 3,
        "rebounds": 46.33, "assists": 22.33,
        "steals": 7.67, "blocks": 5.33,
        "turnovers": 12.33, "fouls": 23.0,
        "fg_pct": 42.67, "three_point_pct": 35.0, "free_throw_pct": 82.33
      },
      "rates_season": { /* same keys as rates_l5; window "season" */ }
    },
    "away": { /* same shape as "home" */ }
  },
  "head_to_head": {
    "window": 10,
    "meetings": [
      { "home_points": 111, "away_points": 115 }
    ],
    "total": 1
  }
}

Response fields — NBA

FieldTypeDescription
windowsobjectrecent_form (5), rates_l5 (5), rates_season ("season"), head_to_head (10).
teams.{home,away}.recent_formobjectUp to the last window (5) completed results, most-recent-first. results is "W"/"L" (NBA games don't tie); points_scored/points_allowed align by index.
teams.{home,away}.rest_daysinteger or nullDays since this team's last completed game in the same league (NBA or NCAAB). null if this is their first game of the season. Preseason is excluded from the lookback.
teams.{home,away}.recordobjectSeason-to-date record: wins, losses, games, win_pct, points_for, points_against, point_differential.
teams.{home,away}.rates_l5 / rates_seasonobjectTeam box averages (source: "event_stats_avg"): rebounds, assists, steals, blocks, turnovers, fouls; fg_pct / three_point_pct / free_throw_pct as ratio-of-sums.
teams.{home,away}.player_box.playersobject[]This event's per-player counting stats (final, in-progress, and delayed when rows exist): points, rebounds, assists, turnovers, steals, blocks, fouls, three_pointers_made, three_pointers_attempted. Sorted points desc. as_of is the newest row timestamp. NCAAB uses the same basketball shape. Settleables: /player-props.
head_to_headobjectUp to the last window (10) meetings. meetings use home_points/away_points.

NCAAB uses the same basketball-native field catalog as NBA above (league_slug: "ncaab"). Aggregates are scoped to the NCAAB league so college rates never mix with NBA.

Response — NHL

JSON
{
  "event_id":    8756,
  "available":   true,
  "league_slug": "nhl",
  "windows": {
    "recent_form": 5,
    "rates_l5": 5,
    "rates_season": "season",
    "head_to_head": 10
  },
  "teams": {
    "home": {
      "team_id": 124,
      "name": "Vegas Golden Knights",
      "abbreviation": "VGK",
      "rest_days": 3,
      "recent_form": {
        "window": 5,
        "results": ["L", "L", "W"],
        "goals_scored": [2, 3, 5],
        "goals_allowed": [4, 5, 4]
      },
      "record": {
        "source": "season_to_date",
        "wins": 13, "losses": 6, "games": 19,
        "win_pct": 0.6842,
        "goals_for": 70, "goals_against": 54,
        "goal_differential": 16
      },
      "rates_l5": {
        "source": "event_stats_avg", "window": "l5",
        "window_size": 5, "games": 5,
        "shots_on_goal": 26.0, "hits": 39.2,
        "penalty_minutes": 6.0, "power_play_goals": 0.4,
        "giveaways": 17.8, "takeaways": 4.6,
        "blocked_shots": 20.2
      },
      "rates_season": { /* same keys as rates_l5; window "season" */ }
    },
    "away": { /* same shape as "home" */ }
  },
  "head_to_head": {
    "window": 10,
    "meetings": [
      { "home_goals": 2, "away_goals": 4 }
    ],
    "total": 5
  }
}

Response fields — NHL

FieldTypeDescription
windowsobjectrecent_form (5), rates_l5 (5), rates_season ("season"), head_to_head (10).
teams.{home,away}.recent_formobjectUp to the last window (5) completed results, most-recent-first. results is "W"/"L" (NHL games always resolve via OT/shootout — no ties); goals_scored/goals_allowed align by index.
teams.{home,away}.rest_daysinteger or nullDays since this team's last completed game. null if this is their first game of the season.
teams.{home,away}.recordobjectSeason-to-date record: wins, losses (includes OT/shootout losses), games, win_pct, goals_for, goals_against, goal_differential.
teams.{home,away}.rates_l5 / rates_seasonobjectTeam box averages from persisted NHL box scores (source: "event_stats_avg"): shots_on_goal, hits, penalty_minutes, power_play_goals, giveaways, takeaways, blocked_shots.
teams.{home,away}.player_box.skaters / goaliesobject[]This event's per-player counting stats (final, in-progress, and delayed when rows exist). Skaters: goals, assists (slug assists_nhl), plus_minus, penalty_minutes, hits (slug hits_nhl), power_play_goals, shots_on_goal, blocked_shots, giveaways, takeaways. Goalies: saves, goals_against, shots_against, penalty_minutes. as_of is the newest row timestamp. Settleables: /player-props.
head_to_headobjectUp to the last window (10) meetings. meetings use home_goals/away_goals.

Response — tennis (ATP/WTA main-draw singles)

JSON
{
  "event_id":    17438,
  "available":   true,
  "league_slug": "wta",
  "draw_type":   "singles",
  "windows": {
    "recent_form": 10,
    "surface_form": 10,
    "head_to_head": 10,
    "record_lookback_days": 365,
    "career_surface_years": 3
  },
  "match": {
    "surface": "hard",
    "competition_name": "Canadian Open",
    "tier": "1000",
    "round": "Round of 16",
    "court": null,
    "status": "final",
    "scoreboard": {
      "sets_won": { "player_1": 2, "player_2": 0 },
      "winner_role": "player_1",
      "sets": [
        { "period_num": 1, "player_1_games": 6, "player_2_games": 4, "tiebreak_score": null }
      ]
    }
  },
  "players": {
    "player_1": {
      "player_id": 1,
      "name": "Alexandra Eala",
      "ranking": 64,
      "rest_days": 2,
      "recent_form": { "window": 10, "results": ["W", "L"], "sets_for": [2, 0], "sets_against": [0, 2] },
      "surface_form": { "surface": "hard", "window": 10, "results": ["W"], "sets_for": [2], "sets_against": [0] },
      "record": { "source": "tour_lookback", "lookback_days": 365, "wins": 12, "losses": 8, "games": 20 },
      "career_rates": {
        "source": "tennis_api", "window": "career",
        "hold_pct": 0.78, "break_pts_converted": 0.42, "break_pts_saved": 0.61,
        "first_serve_pct": 0.63, "first_serve_won": 0.71, "second_serve_won": 0.52,
        "aces": null, "double_faults": null
      },
      "career_surface": {
        "source": "tennis_api", "window_years": 3, "surface": "hard",
        "wins": 40, "losses": 18, "win_pct": 0.6897
      }
    },
    "player_2": { /* same shape as player_1 */ }
  },
  "head_to_head": {
    "window": 10,
    "meetings": [
      { "event_id": 9001, "surface": "hard", "winner_role": "player_1", "sets_won": { "player_1": 2, "player_2": 1 } }
    ],
    "total": 1
  }
}

Response fields — tennis

FieldTypeDescription
draw_typestringAlways singles when available is true. Doubles, qualifying, or unresolved players return available: false (free).
matchobjectSurface, competition, tier, round, court, status, result_type, and scoreboard (sets won + per-set games from lumify_period_scores). round uses ingested labels (Round of 16, Quarterfinal, … — not abbreviated codes like R16). court and sets[].tiebreak_score are not currently populated by ingest.
players.{player_1,player_2}objectIdentity, current ranking/points (not as-of match day), rest days, recent_form / surface_form (W/L + sets), record (tour_lookback wins/losses over 365 days), plus Stage-2 career_rates (serve/return percentages; null until ingest) and career_surface (match-surface W/L over windows.career_surface_years). Form/record/rest count final/complete only — walkovers excluded; retirements included.
head_to_headobjectPrior singles meetings (final/complete only — walkovers excluded). winner_role / sets_won are relative to this fixture's player_1 / player_2 ids.

Pairs with /intelligence: /stats is the sport-specific Data layer (no scoring). /intelligence is judgment. Soccer, MLB, tennis, NFL, and NCAAF expose predictive bets[] (fair-probability + Price). MLB, NFL, NCAAF, NBA, NCAAB, and NHL also score player-prop forecasts[] — including when available is still false on NBA / NCAAB / NHL. Fetch both when you want Data alongside judgment. How forecasts work: /docs/forecasts.

Get player-prop lines and live progress

GET /v1/events/{id}/player-props Open in Playground

NFL, NCAAF, NBA, NCAAB, NHL, MLB, and Soccer only. Joins persisted player-prop mains to this-event player box counts and grades over/under/push (1:1 slugs, combo sums including rush+rec and pass+rush yards, weighted total bases, anytime TD, double-double/triple-double, hockey points). GET /odds stays on moneyline/spread/total — this is the settleable surface. Always returns 200 when the event exists and the sport is supported — check available. Returns 404 if the event ID does not exist, and 400 if the sport is not NFL, NCAAF, NBA, NCAAB, NHL, MLB, or Soccer. available:false is free.

Coverage inventory (sport × market key, settleable vs returned-not-graded): /docs/player-props. Other sports return 400.

Path parameters

NameTypeDescription
idintegerLumify event ID.

Request

cURL
curl https://lumify.ai/v1/events/12345/player-props \
  -H "Authorization: Bearer $LUMIFY_API_KEY"

Response

JSON
{
  "event_id": 12345,
  "available": true,
  "sport": "nfl",
  "status": "inprogress",
  "period": "3",
  "clock": "8:42",
  "player_props": [
    {
      "player": "Patrick Mahomes",
      "player_id": 42,
      "market": "passing_yards",
      "line": 275.5,
      "current": 187.0,
      "pct_of_line": 67.9,
      "settleable": true,
      "result": "in_progress",
      "books": {
        "pinnacle": { "over": -115, "under": -105 },
        "fanduel": { "over": -110, "under": -110 }
      }
    }
  ]
}

Response fields

FieldTypeDescription
availablebooleanFalse when no player-prop mains have been ingested. Not charged.
sportstringEvent sport slug (nfl, ncaaf, nba, ncaab, nhl, mlb).
statusstringEvent status. Same enum as #status-values.
period / clockstring or nullLive clock. Same sport-specific formats as #event-score. Both null before tip / kickoff / first pitch.
player_props[].playerstringDisplay name from the priced line (generational suffixes stripped). Bind to a roster via player_id.
player_props[].player_idinteger or nullLumify player ID. Join key to GET /v1/players/{id}. Null when the priced name did not bind to this event's roster.
player_props[].marketstringProp market category (used as lumify_odds.market_key). Inventory: /docs/player-props.
player_props[].linenumberPosted main Over/Under for this player × market. Half-points are typical. A book that posted a different number is a separate row, not an alternate on this one.
player_props[].currentnumber or nullThis-event counting stat from the live/final box — a direct 1:1 slug, a combo sum, or a yes/no threshold (1.0/0.0). Null before the box lands, when the player is unmatched, or when settleable is false.
player_props[].pct_of_linenumber or nullcurrent / line × 100, one decimal. Can exceed 100. Null when current or line is null, when line is 0, or on yes/no markets (double-double / triple-double / anytime TD) — those grade from a 1.0/0.0 threshold, not a percent of line.
player_props[].settleablebooleanTrue when this market has a settlement rule. When false, current and result are null — the line is returned, not graded. Catalog: /docs/player-props#fields.
player_props[].resultstring or nullin_progress while the event is not final. over / under / push once final and current is present. At final with no current: dnp (box ingest landed and this player has zero rows) or no_stat (played but this slug never landed, unmatched name, or box not landed yet). Null when settleable is false.
player_props[].booksobjectPer-book Over/Under American-odds integers at this exact line. A missing side is null. A book that posted a different number appears on a separate row.

Get team-total lines and live progress

GET /v1/events/{id}/team-props Open in Playground

NFL, NCAAF, MLB, and soccer. Joins persisted Pinnacle team-total mains (each team's full-game points/runs/goals Over/Under) to this-event participant scores and grades over/under/push. GET /odds stays on moneyline/spread/total — this is the settleable surface. Always returns 200 when the event exists and the sport is supported — check available. Returns 404 if the event ID does not exist, and 400 if the sport is not supported. available:false is free.

Path parameters

NameTypeDescription
idintegerLumify event ID.

Request

cURL
curl https://lumify.ai/v1/events/12345/team-props \
  -H "Authorization: Bearer $LUMIFY_API_KEY"

Response

JSON
{
  "event_id": 12345,
  "available": true,
  "sport": "nfl",
  "status": "inprogress",
  "period": "3",
  "clock": "8:42",
  "team_props": [
    {
      "team": "Buffalo Bills",
      "team_id": 12,
      "side": "away",
      "market": "team_total",
      "line": 21.5,
      "current": 14.0,
      "pct_of_line": 65.1,
      "settleable": true,
      "result": "in_progress",
      "books": {
        "pinnacle": { "over": -105, "under": -115 }
      }
    }
  ]
}

Response fields

FieldTypeDescription
availablebooleanFalse when no team-total mains have been ingested. Not charged.
sport / status / period / clockstring or nullSame live-progress envelope as player-props. Clock formats: #event-score.
team_props[].teamstringDisplay name of the side this total is priced on.
team_props[].team_idinteger or nullLumify team ID. Join key to GET /v1/teams/{id}.
team_props[].sidestringhome or away.
team_props[].marketstringAlways team_total in v1 — each team's full-game points, runs, or goals Over/Under.
team_props[].linenumberPosted main Over/Under for this team's full-game points. Half-points are typical.
team_props[].currentnumber or nullThis-event team score from the live/final box. Null before kickoff.
team_props[].pct_of_linenumber or nullcurrent / line × 100, one decimal. Can exceed 100. Null when current or line is null.
team_props[].settleablebooleanTrue when this market grades from the team score. When false, result is null.
team_props[].resultstring or nullin_progress while the event is not final. over / under / push once final and current is present.
team_props[].booksobjectPer-book Over/Under American-odds integers at this exact line.

Get first-half / first-five / first-set lines and live progress

GET /v1/events/{id}/period-odds Open in Playground

NFL, NCAAF, NBA, NCAAB, MLB, soccer, and tennis. Joins persisted first_half_spreads / first_half_totals mains to this-event period scores and grades the period, not the full game (1H = Q1+Q2, or NCAAB/soccer native 1H; MLB F5 = innings 1–5; tennis S1 = first-set games). GET /odds stays on moneyline/spread/total. Always returns 200 when the event exists and the sport is supported — check available. Returns 404 if the event ID does not exist, and 400 if the sport is not supported. available:false is free.

Path parameters

NameTypeDescription
idintegerLumify event ID.

Request

cURL
curl https://lumify.ai/v1/events/12345/period-odds \
  -H "Authorization: Bearer $LUMIFY_API_KEY"

Response

JSON
{
  "event_id": 12345,
  "available": true,
  "sport": "nfl",
  "status": "inprogress",
  "period": "3",
  "clock": "8:42",
  "period_odds": [
    {
      "scope": "1H",
      "market": "first_half_totals",
      "line": 24.5,
      "home_score": 17.0,
      "away_score": 10.0,
      "current": 27.0,
      "pct_of_line": 110.2,
      "settleable": true,
      "scope_complete": true,
      "result": "over",
      "books": {
        "fanduel": { "over": -110, "under": -110 }
      }
    }
  ]
}

Response fields

FieldTypeDescription
availablebooleanFalse when no first-half / first-five / first-set mains have been ingested. Not charged.
sport / status / period / clockstring or nullSame live-progress envelope as player-props. Clock formats: #event-score.
period_odds[].scopestring1H (NFL/NCAAF/NBA/NCAAB — Q1+Q2, or NCAAB/soccer native 1H), F5 (MLB innings 1–5), or S1 (tennis first-set games).
period_odds[].marketstringfirst_half_spreads or first_half_totals.
period_odds[].linenumberPosted main for this scope. Spread point or total number. Half-points are typical.
period_odds[].home_score / away_scorenumber or nullPoints (or runs) scored in this scope so far. Null until the first period row lands.
period_odds[].currentnumber or nullTotals: home_score + away_score. Spreads: the home-minus-away differential used to grade the home side. Null until both scores are present.
period_odds[].pct_of_linenumber or nullTotals only: current / line × 100, one decimal. Can exceed 100. Null on spreads, or when current / line is null.
period_odds[].settleablebooleanTrue when this market has a settlement rule. Grade only when scope_complete is also true.
period_odds[].scope_completebooleanTrue when every required period label is numeric for both sides and the following period has started (e.g. Q3 for 1H, inning 6 for F5) — or the event is final/walkover. Grade only then.
period_odds[].resultstring or nullTotals only: in_progress / over / under / push. Null on spreads — those use the two result fields below.
period_odds[].home_result / away_resultstring or nullSpreads only: won / lost / push once scope_complete. Null on totals and while the scope is still open.
period_odds[].booksobjectPer-book American-odds integers. Spreads use home/away; totals use over/under.

Get bet intelligence for an event

GET /v1/events/{id}/intelligence Open in Playground

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.

  • Predictive bets[] — vig-stripped probability / fair_price plus Price overlay. How to read: /docs/understanding-odds.
  • Main-line ev (Beta) — the same sharp-fair gap packaged as ev_pct + Kelly on moneyline, spreads, and totals (tennis totals stay unpublished). Scan a sport: GET /intelligence/ev.
  • forecasts[] — probability-based prediction, not a beat-the-market claim. Player-prop shrunken-rate Over/Under on MLB/NFL/NCAAF/NBA/NCAAB/NHL; high-confidence main-line picks on tennis, NFL, NCAAF, MLB, and soccer — moneyline, spread, and total (one side per market when the uncertainty band does not include a coin flip). Same objects as GET /intelligence/forecasts. A selected main-line row's research.note is copied to the matching bets[].narrative. How + fields: /docs/forecasts.

available gates bets[] only. forecasts[] can still populate when available is false (NBA / NCAAB / NHL props, a high-confidence main-line pick, or an MLB/NFL/NCAAF game that has not been priced yet). Do not drop the payload because available is false.

SportSurfaceNotes
MLBPredictive + forecastsbets[] fields · forecasts[]
TennisPredictive + forecastsbets[] fields · forecasts[] moneyline
SoccerPredictiveMLS, EPL, La Liga, Serie A, Bundesliga, Ligue 1. Fields
NFLPredictive + forecastsIn season. bets[] · forecasts[]
NCAAFPredictive + forecastsIn season. bets[] · forecasts[]
NBA, NCAAB, NHLForecastsbets[] is empty and available is false; forecasts[] still scores player props. Fields
Other—available: false (free when forecasts is also empty)
  • Two sibling arrays: bets[] is vig-stripped probability / fair_price plus Price overlay and main-line ev (Beta) — a gated beat-the-market claim. forecasts[] is an ungated probability-based prediction — player props plus tennis moneyline/spreads/totals, same objects as GET /intelligence/forecasts.
  • Check available before reading bets[]. forecasts[] can populate when available is false (NBA / NCAAB / NHL, or an MLB/NFL/NCAAF game that has not been priced yet).
  • has_recommend stays false until a model-backed edge is published.

Path parameters

ParameterTypeDescription
idintegerrequired Lumify event ID. Returns 404 if the event doesn't exist. If the event exists but predictive bets[] have not been computed yet, returns 200 with available: false and empty bets. forecasts[] can still populate on MLB / NFL / NCAAF / NBA / NCAAB / NHL.

Request

curl
curl https://lumify.ai/v1/events/4821/intelligence \
  -H "Authorization: Bearer YOUR_API_KEY"
Python
resp = requests.get(
    "https://lumify.ai/v1/events/4821/intelligence",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
)
intel = resp.json()
JavaScript
const intel = await fetch("https://lumify.ai/v1/events/4821/intelligence", {
  headers: { "Authorization": "Bearer YOUR_API_KEY" },
}).then(r => r.json());

Top-level fields

FieldTypeDescription
availablebooleanfalse when predictive bets[] have not been computed yet. forecasts[] can still populate. Free only when available is false and forecasts is empty.
sportstring | nullSport slug for this event.
leaguestring | nullLeague slug for this event, if any — e.g. mls, epl.
odds_sourcestring | nullBookmaker the bets[].market prices came from — the book the assessment was priced against, not a live overlay. Per-bet market.book is authoritative if the two ever differ.
playersobjectParticipant 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_recommendboolean | nulltrue if at least one bet meets the recommendation threshold; false if none do; null if the pipeline hasn't run yet. Stays false until a model-backed edge/tier is published.
intelligence_updated_atstring | nullISO-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.
match_overviewstring | nullEvent-level pre-game matchup preview. Written before kickoff (sport-shaped Search overlay). The request path only reads the stored row. Null until the pre-kickoff job has run.
rationalestring[] | nullEvent-level factual pre-game matchup chips (context overlay). Not a pick.
betsarrayOne 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.
forecastsarrayForecasted wagers for this event — player-prop rows on MLB/NFL/NCAAF/NBA/NCAAB/NHL; high-confidence main-line picks on tennis, NFL, NCAAF, MLB, and soccer (moneyline, spread, total). Same object as GET /intelligence/forecasts. selected marks the pick — high-confidence main-lines and the top-conviction prop rows on the daily slate. How + fields: /docs/forecasts.
matchupobject | nullNot returned. Use GET /v1/events/{id}/stats for pitcher/lineup Data.

Predictive intelligence fields

The same keys for MLB, soccer (MLS + big-five), tennis, NFL, and NCAAF. Fair probability + Price overlay — framework plumbing is omitted. edge/tier are null and has_recommend is false until Edge clears. best.edge / edges_by_book are line-shopping price gaps vs sharp consensus; main-line ev (Beta) is the same gap packaged as ev_pct + Kelly. Example below is MLS; other live sports use the same keys with sport-specific tokens and fair books.

Response

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,
  "match_overview":  null,
  "rationale":       null,
  "bets": [
    {
      "bet_type":      "ML_HOME",
      "player_role":   "home",
      "team_id":       858,
      "player_name":   "D.C. United",
      "probability":   0.31903,
      "interval":     [0.2631, 0.37496],
      "fair_price":   213,
      "market": { "price": 199, "line": null, "book": "pinnacle" },
      "edge":         null,
      "tier":         null,
      "fair": {
        "probability": 0.31903,
        "books": ["pinnacle"],
        "n_books": 1,
        "is_consensus": false
      },
      "edges_by_book": { "fanduel": 0.031, "hardrock": -0.012 },
      "best": { "book": "fanduel", "price": 220, "edge": 0.031, "quote_age_seconds": 180.0 },
      "ev": { "beta": true, "book": "fanduel", "price": 220, "ev_pct": 3.1, "kelly_fraction": 0.0141, "quote_age_seconds": 180.0, "n_books": 1 },
      "narrative":    null,
      "computed_at":  "2026-07-27T10:07:20Z"
    }
  ]
}
ReturnedOmitted
probability, interval, fair_price, market, edge, tier, fair, edges_by_book, best, ev (Beta), narrative, computed_at, participant fields p_model, p_market, blend_w, sufficiency, phase, model_version, drivers, alignment

Field reference

Same keys across every live intelligence sport. Framework plumbing listed as omitted above is never present in the JSON.

FieldTypeDescription
bet_typestringToken for this outcome. Enumerated by sport in #intelligence-tokens.
player_role / player_name / player_id / team_idstring or integer or nullParticipant this token is priced on. null on match-level tokens (soccer ML_DRAW, every sport's Over/Under).
probabilitynumber or nullPublished probability for this outcome, 0–1. Outcomes of a market are solved together and sum to 1.
fair_priceinteger | nullAmerican-odds fair price implied by probability — the vig-free line. Comparing it to market.price gives the book's margin on this side.
edgenumber | nullExpected profit per 1 unit staked at market.price. null on Stage 1 until a model-backed edge is published — reserved for recommend gating.
intervalnumber[] | null[lo, hi] band around probability. Read it as how much evidence backs this number, not as a statistical confidence interval. A freshly-opened line gets a wider band than a heavily-traded one.
tierstring | nullvery_high, strong, moderate, or avoid. null whenever edge is null.
fairobject | nullSharp-consensus fair: {probability, books, n_books, is_consensus}. MLS/tennis: Pinnacle. MLB: Pinnacle+Circa when both quote.
edges_by_bookobject | nullPrice gap vs fair.probability per soft book — a line-shopping metric. See ev for Beta display packaging on main-line tokens.
bestobject | nullHighest price-gap book: {book, price, edge, quote_age_seconds}. Same line-shopping metric as edges_by_book. See ev for Beta display packaging on main-line tokens.
evobject | nullBeta. {beta, book, price, ev_pct, kelly_fraction, quote_age_seconds, n_books} — the same sharp-fair price gap as best, re-expressed as an EV% and a full-Kelly stake (capped at 1). Main-line tokens (moneyline, spreads, totals). Tennis totals stay unpublished. A single sharp book (n=1 Pinnacle) is a valid fair. Gaps ≤ 0 or above 25% are dropped. When best is a suppressed MLB moneyline book, the next eligible book is used. Not a backtested +EV claim. Scan a sport with GET /v1/intelligence/ev.
market.bookstring or nullBookmaker this bet's price/line came from. The ?bookmaker= parameter is ignored.
market.priceinteger or nullAmerican-odds integer quoted by market.book. Walkthrough: /docs/understanding-odds#fair-price.
market.linenumber or nullSpread or total number this token is priced on (e.g. 8.5 on OVER/UNDER). null on moneyline.
best.quote_age_secondsnumber or nullAge of best.book's posted price at publish, in seconds. Explained on /docs/understanding-odds.
narrativestring | nullPick-defense rationale for a high-confidence main-line side — why this token hits, not a match preview. Present only when a matching forecasts[] row is selected: true and carries research.note. Null on the opposite side, on coin-flip markets, and when no pick was made.
computed_atstring | nullISO-8601 UTC time this bet's numbers last materially changed — not when they were last checked.

Reading a market-anchored event. probability and fair_price are a vig-stripped fair-price reference. Read fair / edges_by_book / best for cross-book line-shopping (Price overlay; Edge is unpublished). Main-line ev (Beta) packages a positive gap as ev_pct + Kelly. edge / tier / has_recommend stay null/false until Edge clears — code against them now and they will fill in without a response-shape change. For a worked numeric walkthrough of every field on this page, see Understanding Odds & Probability.

Bet tokens by sport

SportsTokens
MLB, tennis, NFL, NCAAFML_P1 (home / player_1), ML_P2 (away / player_2), SPREAD_P1, SPREAD_P2, OVER, UNDER
SoccerML_HOME, ML_AWAY, ML_DRAW, SPREAD_HOME, SPREAD_AWAY, OVER, UNDER. ML_DRAW is match-level — player_role / team_id are null, same as Over/Under.

List, then fetch. GET /v1/events with sport / league / status=scheduled, then GET /v1/events/{id}/intelligence.

  • has_recommend stays false until edge is published. Use probability / fair_price and fair / edges_by_book / best for line-shopping. ev (Beta) badges the same gap on main-line tokens. Scan a sport with GET /v1/intelligence/ev (market=h2h|spreads|totals; tennis totals return 400).

List main-line EV (Beta)

GET /v1/intelligence/ev Open in Playground

Beta. Pregame main-line opportunities (moneyline, spreads, or totals) whose sharp-fair price gap is positive and at most 25% EV, for soccer, MLB, tennis, NFL, and NCAAF. Pass market=h2h (default), spreads, or totals. Tennis totals return 400 — Stage 1 intelligence is moneyline + spreads only. Same number as bets[].ev on GET /intelligence. Always 200 when the market is offered; an empty list means nothing currently clears the gates. 1 credit.

ParameterTypeDescription
sportstringrequiredsoccer, mlb, tennis, nfl, or ncaaf.
leaguestringOptional league slug. Soccer without a league scans every published soccer league (MLS + big-five).
marketstringh2h (default, moneyline), spreads, or totals. Tennis + totals returns 400.
min_evnumberMinimum EV%. Default 0. Clamped to 0–25.
bookstringRestrict to one sportsbook slug.
limitintegerMax rows (1–200). Default 50.
json
{
  "sport": "soccer",
  "league": "mls",
  "market": "h2h",
  "min_ev": 0,
  "max_ev": 25,
  "book": null,
  "beta": true,
  "opportunities": [{
    "event_id": 84213,
    "side": "home",
    "team": "LA Galaxy",
    "bet_type": "ML_HOME",
    "fair_probability": 0.4123,
    "ev": { "beta": true, "book": "fanduel", "price": 155, "ev_pct": 3.1, "kelly_fraction": 0.02, "n_books": 1 }
  }],
  "total": 1
}

Response fields

FieldTypeDescription
sportstringSport slug this scan was run for.
leaguestring or nullLeague filter, or null when the scan covered the whole sport.
marketstringh2h, spreads, or totals — the family scanned.
min_ev / max_evnumberEV% gates applied to this response. Default 0–25. Gaps ≤ 0 or above 25% are dropped.
bookstring or nullBook filter, or null when every eligible book was considered.
betabooleanAlways true — this surface is an unverified market-consensus estimate.
opportunities[].event_idintegerLumify event ID. Fetch the full payload with GET /intelligence.
opportunities[].sidestringhome, away, p1, p2, draw, over, or under.
opportunities[].teamstring or nullDisplay name of the side; Draw / Over / Under for those tokens.
opportunities[].bet_typestringMain-line token for the scanned family (e.g. ML_HOME, SPREAD_P1, OVER).
opportunities[].linenumber or nullPosted line for spreads and totals. Null on moneyline.
opportunities[].fair_probabilitynumberSharp-fair probability (0–1) the gap is measured against. A single sharp book (n=1 Pinnacle) is a valid fair.
opportunities[].ev.ev_pctnumberPrice gap vs sharp-fair, in percent points (0–25 after the gate). Same number as bets[].ev.ev_pct.
opportunities[].ev.kelly_fractionnumberFull-Kelly stake from that gap, capped at 1.0. Not a recommended bet size.
opportunities[].ev.n_booksintegerSharp books used to build the fair. n=1 Pinnacle is valid.
opportunities[].ev.quote_age_secondsnumber or nullAge of the quoted book's posted price at publish, in seconds. Present on bets[].ev; may be omitted on this list.
totalintegerCount of opportunities on this page. Empty list is still 200, still 1 credit.

List forecasted wagers

GET /v1/intelligence/forecasts Open in Playground

Daily board of forecasted wagers from Lumify's model — a probability-based prediction, not a beat-the-market claim (no OOS/independence gate; see GET /intelligence/ev for the gated main-line +EV claim). Player-prop wagers from the shrunken rate model — MLB, NCAAF, NFL, NBA, NCAAB, and NHL. Tennis main-line: moneyline from ranking Bradley-Terry (bet_type ML_P1/ML_P2), game handicap (SPREAD_P1/SPREAD_P2), and total games (OVER/UNDER) from a Normal-approx games model. Ranked by conviction. Always 200; an empty list means nothing is on today's slate. 1 credit. The same objects also appear per-event on GET /intelligence as forecasts[]. How the model works, field catalog, and the forecastable-vs-settleable subset: /docs/forecasts.

Query parameters

ParameterTypeDescription
sportstringrequiredPlayer props: mlb, ncaaf, nfl, nba, ncaab, nhl. Main-line: tennis (moneyline, spreads, and totals).
datestringUTC slate date YYYY-MM-DD. Defaults to today UTC.
marketstringh2h, spreads, or totals — filters a sport's main-line forecast rows. Ignored on prop-only sports.
limitintegerMax wagers (1–100). Default 25.

Response

json
{
  "sport": "mlb",
  "date": "2026-08-27",
  "model_version": "forecast-v0",
  "reliability": "emerging",
  "wagers": [{
    "event_id": 18258,
    "sport": "mlb",
    "league": "mlb",
    "player": "Ben Rortvedt",
    "player_id": 5128,
    "bet_type": null,
    "player_role": null,
    "market": "runs",
    "line": 0.5,
    "side": "under",
    "p_hit": 0.7617,
    "interval": [0.6982, 0.8252],
    "sufficiency": 0.4545,
    "reliability": "emerging",
    "conviction": 0.2379,
    "model_version": "forecast-v0",
    "selected": true,
    "books": { "draftkings": -453 },
    "drivers": [{
      "id": "player.l10_rate",
      "input": 0.27,
      "effect": 0.082,
      "direction": "up"
    }],
    "research": null
  }],
  "total": 1
}

How + fields live off this page. Model (shrink L10 → Poisson → side), drop rules, conviction formula, selected vs the full event catalog, and every wager field: /docs/forecasts. Worked numeric read of one wager: /docs/understanding-odds#forecasts. Settleable inventory (including combos the model does not pick): /docs/player-props.

Get betting splits for an event

GET /v1/events/{id}/splits Open in Playground

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 ~10 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, NFL, NCAAF, and NCAAB (during their respective seasons; NCAAB is often empty off-season). Tennis and soccer splits are not available (upstream 400). 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

NameTypeDescription
idintegerLumify event ID.

Request

cURL
curl https://lumify.ai/v1/events/479/splits \
  -H "Authorization: Bearer $LUMIFY_API_KEY"

Response

JSON
{
  "event_id":    479,
  "available":   true,
  "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 }
    }
  },
  "bookmakers": [
    {
      "bookmaker": "draftkings",
      "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

FieldTypeDescription
availablebooleanfalse when no splits have been ingested yet. consensus will be an empty object and bookmakers an empty array.
captured_atstring | nullUTC timestamp of the most recent ingest cycle for this event's splits data. null when available is false.
consensusobjectMarket averages across all available bookmakers. Contains moneyline, spread, and total objects.
consensus[market][side].bets_pctintegerPercentage of total bets placed on this side (0–100). Opposite sides sum to ~100.
consensus[market][side].handle_pctintegerPercentage 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].priceinteger | nullAmerican odds for this side. Present on moneyline; null on spread/total sides.
consensus[market][side].linenumber | nullSpread or total line. Present on spread/total; null on moneyline.
bookmakersarrayPer-bookmaker breakdown. Same market nesting as consensus. bookmaker is the odds slug (e.g. draftkings) — same namespace as /odds; 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.

Get injury / availability for an event (Beta)

GET /v1/events/{id}/injuries Open in Playground

Beta. Returns structured late-breaking player injury and availability status for an upcoming event. Written by an automated monitoring pipeline that watches for late-breaking status changes. Always returns 200 when the event exists — check available to determine whether a first report has been written. Returns 404 only if the event ID does not exist. available:false is not charged. How-to: /docs/injuries.

Beta coverage: NFL and MLB today, with more sports rolling out. NFL monitoring starts ~4.5 days before kickoff (two sparse checks on the Wednesday/Thursday practice-report window), then the regular hours-to-start cadence; MLB stays on a 48-hour window. Checks also run when a main-market line (moneyline / spread / total) moves ≥20 American-odds points or ≥1.0 point, and at a guaranteed pre-kickoff confirmation (NFL ~T-90min, MLB ~T-2.5h). Subscribe to webhook event_types: ["injury"] for material/critical status changes (info-level reconfirms never fire; 0.5 credits per delivery). No in-game feed.

Path parameters

NameTypeDescription
idintegerLumify event ID.

Query parameters

ParameterTypeDefaultDescription
exclude_statusstring optional— Comma-separated, case-insensitive statuses to drop from injuries[] (e.g. exclude_status=ir). Useful for hiding injured-reserve/long-term-IL rows, which are long-standing roster designations rather than late-breaking news — and, since they come from roster/depth-chart sources reporting a designation plus return-eligibility date rather than an injury description, are also the rows most likely to have body_part: null. Unknown values return 400. Does not affect available or X-Credits-Used — filtering every row out still reports available: true if the monitor has data.

Request

cURL
curl https://lumify.ai/v1/events/19190/injuries \
  -H "Authorization: Bearer $LUMIFY_API_KEY"

# Hide injured-reserve rows
curl "https://lumify.ai/v1/events/19190/injuries?exclude_status=ir" \
  -H "Authorization: Bearer $LUMIFY_API_KEY"

Response

JSON
{
  "event_id":         19190,
  "available":        true,
  "last_checked_at":  "2026-09-03T21:07:00Z",
  "confirmed_at":     null,
  "next_check_at":    "2026-09-03T21:22:00Z",
  "injuries": [
    {
      "player":           "Patrick Mahomes",
      "player_id":        44012,
      "team":             "Kansas City Chiefs",
      "team_id":          18,
      "status":           "questionable",
      "previous_status":  null,
      "body_part":        "ankle",
      "note":             "Listed as questionable on the Friday injury report.",
      "source_url":       "https://www.nfl.com/injuries",
      "severity":         "info",
      "last_changed_at":  "2026-09-03T21:07:00Z"
    }
  ]
}

Response fields

FieldTypeDescription
availablebooleanfalse until the monitor has written at least one structured report. Not charged when false.
last_checked_atstring | nullMost recent monitor search for this event, any outcome.
confirmed_atstring | nullGuaranteed pre-kickoff confirmation timestamp. Null until that checkpoint has run.
next_check_atstring | nullNext scheduled cadence check; null if watching has stopped.
injuriesarrayPer-player rows. Empty when available is false.
injuries[].playerstringExtracted player name (always present, even if unmatched).
injuries[].player_idinteger | nullLumify player ID when the name bound to a roster or participant.
injuries[].team / team_idstring | integer | nullResolved team; null for unmatched names or tennis singles.
injuries[].statusstringout, doubtful, questionable, probable, day_to_day, ir, suspended, or available.
injuries[].previous_statusstring | nullPrior status on this event; null on first sighting.
injuries[].severitystringinfo (unchanged reconfirm, or a first sighting of a routine questionable/probable/day_to_day/available tag with nothing to compare against), material (a real status change, including a first-seen out/doubtful/ir/suspended), critical (out/doubtful first seen, or worsened from available/probable, with ≤6h to start).
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 →

List teams

GET /v1/teams Open in Playground

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

ParameterTypeDefaultDescription
sportstringoptional—Sport slug, e.g. nba, nhl, nfl.
leaguestringoptional—League slug, e.g. nba.
conferencestringoptional—Conference filter, e.g. Eastern.
divisionstringoptional—Division filter, e.g. Atlantic.
countrystringoptional—ISO country code, e.g. USA.
qstringoptional—Partial team-name search.
activebooleanoptional—Filter by active status. Omit to return both active and inactive teams.
after_idintegeroptional—Pagination cursor from next_after_id.
limitintegeroptional25Page size, range 1–100.

Request

curl
# 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",
      "image_url":     "https://lumify.ai/media/teams/nba/1.png",
      "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} Open in Playground

Returns a single team's profile, including home venue when linked. Returns 404 if the team does not exist.

Path parameters

NameTypeDescription
idintegerLumify team ID.

Request

curl
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",
  "image_url":     "https://lumify.ai/media/teams/nba/1.png",
  "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
}

Resolve a team

POST /v1/resolve/team

Map a messy team string plus optional sport, league, event, or player clues to a Lumify team_id. Returns ambiguous or not_found instead of guessing. Accepts nicknames, abbreviations, slugs, city/metro tokens, and declared aliases such as Niners. City-only metros sit below the 0.85 auto-resolve floor and surface as candidates. A sport-scoped key still binds the in-scope side of a collision and returns not_found when every hit is out of scope — it does not name the other sport. GET /v1/teams?q= is a simple name list, not this resolver.

Request body

FieldTypeDescription
querystringTeam string, e.g. Niners, CIN, NY Giants. Required unless a clue id is set.
cluesobjectOptional sport, league, event_id, player_id, team_id.
options.min_confidencenumberAuto-resolve floor (default 0.85). Unique hits below the floor return ambiguous.

Request

curl
curl -X POST https://lumify.ai/v1/resolve/team \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"Niners"}'

Response

JSON
{
  "status": "resolved",
  "team_id": 31,
  "display_name": "San Francisco 49ers",
  "sport": "nfl",
  "abbreviation": "SF",
  "confidence": 0.92,
  "match_reasons": ["alias"],
        "clues_used": []
}

Resolve an event

POST /v1/resolve/event

Map an event_id, or a player_id/team_id clue plus an optional date window, to a Lumify event_id. Returns ambiguous or not_found instead of guessing. Falls back to a team-sport player's roster schedule; combine team_id + opponent_team_id for a matchup.

Request body

FieldTypeDescription
clues.event_idintegerDirect id (ground truth).
clues.player_idintegerFind this player's event.
clues.team_idintegerFind this team's event.
clues.opponent_team_idintegerNarrows to a game against this team; requires team_id or player_id.
clues.start_date / end_datestringInclusive UTC YYYY-MM-DD window. Required together.

Request

curl
curl -X POST https://lumify.ai/v1/resolve/event \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"clues":{"team_id":31,"start_date":"2026-09-14","end_date":"2026-09-15"}}'

Response

JSON
{
  "status": "resolved",
  "event_id": 8842,
  "sport": "nfl",
  "method": "team_window",
  "confidence": 0.85,
  "clues_used": ["team_id", "date_window"]
}
Players Shared player identity across every sport. Null means unknown, not wrong sport. Tennis standings nest under rankings. MCP: search_players, get_player, get_player_events, resolve_player, grade_slip → Guide: How Lumify grades a slip →

List players

GET /v1/players Open in Playground

Returns a paginated list of players. Same identity object on every sport — null means unknown, not wrong sport. Tennis standings nest under rankings.{singles, points}; rankings is null on every other sport. Supports filtering by sport, country, name search, active status, and ranking. Results are sorted by ascending player ID.

Query parameters

ParameterTypeDefaultDescription
sportstringoptional— Filter by sport slug: tennis, mlb, nfl, etc.
qstringoptional— Partial name search. Case-insensitive match against full_name (e.g. ?q=sinner).
countrystringoptional— ISO 3166-1 alpha-3 country code (e.g. USA, ITA, GBR). Case-insensitive.
activebooleanoptional— Pass true for active players only, false for retired.
rankedbooleanoptional— If true, returns only tennis players with an ATP/WTA singles ranking (rankings.singles).
after_idintegeroptional— Pagination cursor. Pass next_after_id from the previous response.
limitintegeroptional25 Page size. Range: 1–100.

Request

curl
# 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"
Python
# 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"]
JavaScript
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

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,
      "rankings":              { "singles": 1, "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} Open in Playground

Returns a single player's full profile. Returns 404 if the player does not exist.

Path parameters

NameTypeDescription
idintegerLumify player ID.

Request

curl
curl "https://lumify.ai/v1/players/1" \
  -H "Authorization: Bearer YOUR_API_KEY"
Python
player = requests.get(
    "https://lumify.ai/v1/players/1",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
).json()
JavaScript
const player = await fetch("https://lumify.ai/v1/players/1", {
  headers: { "Authorization": "Bearer YOUR_API_KEY" },
}).then(r => r.json());

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,
  "rankings":              { "singles": 1, "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 Open in Playground

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

NameTypeDescription
idintegerLumify player ID.

Query parameters

ParameterTypeDefaultDescription
statusstringoptional— Filter by event status: scheduled, inprogress, final, etc.
fromstringoptionaltoday −30d Start date (UTC, inclusive). Format: YYYY-MM-DD.
tostringoptionaltoday +30d End date (UTC, inclusive). Max range: 90 days.
after_idintegeroptional— Pagination cursor.
limitintegeroptional25 Page size. Range: 1–100.

Request

curl
# 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"
Python
events = requests.get(
    "https://lumify.ai/v1/players/1/events",
    params={"status": "scheduled"},
    headers={"Authorization": "Bearer YOUR_API_KEY"},
).json()["data"]
JavaScript
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

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 → Guide: Track live odds movement →

Get current odds for an event

GET /v1/events/{id}/odds Open in Playground

Returns the current moneyline, spread, and total lines for an event — pregame and in-play. Defaults to Pinnacle. Use bookmaker=all or a comma-separated list to fetch multiple books — still 1 credit. Data is updated every ~10 minutes and cached for 2 minutes. While the event is in progress, books that have not quoted since kickoff are omitted. Final events grade result / close from the pre-kickoff snapshot, not the last in-play line. 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: 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).

Default-book fallback: Pinnacle usually posts preseason/early-week lines within ~24-72h of kickoff, so the first and last games of a slate can go final (or still be too far out) before Pinnacle ever prices them, even though other books already have full odds. When bookmaker is omitted and Pinnacle has no line yet, this endpoint falls back to the best-covered other book instead of returning available: false, adding requested_bookmaker: "pinnacle" and fallback_bookmaker to the response. Passing bookmaker=pinnacle explicitly never falls back.

Path parameters

NameTypeDescription
idintegerLumify event ID.

Query parameters

NameTypeDefaultDescription
bookmaker string pinnacle Bookmaker filter. Accepted values: pinnacle, fanduel, draftkings, betmgm, caesars, bet365, circa, westgate, wynn, south_point, stations, hardrock, betonline, betr, betrivers, lowvig, bovada, all, or a comma-separated combination (e.g. fanduel,betmgm).
1 credit for one bookmaker or all.
include_alts boolean false Include alternate spread/total rungs (is_main=false). Default is mains only.

Request — Pinnacle only (default, 1 credit)

cURL
curl https://lumify.ai/v1/events/4821/odds \
  -H "Authorization: Bearer $LUMIFY_API_KEY"

Request — all bookmakers (1 credit)

cURL
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

FieldTypeDescription
availablebooleanfalse when no odds have been ingested yet. bookmakers will be an empty array.
bookmakersarrayOne entry per bookmaker with odds data.
bookmakerstringBookmaker key, e.g. pinnacle, draftkings, betmgm. Supported odds books: pinnacle, fanduel, draftkings, betmgm, caesars, bet365, circa, westgate, wynn, south_point, stations, hardrock, betonline, betr, betrivers, lowvig, bovada. Availability per event depends on what has been ingested.
marketsarrayMarkets for this bookmaker, ordered: moneyline → spread → totals.
keystringMarket key: h2h, spreads, or totals.
labelstringHuman-readable label: moneyline, spread, or totals.
outcomes[].outcomestringSide name — team/player, Over, Under, or soccer Draw. Soccer h2h is 3-way (home, away, Draw).
outcomes[].priceintegerAmerican-odds integer.
outcomes[].pointnumber or nullSpread or total number. null on moneyline.
outcomes[].is_mainbooleantrue on the book's main line. Alternates ship only when include_alts=true.
outcomes[].resultstringFinal events only: won / lost / push / void, graded from the stored score against the pre-kickoff close — not the last in-play price. Omitted while the event is not final.
outcomes[].closenumber or nullFinal events only, on spread/total: the pre-kickoff closing point. Omitted on moneyline and while the event is live.
outcomes[].fair_price / consensusinteger or object or nullMLB, tennis, and soccer (MLS + big-five) mains only — mirrored from published assessments. Absent on other sports.
requested_bookmaker / fallback_bookmakerstringPresent only when the default-book fallback fired (Pinnacle requested, another book returned). Passing bookmaker=pinnacle explicitly never falls back.
captured_atstringISO-8601 UTC timestamp when this bookmaker's odds were last ingested.
last_updatedstringMost recent captured_at across all bookmakers.

Get odds movement history

GET /v1/events/{id}/odds/history Open in Playground

Returns all recorded line movements for an event — any time a price or point changed between ingest cycles. Defaults to Pinnacle. Use bookmaker=all or a comma-separated list for multiple books — still 1 credit. Ordered newest-first. Useful for detecting sharp line movement. Not cached. Returns 404 if no movement has been recorded.

Path parameters

NameTypeDescription
idintegerLumify event ID.

Query parameters

NameTypeDefaultDescription
bookmaker string pinnacle Bookmaker filter. Accepted values: pinnacle, fanduel, draftkings, betmgm, caesars, bet365, circa, westgate, wynn, south_point, stations, hardrock, betonline, betr, betrivers, lowvig, bovada, all, or a comma-separated combination.
1 credit for one bookmaker or all.
limitinteger50Max 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
}

Response fields

FieldTypeDescription
event_idintegerLumify event ID.
movements[].bookmakerstringBook slug this tick belongs to. Same namespace as /odds.
movements[].marketstringHuman label: moneyline, spread, or totals.
movements[].market_keystringPersisted key: h2h, spreads, or totals. Use this for joins; market is the display label.
movements[].outcomestringSide that moved — team/player name, Over, Under, or soccer Draw.
movements[].price_from / price_tointegerAmerican-odds integers before and after this tick.
movements[].point_from / point_tonumber or nullSpread or total number before and after this tick. Both null on moneyline.
movements[].moved_atstringISO-8601 UTC timestamp of this ingest cycle. Newest first.
totalintegerCount of movements on this page. HTTP 404 if no movement has been recorded for the requested books — not an empty list.
Push & Streaming Server-Sent Events and webhook subscriptions — remove the need to poll for live score changes. Guide: Track live 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 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.

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

ParameterTypeDescription
idintegerrequired Lumify event ID. Returns an event: error message if the event does not exist.

Request

curl
curl -N "https://lumify.ai/v1/events/4812/stream" \
  -H "Authorization: Bearer YOUR_API_KEY"
JavaScript
// 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());
// Hit the 5-min cap? Reconnect to the same URL (SDKs do this for you).
es.addEventListener("reconnect", () => { es.close(); /* open a new EventSource here */ });

Response

text/event-stream
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}

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 below.

Billing. Creating, listing, and deleting subscriptions is 1 credit per call, same as any other request. Deliveries fire at a small fraction of a credit each (varies by event type — high-frequency types like score cost less per delivery than low-frequency, high-signal types), drawn from your balance as they accrue, so a quiet subscription can run for a long time on a small balance.

line_move deliveries. One signed POST per event per ingest cycle (~10 min), not one per book/market/outcome. The body is data.moves (the material main-line changes that cycle). A move is pushed only for main lines on live or imminent games (starting within ~48 hours), and only when the point changed or the American price moved by at least 5¢. Alternate rungs and 1–4¢ juice ticks are not delivered.

injury deliveries (Beta). One signed POST per player-status transition at material or critical severity (a first-seen out/doubtful/ir/suspended, or a change such as questionable → out). Unchanged reconfirms, and a first sighting of a routine questionable/probable/day_to_day/available tag, are info and never fire. Deduped via notified_at so the same transition is not re-sent. Payload data carries player, team, status, previous_status, severity, hours_to_start, and trigger (cadence / odds_move / checkpoint / early_report). Billed at 0.5 credits per delivery. How-to: /docs/injuries.

Create a subscription

POST /v1/webhooks
FieldTypeDescription
urlstringrequiredHTTPS endpoint to receive deliveries. Rejected if it resolves to a private/internal address.
event_typesstring[]optionalOne or more of score, status, line_move, injury. Defaults to ["score", "status"]. Including line_move requires sport or event_id (platform-wide line_move is rejected). injury fires only on material/critical status changes (not info reconfirms).
sportstringoptionalRestrict to one sport slug. Required (or event_id) when event_types includes line_move.
event_idintegeroptionalRestrict to a single event. Required (or sport) when event_types includes line_move.
curl
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 — response
{
  "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 & pathDescription
GET /v1/webhooksList the caller's webhook subscriptions.
DELETE /v1/webhooks/{id}Delete a subscription. Returns 404 if it does not belong to the caller.
curl
# 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
ParamTypeDescription
after_idintegeroptionalCursor: return deliveries with id < after_id (list is newest-first).
limitintegeroptionalPage size, default 25, max 100.
successbooleanoptionalFilter to successful (true, 2xx) or failed (false) deliveries.
given_upbooleanoptionalFilter to deliveries that exhausted retries (true) or still have / had a retry path (false).
event_typestringoptionalFilter by event type: score, status, line_move, or injury.
curl
curl https://lumify.ai/v1/webhooks/42/deliveries -H "Authorization: Bearer YOUR_API_KEY"
json — response
{
  "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 → 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 & pathDescription
POST /api/agent/keysCreate a new API key. The secret value is returned only once.
GET /api/agent/keysList 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
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 — response
{
  "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 & pathDescription
GET /api/agent/creditsCurrent tier, credits used, credit limit, bonus credits, and billing period.
GET /api/agent/credit-packsList purchasable one-time credit packs.
POST /api/agent/credits/topupPurchase a credit pack by pack_id — charged off-session to the account's Stripe payment method on file.

Request — check balance

curl
curl https://lumify.ai/api/agent/credits \
  -H "Authorization: Bearer YOUR_API_KEY"
json — response
{
  "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
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.

Planning Pre-call credit-cost estimates — plan spend before you spend it. MCP: estimate_cost →

Resolve a player

POST /v1/resolve/player

Map a messy player name plus optional sport, team, event, or position clues to a Lumify player_id. Returns ambiguous or not_found instead of guessing. Prefer this before grade_slip when the name is a fragment, initial, or nickname. A sport-scoped key still binds the in-scope side of a collision and returns not_found when every hit is out of scope — it does not name the other sport. GET /v1/players?q= is a simple name list, not this resolver.

Request body

FieldTypeDescription
querystringrequiredMessy name or fragment, e.g. J. Chase.
cluesobjectOptional sport, team_id, team_text, event_id, position.
options.min_confidencenumberAuto-resolve floor (default 0.85). Unique hits below the floor return ambiguous.
options.modestringstrict (default), permissive, or ingest.

Request

curl
curl -X POST https://lumify.ai/v1/resolve/player \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query":"J. Chase","clues":{"sport":"nfl","team_text":"CIN"}}'

Response

JSON
{
  "status": "resolved",
  "player_id": 12345,
  "display_name": "Ja'Marr Chase",
  "sport": "nfl",
  "confidence": 0.90,
  "match_reasons": ["normalized_name", "team_clue", "initial"],
  "clues_used": ["sport", "team_text"]
}

Grade a slip

POST /v1/grade/slip

Grade settleable player-prop legs against this-event box counts — the same over / under / push / in_progress / dnp / no_stat engine and sport coverage as GET /v1/events/{id}/player-props (nfl, nba, ncaaf, ncaab, nhl, mlb, soccer) — plus structured game markets (h2h, spreads, totals) against the official score, and team_total against that team's official score on nfl / ncaaf / mlb / soccer (same sports as GET /team-props). Game legs use the submitted ticket line, not a book close, on any sport GET /odds already settles. Prefer structured event_id + market_key + side; player_id is required on props and forbidden on game keys and team_total; team_id is required on team_total and omitted elsewhere; line is required on props, spreads, totals, and team totals and omitted on h2h. market_key accepts common aliases (moneyline/ml → h2h, spread → spreads, total/ou → totals, team totals/tt → team_total). Mode S side is home | away | over | under | draw (draw only grades on soccer — off soccer it returns unmapped). Spreads line is signed to the submitted side, not the favorite — a home −3.5 favorite is {"side":"home","line":-3.5}, and the away underdog on the same game is {"side":"away","line":3.5}. Free-text game phrases (Chiefs -3.5 tonight, Over 47.5 vs Bills, Yankees ML yesterday, Yankees to win last night, Yankees over 4.5 runs, Chiefs team total over 24.5, Novak Djokovic ML) grade when the team or player and event resolve uniquely. A tennis moneyline leftover is a player, not a team; vs is the opponent player. Last-name-only tennis stays unmapped. A last-name-only player prop grades when a date is present (e.g. Bergeron over 1.5 shots on goal last night) and that surname is unique in scope — a named sport, team, or event narrows it (e.g. Johnson nhl last night); with no such clue, uniqueness is across every prop sport. No date, or a surname shared by more than one player in that scope, stays unmapped. vs/at on a player prop is the opponent, not the player's team. anytime goal/anytime assist implies soccer_goals/soccer_assists over 0.5, and over a hit/over a homer implies hits/home_runs over 1.0 — neither needs an explicit number in the text. A bare Over 47.5 stays unmapped — it never picks the first slate game. Free-text player props are graded only when the player and event resolve uniquely — it does not invent a result. Cap 8 legs. Attribution is always Graded by Lumify. Not wagering advice. Call this endpoint or MCP grade_slip with an API key to grade from an app or agent. The public /grade sheet uses the same engine without a key (20 grades / IP / day; Visual cards + raw JSON). How it works: /docs/grade.

How it works lives off this page. Never-guess resolution, date words, multi-leg slips, Visual vs JSON, and the 20/day sheet: /docs/grade. Try it: /grade.

Request body

FieldTypeDescription
legs[]object[]Structured legs: event_id, market_key, side. Props also require player_id + line. Spreads/totals require line (signed to side, e.g. -3.5 for the favorite) and omit player_id. team_total requires team_id + line + over/under (nfl/ncaaf/mlb/soccer). h2h omits player, team, and line; draw only grades on soccer.
textstringFree-text slip (e.g. Chase over 74.5 receiving yards tonight, or Chiefs -3.5 / Over 47.5 vs Bills / Yankees ML / Yankees to win / Yankees over 4.5 runs / Over 52.5 Syracuse vs Pittsburgh / Novak Djokovic ML / Mbappe anytime goal / Judge over a hit / Bergeron over 1.5 shots on goal last night). Date words (today, tonight, yesterday, last night, last Wednesday, this Thursday, or an ISO date) narrow the event. A colliding nickname plus a date promotes only the unique in-window team or player; a football-sized line will not pick a baseball club, and vs plus that date keeps the unique matchup. A roster-team span (CIN, Niners) is resolved first — unique team_id scopes a player or binds a game/team-total leg; ambiguous/not_found stays on team_resolve. vs/at on a player prop is the opponent and narrows the event, not the player's team. A tennis moneyline leftover is a player; vs is the opponent player. Graded only when team/player and event are unique. A bare Over 47.5 stays unmapped.
cluesobjectOptional sport, league, event_id, team_id, team_text for free-text mode.

Request

curl
curl -X POST https://lumify.ai/v1/grade/slip \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"legs":[{"event_id":12345,"player_id":678,"market_key":"receiving_yards","line":74.5,"side":"over"}]}'

Request — game market (spread, home favorite)

curl
curl -X POST https://lumify.ai/v1/grade/slip \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"legs":[{"event_id":12345,"market_key":"spreads","line":-3.5,"side":"home"}]}'

Request — free-text main line

curl
curl -X POST https://lumify.ai/v1/grade/slip \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"Chiefs -3.5 last night","clues":{"sport":"nfl"}}'

Response

FieldTypeDescription
statusstringgraded | partial | pending | unmapped.
legs[].resultstring or nullProps: over | under | push | in_progress | dnp | no_stat. Spreads and moneyline: won | lost | push | void | in_progress. Totals: over | under | push | void | in_progress. Null when the leg is unmapped.
legs[].currentnumber or nullNumber compared to the line: prop box count, totals home+away, spreads home−away. Null on moneyline (read market.home_score / away_score).
legs[].messagestring or nullWhy the leg did not settle, when it did not.
legs[].marketobjectWhat was actually graded: event_id, market_key, line, side, settleable, plus home_team / away_team / starts_at / home_score / away_score when the event resolved.
legs[].resolveobjectPlayer bind: status, player_id, display_name, candidates (when ambiguous).
legs[].team_resolveobject or nullTeam span on a free-text leg. Unique team_id scopes the player; ambiguous/not_found stays here.
legs[].event_resolveobject or nullEvent bind on a free-text leg: status, event_id, candidates, message. Null on structured legs that already supplied event_id.
legs[].inputobjectParse echo (player, market, line, side, sport, team). Prefer market + resolve for the graded IDs.
summaryobjectgraded, pending, unmapped, hits, misses, pushes, voids. Hits/misses are your-side wins/losses. A void is not a miss.
brandingobjectAlways Graded by Lumify plus docs links.
honestyobjectStatic notes (settleable markets only; not wagering advice).
slip_idstring or nullAlways null in this version — share URLs are not persisted.

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

FieldTypeDescription
callsobject[]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

curl
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
}

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.

Statuserror.codeWhen it occurs
400bad_requestInvalid parameter — unknown status value, bad date format, date+from conflict, or date range > 90 days.
401unauthorizedMissing, malformed, invalid, inactive, or expired API key. Credit exhaustion is not a 401 — see 402.
402insufficient_credits / daily_credit_cap_exceeded / payment_failedValid key, but billing blocks access. Envelope includes upgrade_url / topup_url / billing_url. Daily cap is a 24h window (resets_at). payment_failed: two declined invoices; paid invoice restores access. May include hosted_invoice_url.
403forbidden / sport_scope_deniedValid key denied for this resource (e.g. sport not in key scopes). Structured extras may include sport, granted_scopes, and upgrade_url.
404not_foundThe 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.
422validation_errorType validation failed — non-integer id, or limit outside 1–100. Field errors are listed under error.errors.
429rate_limit_exceeded / concurrency_limit_exceededPer-key sliding window (plan rpm, or the 20/min score-poll budget) or too many in-flight requests (16 across /v1+/mcp, 4 on score polls, 3 on grade_slip). See error.retry_after and the Retry-After header. SSE streams use stream_limit_exceeded.
500internal_errorUnexpected error. Retry with exponential backoff.

Error response shape

json — 400 example
{
  "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.

ValuePhaseDescription
scheduledPre-gameConfirmed and scheduled; not yet started
inprogressLiveCurrently being played
delayedPre-game holdStart pushed back but game has not begun (e.g. weather delay before first pitch)
suspendedMid-game haltPlay stopped after the game began (e.g. rain delay mid-inning)
postponedPre-gameMoved to a different date entirely
cancelledTerminalWill not be played
finalTerminalConcluded. Check result_type for how it ended.
walkoverTerminalTennis — opponent withdrew before the match. Winner is set; no score recorded.

Result type values

Present on final events. Describes how the outcome was reached.

ValueSportsDescription
regulationAllDecided in normal time
overtimeNHL, NBA, NFL, Soccer (AET)Decided in extra time or OT period
shootoutNHL, Soccer (PEN)Decided by penalty shootout
retiredTennisOpponent retired mid-match due to injury
walkoverTennisOpponent withdrew before the match

Sports Coverage

Yes means the public route accepts the sport — an empty off-season board is still Yes (available: false, not HTTP 400). Some means live with a league gate. No means the sport is not on that surface.

Yes Shipped (slate may be empty off-season) Some League-gated (soccer MLS + big-five) No Not offered for this sport
Surface MLBNFLNCAAFNBANCAABNHLSoccerTennis
Event Events YesYesYesYesYesYesYesYes
Stats Stats YesYesYesYesYesYesYesYes
Odds Game odds YesYesYesYesYesYesYesYes
Odds Player props YesYesYesYesYesYesYesNo
Odds Period odds YesYesYesYesYesNoYesYes
Odds Team totals YesYesYesNoNoNoYesNo
Splits Public splits YesYesYesYesYesYesNoNo
Injuries Injury / availability YesYesNoNoNoNoNoNo
Intelligence Probability & Price YesYesYesNoNoNoSomeYes
Intelligence Main-line EV YesYesYesNoNoNoSomeYes
Intelligence Match context YesYesYesYesYesYesYesYes
Intelligence Forecasts YesYesYesYesYesYesNoYes

Notes and league slugs (NFL, NBA, MLB, NHL, NCAAF, NCAAB, ATP/WTA, soccer mls + big-five + ucl): /docs/sports-coverage. Tennis doubles and qualifying return available: false on /stats.

All timestamps are stored and returned in UTC. Use the venue timezone field to convert to local time for display.