API Reference

Every capability of the Lumify v1 API, grouped by what it does rather than its raw path. New to the API? Start with the Docs overview for auth and rate limits.

For agents: the machine-readable twin of this entire reference is llms-full.txt (generated from the OpenAPI schema), or explore interactively via ReDoc / /openapi.json. Discovery/manifest for autonomous agents lives at /.well-known/agent.json.

API key Sign in, or get an instant trial key below — 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 the full season calendar across all leagues. Use this endpoint to look up a season_id before filtering events by season, or to find historical seasons for a sport.

Query parameters

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

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, fifa_world_cup). 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: 1100.
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).
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
      "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: 1100.

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-progressinprogress; 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-DDdate, 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 up to 2 extra round trips. Each include parameter adds +1 credit to the call cost. Use /v1/events/{id}/score when you only need live score data.

Path parameters

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. Adds +1 credit (total 2 credits).
include_intelligencebooleanoptionalfalse When true, embeds the bet intelligence payload under an intelligence key — same shape as GET /v1/events/{id}/intelligence. Adds +1 credit (total 2 credits).
bookmakerstringoptionalsystem default Bookmaker for intelligence.bets[].market prices when include_intelligence=true. Valid values: pinnacle, fanduel, draftkings. Defaults to the system-configured book.

Credit costs: A plain call costs 1 credit. Adding include_odds=true costs 2 credits. Adding include_intelligence=true costs 2 credits. Using both costs 3 credits — cheaper than fetching event + odds + intelligence as three separate calls (3 credits each).

Request

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

# Compound — event + odds + intelligence in one call (3 credits)
curl "https://lumify.ai/v1/events/4812?include_odds=true&include_intelligence=true" \
  -H "Authorization: Bearer YOUR_API_KEY"
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 (3 credits)
resp = requests.get(
    "https://lumify.ai/v1/events/4812",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    params={"include_odds": "true", "include_intelligence": "true"},
)
event = resp.json()  # event.odds and event.intelligence are now populated
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 (3 credits)
const eventFull = await fetch(
  "https://lumify.ai/v1/events/4812?include_odds=true&include_intelligence=true",
  { headers: { "Authorization": "Bearer YOUR_API_KEY" } }
).then(r => r.json());
// eventFull.odds and eventFull.intelligence are populated

Response

json
// Team sport example (NHL)
{
  "id":                   4812,
  "name":                 "Bruins vs Maple Leafs",
  "sport":                "nhl",
  "league":               "nhl",
  "season_id":            1,
  "starts_at":            "2026-05-10T23:00:00Z", // UTC
  "scheduled_start_at":   null,                   // original time before drift; set for tennis
  "starts_at_qualifier":  null,                   // exact | not_before | following | tbd
  "status":               "final",
  "result_type":          "overtime",             // regulation | overtime | shootout | retired | walkover
  "period":               null,                   // null after game ends; "3", "OT" while live
  "period_label":         null,                   // human-readable period, e.g. "Set 2", "Q3"
  "clock":                null,                   // null after game ends; "8:42" while live
  "round":                "Round 2",
  "draw_type":            null,                   // singles | doubles (tennis); null for team sports
  "neutral_site":         false,
  "broadcast":            "ESPN",
  "court":                null,                   // named court, e.g. "Centre Court" (tennis)
  "order_of_play":        null,                   // 1 = first match of the day on this court
  "competition": null,                          // populated for tennis ATP/WTA events — see tennis example below
  "venue": {
    "id":       1,
    "name":     "TD Garden",
    "city":     "Boston",
    "country":  "USA",
    "surface":  "ice",
    "capacity": 17850,
    "timezone": "America/New_York"       // convert starts_at to local time with this
  },
  "participants": [
    {
      "role":          "home",              // home | away (team sports); player_1 | player_2 (tennis)
      "score":         "3",                // null before game starts; "6-4, 7-5" for tennis
      "game_score":    null,               // live in-game score, e.g. "40-15" (tennis); null otherwise
      "is_winner":     true,               // null until final; true/false after
      "team": {
        "id": 1, "name": "Boston Bruins", "abbreviation": "BOS", "country_code": "USA"
      },
      "player":        null,               // null for team sports; see tennis example below
      "period_scores": []                 // per-period breakdown; empty [] if unavailable
    },
    {
      "role": "away", "score": "2", "game_score": null, "is_winner": false,
      "team": { "id": 2, "name": "Toronto Maple Leafs", "abbreviation": "TOR", "country_code": "CAN" },
      "player": null, "period_scores": []
    }
  ],
  "updated_at": "2026-05-11T02:14:37Z"
}

// Tennis singles example — note name format, competition object, and player shape
{
  "id":                   4821,
  "name":                 "Jacob Fearnley v. Giovanni Mpetshi Perricard", // "First Last v. First Last"
  "sport":                "tennis",
  "league":               "atp",
  "season_id":            12,
  "starts_at":            "2026-05-08T10:00:00Z",
  "scheduled_start_at":   "2026-05-08T11:00:00Z", // original announced time
  "starts_at_qualifier":  "not_before",
  "status":               "final",
  "result_type":          "regulation",
  "period":               null,
  "period_label":         null,
  "clock":                null,
  "round":                "Round of 16",
  "draw_type":            "singles",
  "neutral_site":         false,
  "broadcast":            null,
  "court":                "Campo Centrale",
  "order_of_play":        2,
  "competition": {
    "id":      1,
    "name":    "ATP Rome",
    "surface": "clay",           // clay | grass | hard | indoor_hard
    "tier":    "masters_1000"   // grand_slam | masters_1000 | atp_500 | atp_250 | wta_1000 | wta_500 | wta_250
  },
  "venue": {
    "id": 7, "name": "Foro Italico", "city": "Rome", "country": "ITA",
    "surface": "clay", "capacity": null, "timezone": "Europe/Rome"
  },
  "participants": [
    {
      "participant_id": 307,                        // stable Lumify join ID for this participant in this event
      "role":          "player_1",
      "score":         "6-4, 3-6, 6-3",
      "game_score":    null,                       // live: "40-15"; null when not in a game point
      "is_winner":     true,
      "team": null,
      "player": {
        "id":           102,
        "name":         "Jacob Fearnley",          // "First Last" format
        "country_code": "GBR",                     // ISO 3-letter code
        "image_url":    "https://lumify.ai/media/players/tennis/028BdVOj.png"
      },
      "period_scores": [                          // per-set scores
        { "period": "S1", "score": 6, "tiebreak": null, "confirmed": true },
        { "period": "S2", "score": 3, "tiebreak": null, "confirmed": true },
        { "period": "S3", "score": 6, "tiebreak": null, "confirmed": true }
      ]
    },
    {
      "participant_id": 308,
      "role":          "player_2",
      "score":         "4-6, 6-3, 3-6",
      "game_score":    null,
      "is_winner":     false,
      "team": null,
      "player": {
        "id":           217,
        "name":         "Giovanni Mpetshi Perricard",
        "country_code": "FRA",
        "image_url":    null                         // null until enrichment runs; format: lumify.ai/media/players/tennis/{hash}.png
      },
      "period_scores": [
        { "period": "S1", "score": 4, "tiebreak": null, "confirmed": true },
        { "period": "S2", "score": 6, "tiebreak": null, "confirmed": true },
        { "period": "S3", "score": 3, "tiebreak": null, "confirmed": true }
      ]
    }
  ],
  "updated_at": "2026-05-08T14:31:00Z"
}

Compound response — with include_odds=true&include_intelligence=true

json
// Standard event fields are unchanged — two additional top-level keys are appended
{
  "id": 4812,
  // ... all standard event fields ...
  "updated_at": "2026-05-10T02:14:37Z",

  "odds": {
    "available": true,
    "bookmakers": [
      {
        "bookmaker": "pinnacle",
        "markets": [
          {
            "key": "h2h",
            "label": "moneyline",
            "outcomes": [
              { "outcome": "Boston Bruins",       "price": -140, "point": null },
              { "outcome": "Toronto Maple Leafs", "price": 120,  "point": null }
            ]
          }
        ],
        "captured_at": "2026-05-10T01:30:00Z"
      }
    ],
    "last_updated": "2026-05-10T01:30:00Z"
  },

  "intelligence": {
    "available": true,
    "odds_source": "pinnacle",
    "has_recommend": true,
    "analyst_take": "...",
    "match_overview": null,
    "intelligence_updated_at": "2026-05-10T01:45:00Z",
    "bets": [ /* same bet objects as GET /v1/events/{id}/intelligence */ ]
  }
}

Caching note: When include_odds=true, the compound response is cached for 2 minutes (pre-game/live) rather than the standard 5-minute event TTL, to reflect the faster-moving odds data. Final events remain cached for 1 hour.

Tennis scheduling: For tennis, starts_at is a floor (not a fixed time). scheduled_start_at preserves the original announced time. starts_at_qualifier will be not_before for order-of-play matches. Use court and order_of_play to understand draw position.

Batch get events

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 on each event (+1 credit per event when odds are available).
include_intelligencebooleanoptional Inline bet intelligence on each event (+1 credit per event when available).
bookmakerstringoptional Bookmaker for inlined odds; defaults to pinnacle. Use all for every book.

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 optimised for live-polling. Returns only the fields needed to render a scoreboard: status, period, clock, and per-participant scores. Cache TTL is 30 seconds when status=inprogress, and 5 minutes otherwise — poll this endpoint instead of /v1/events/{id} when you only need score state.

Path parameters

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 Confidence scores, signal breakdowns, validator output, LLM-generated narratives, and public betting splits per event. MCP: get_intelligence, get_splits → Guide: Build an MCP betting-splits agent → Guide: Pull a full intelligence report →

Get bet intelligence for an event

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

Returns the full bet intelligence payload for an event: confidence scores for each bet type, individual signal breakdowns, validator output, and LLM-generated narratives. This endpoint is not cached — it always returns the latest computed data. Always returns 200 when the event exists — check available to determine whether intelligence has been computed yet. Returns 404 only if the event ID does not exist.

Sports coverage: Available for MLB, ATP/WTA tennis, FIFA World Cup 2026 soccer, NFL, and NCAAF. Intelligence is computed by the Lumify analysis pipeline and updated after each run. has_recommend: null means the pipeline has not yet run for that event. NFL and NCAAF analysis is seasonal — the pipelines self-skip in the offseason, so intelligence is only present during the football season.

MLB: Uses a 6-bet model. Bet tokens are ML_P1 (home), ML_P2 (away), SPREAD_P1, SPREAD_P2, OVER, UNDER. The model scores 9 core signals plus up to 4 optional supplementary signals (umpire factor, batting trend, travel fatigue, injury impact) returned in signals.signal_meta. OVER/UNDER confidence is capped at 0.80 — very_high tier is only reachable on moneyline and spread bets. Bets at extreme moneyline prices (≤ −400) are filtered out and will not appear in the response.

Soccer (FIFA World Cup): Soccer uses a 3-way moneyline model. Bet tokens are ML_HOME, ML_AWAY, and ML_DRAW — each scored independently with its own confidence score, narrative, and tier. Signal keys are the same as tennis but map to soccer-specific concepts and different max points (see signal table below). Filter events with sport=soccer&league=fifa_world_cup.

NFL & NCAAF: Both use the same P1/P2 bet tokens as MLB — ML_P1 (home), ML_P2 (away), SPREAD_P1, SPREAD_P2, OVER, UNDER. Signals map to football-specific concepts. NFL: QB Edge, Offensive Efficiency, Defensive Strength, Situational/Weather, Recent Form, Market Odds, Research Alignment, Head-to-Head, Betting Splits. NCAAF: SP+ Power Rankings, Offensive/Defensive Efficiency, Market Odds, Research Alignment, Recent Form, Home Field/Weather, Head-to-Head, Betting Splits. Human-readable labels for each signal are included in the signals._labels object.

Path parameters

ParameterTypeDescription
idintegerrequired Lumify event ID. Returns 404 if the event doesn't exist. If the event exists but intelligence has not been computed yet, returns 200 with available: false and empty bets array.

Request

curl
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());

Response

json
{
  "event_id":                4821,
  "available":               true,        // false when the pipeline hasn't run yet
  "players": {                                         // maps bet-token roles to players/teams
    "player_1": { "name": "Jacob Fearnley",            "player_id": 44, "team_id": null },
    "player_2": { "name": "Giovanni Mpetshi Perricard", "player_id": 51, "team_id": null }
  },
  "has_recommend":           true,       // false = all bets avoided; null = pipeline hasn't run
  "analyst_take":            "Expert consensus leans toward Fearnley given his dominant clay-court form and 3-1 H2H advantage. The market has installed him as a moderate favorite at -185, though prevailing sharp money has shown some interest on the underdog at +155. The narrow spread pricing suggests bookmakers expect a competitive match.",
  "match_overview":          null,         // populated instead of analyst_take when no bets are recommended
  "intelligence_updated_at": "2026-05-13T05:09:50Z",
  "bets": [
    {
      "bet_type":                       "ML_P1",          // MLB: ML_P1 (home) | ML_P2 (away) | SPREAD_P1 | SPREAD_P2 | OVER | UNDER
                                                             // tennis: ML_P1 | ML_P2 | SPREAD_P1 | SPREAD_P2 | OVER | UNDER
                                                             // soccer: ML_HOME | ML_AWAY | ML_DRAW | SPREAD_HOME | SPREAD_AWAY | OVER | UNDER
      "player_role":                    "player_1",       // matches participants[].role in GET /v1/events/{id}; null for OVER/UNDER
      "player_id":                      44,               // permanent player ID — matches participants[].player.id; null for OVER/UNDER
      "team_id":                        null,             // set for team sports (MLB etc.); null for player sports (tennis)
      "player_name":                    "Jacob Fearnley",
      "tier":                           "moderate",  // very_high | strong | moderate | avoid
      "confidence_score":               0.646,       // 0.0–1.0, after validator adjustment
      "confidence_score_pre_validator": 0.652,       // pre-validator score; null if validator hasn't run
      "coverage":                       1.0,         // fraction of 100-pt model that could be scored
      "market": {
        "price": -185,                              // American odds; null if no market data
        "line":  null                                // spread or total line; null for moneyline bets
      },
      "signals": {
        "signal_research":   20,   // Research Alignment         — max 22 pts (all sports)
        "signal_surface":    12,   // Surface Performance (tennis, max 15) | Tournament Context (soccer, max 8)
        "signal_serve_rtn":  8,    // Serve / Return Edge (tennis, max 12) | Attack / Defense Edge (soccer, max 12)
        "signal_form":       10,   // Recent Form L10 (tennis, max 12)     | Recent Form (soccer, max 15)
        "signal_surface_frm":6,    // Surface Form L10 (tennis, max 8)     | Travel / Neutral (soccer, max 5)
        "signal_market_odds":4,    // Market Odds Value          — max  8 pts (all sports)
        "signal_fatigue":    6,    // Fatigue / Context (tennis, max 8)    | Goal Environment (soccer, max 8)
        "signal_h2h":        5,    // Head-to-Head (tennis, max 5)         | Head-to-Head (soccer, max 10)
        "signal_ranking":    8,    // Ranking Differential (tennis, max 10) — omitted for MLB; FIFA Ranking Edge (soccer, max 15)
                                                          // signal_splits omitted for tennis (Owls doesn't support tennis splits)
        "_earned_pts":       84,   // sum of all non-null signal values
        "_max_pts":          108   // max possible given available data (differs by sport)
      },
      "validator": {
        "stance":       "neutral",   // validate | neutral | invalidate
        "confidence":   "medium",    // conviction in the stance: high | medium | low
        "delta":        -0.006,      // adjustment applied to pre-validator score
        "validated_at": "2026-05-13T05:09:43Z"
      },                                               // null if validator hasn't run for this bet
      "narrative":    "Fearnley enters this match having won 7 of his last 10 clay-court matches and holds a commanding H2H edge. The moneyline at -185 reflects his status as a significant favorite, and his superior surface form justifies the price.",
      "rationale": [                                     // structured bullets derived from signal scores — agent-parseable
        "Strong Research Alignment (20/22)",
        "Strong Surface Performance (12/15)",
        "Moderate Serve / Return Edge (8/12)",
        "Strong Recent Form (10/12)",
        "Strong Head-to-Head (5/5)",
        "Deep Research returns no strong signal"
      ],
      "attribution": [                                   // data sources that contributed meaningful signal
        "research_alignment",
        "surface_stats",
        "serve_return",
        "recent_form",
        "head_to_head",
        "deep_research"
      ],
      "computed_at":  "2026-05-13T03:41:17Z"
    }
    // … ML_P2, SPREAD_P1, SPREAD_P2, OVER, UNDER entries follow (MLB / tennis) …
    // … ML_AWAY, ML_DRAW, SPREAD_HOME, SPREAD_AWAY, OVER, UNDER entries follow (soccer) …
  ]
}

Top-level fields

FieldTypeDescription
availablebooleanfalse when the analysis pipeline has not yet run for this event. All other fields will be empty/null — check this first before reading bets.
playersobjectMaps player_1 and player_2 to {name, player_id}. Use player_id to cross-reference with participants[].player.id in GET /v1/events/{id}.
has_recommendboolean | nulltrue if at least one bet meets the recommendation threshold; false if all avoided; null if the pipeline hasn't run yet
analyst_takestring | nullEvent-level narrative summarising expert consensus and market sentiment. Present when at least one bet is recommended.
match_overviewstring | nullShort overview used when no bets are recommended. Explains why no clear edge exists. Mutually exclusive with analyst_take.
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.
matchupobject | nullMLB only. Starting pitcher matchup derived from the research pipeline. Contains home_starter and away_starter objects, each with name (string), hand (pitching hand, if known), era (ERA, if available), and confirmed (boolean — whether the starter is confirmed vs. projected). null when research data is unavailable or for non-MLB events.

Bet object fields

FieldTypeDescription
bet_typestringMLB: ML_P1 (home), ML_P2 (away), SPREAD_P1, SPREAD_P2, OVER, UNDER. Tennis: ML_P1, ML_P2, SPREAD_P1, SPREAD_P2, OVER, UNDER. Soccer: ML_HOME, ML_AWAY, ML_DRAW, SPREAD_HOME, SPREAD_AWAY, OVER, UNDER. Soccer moneyline is a 3-way market — ML_HOME, ML_AWAY, and ML_DRAW are all scored independently.
player_rolestring | nullplayer_1 or player_2 — matches participants[].role in GET /v1/events/{id}. null for OVER/UNDER.
player_idinteger | nullPermanent player identity — matches participants[].player.id in GET /v1/events/{id}. Stable across all events. null for OVER/UNDER and team sports.
team_idinteger | nullPermanent team identity for team-sport bets (MLB, NHL etc). null for player-sport bets (tennis) and OVER/UNDER.
tierstring | nullRecommendation tier: very_high, strong, moderate, avoid
confidence_scorenumberFinal confidence (0.0–1.0) after validator adjustment
confidence_score_pre_validatornumber | nullConfidence score before the deep-research validator ran. null if the validator has not yet processed this bet. Useful for measuring validator impact.
coveragenumberSignal coverage ratio (0.0–1.0). 1.0 = all applicable signals had sufficient data.
market.priceinteger | nullAmerican odds, e.g. -185, +155
market.linenumber | nullSpread or total line, e.g. -3.5, 22.5. null for moneyline bets.
signals.signal_*integer | nullIndividual signal scores. null when data was unavailable for that signal. Signal keys are shared across sports but map to different concepts and max points depending on the sport. MLB max pts: signal_surface 25 (Starting Pitching), signal_serve_rtn 15 (Bullpen), signal_form 15 (Lineup/OPS), signal_market_odds 10 (Market Odds), signal_research 20 (Research Alignment), signal_splits 8 (Sharp Money), signal_fatigue 8 (Recent Form), signal_surface_frm 7 (Park & Weather), signal_h2h 5 (H2H). Tennis max pts: signal_research 22, signal_surface 15, signal_serve_rtn 12, signal_form 12, signal_surface_frm 8, signal_market_odds 8, signal_fatigue 8, signal_h2h 5, signal_ranking 10. (signal_splits is omitted from tennis responses — Owls Insight does not support tennis splits.) Soccer max pts: signal_research 22 (Research Alignment), signal_ranking 15 (FIFA Ranking Edge), signal_form 15 (Recent Form), signal_serve_rtn 12 (Attack/Defense Edge), signal_h2h 10 (Head-to-Head), signal_surface 8 (Tournament Context), signal_market_odds 8, signal_fatigue 8 (Goal Environment), signal_surface_frm 5 (Travel/Neutral), signal_splits 8.
signals.signal_metaobject | nullMLB only. JSON object containing optional supplementary signal scores that did not fit the shared signal columns. Keys: umpire (0–5, home plate umpire run-environment factor), batting_trend (0–3, L15 OPS vs season delta), travel (0–3, time-zone fatigue), injury (0–3, IL key-position player differential). Each key is omitted when data was unavailable. null for all non-MLB sports.
signals._earned_ptsintegerSum of all non-null signal values for this bet
signals._max_ptsintegerMaximum possible points given available signal data.
validator.stancestring | nullvalidate, neutral, or invalidate
validator.confidencestring | nullConviction level of the validator stance: high, medium, or low
validator.deltanumber | nullConfidence adjustment applied by the validator. Positive = boost, negative = reduction.
narrativestring | nullLLM-generated bet rationale written for human consumption. Typically present for recommended bets; null for avoid tier.
rationalestring[]Structured list of signal-derived bullets, designed for agent consumption. Each entry describes a signal's contribution, e.g. "Strong Research Alignment (20/22)". Always present — contains at least one entry.
attributionstring[]List of data-source keys that contributed meaningful signal to this bet, e.g. ["research_alignment", "surface_stats", "deep_research"]. Useful for agents reasoning about signal provenance.

Workflow tip: Call /v1/events to list events — use sport=mlb&status=scheduled for MLB, sport=tennis&status=scheduled for tennis, sport=soccer&league=fifa_world_cup&status=scheduled for World Cup matches, or sport=nfl / sport=ncaaf during football season. Then fetch /v1/events/{id}/intelligence for any game you want bet analysis on. Check has_recommend first — if false, read match_overview; if true, look for bets where tier is not "avoid" and read narrative + analyst_take. For MLB, ML_P1 is always the home team. For soccer, all three moneyline outcomes (ML_HOME, ML_AWAY, ML_DRAW) are scored independently — check each one.

Get betting splits for an event

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 ~30 minutes. Always returns 200 when the event exists — check available to determine whether splits have been ingested yet. Returns 404 only if the event ID does not exist.

Sports coverage: Available for MLB, NBA, NHL, and NFL (during their respective seasons). Tennis, soccer, and NCAAF splits are not available on the Owls Insight v1 API. Soccer odds are available via /v1/events/{id}/odds. Splits are only ingested for pre-game events; data is not updated once a game starts.

Path parameters

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,
  "captured_at": "2026-05-17T14:15:12Z",
  "consensus": {
    "moneyline": {
      "home":  { "bets_pct": 92, "handle_pct": 96, "price": -149 },
      "away":  { "bets_pct": 8,  "handle_pct": 4,  "price": 123  }
    },
    "spread": {
      "home":  { "bets_pct": 87, "handle_pct": 98, "line": -1.5 },
      "away":  { "bets_pct": 13, "handle_pct": 2,  "line": 1.5  }
    },
    "total": {
      "over":  { "bets_pct": 78, "handle_pct": 81, "line": 7.0 },
      "under": { "bets_pct": 22, "handle_pct": 19, "line": 7.0 }
    }
  },
  "books": [
    {
      "book": "dk",
      "name": "DraftKings",
      "moneyline": {
        "home":  { "bets_pct": 83, "handle_pct": 93, "price": -149 },
        "away":  { "bets_pct": 17, "handle_pct": 7,  "price": 123  }
      },
      "spread": {
        "home":  { "bets_pct": 74, "handle_pct": 95, "line": -1.5 },
        "away":  { "bets_pct": 26, "handle_pct": 5,  "line": 1.5  }
      },
      "total": {
        "over":  { "bets_pct": 56, "handle_pct": 62, "line": 7.0 },
        "under": { "bets_pct": 44, "handle_pct": 38, "line": 7.0 }
      }
    }
  ]
}

Response fields

FieldTypeDescription
availablebooleanfalse when no splits have been ingested yet. consensus will be an empty object and books 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 books. 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.
booksarrayPer-bookmaker breakdown. Same structure as consensus but for a single book. book is the bookmaker key (e.g. dk); name is the display name.

Sharp-money signal: When handle_pct significantly exceeds bets_pct on a side (typically ≥20pp gap), it indicates that a small number of large bets — characteristic of sharp bettors — are backing that side against the public. Use this in combination with /v1/events/{id}/intelligence for context on line movement.

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
sportstringoptionalSport slug, e.g. nba, nhl, nfl.
leaguestringoptionalLeague slug, e.g. nba.
conferencestringoptionalConference filter, e.g. Eastern.
divisionstringoptionalDivision filter, e.g. Atlantic.
countrystringoptionalISO country code, e.g. USA.
qstringoptionalPartial team-name search.
activebooleanoptionalFilter by active status. Omit to return both active and inactive teams.
after_idintegeroptionalPagination cursor from next_after_id.
limitintegeroptional25Page size, range 1100.

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",
      "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",
  "sport":         "nba",
  "league":        "nba",
  "city":          "Boston",
  "state":         "MA",
  "country_code":  "USA",
  "conference":    "Eastern",
  "division":      "Atlantic",
  "venue":         { "id": 1, "name": "TD Garden", "city": "Boston" },
  "is_active":     true
}
Players Player profiles, rankings, and event history across all supported sports. MCP: search_players, get_player, get_player_events →

List players

GET /v1/players Open in Playground

Returns a paginated list of players. Supports filtering by sport, country, name search, active status, and ranking. Results are sorted by ascending player ID.

Query parameters

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 players with a current ATP/WTA ranking. Useful for tennis leaderboard use cases.
after_idintegeroptional Pagination cursor. Pass next_after_id from the previous response.
limitintegeroptional25 Page size. Range: 1100.

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,
      "tennis_ranking":        1,
      "tennis_ranking_points": 14350,
      "current_team_id":       null,
      "current_team_name":     null,
      "is_active":             true,
      "retired_at":            null,
      "image_url":             null           // lumify.ai/media/players/tennis/{hash}.png when available
    }
  ],
  "has_more":       true,
  "next_after_id":  25            // null on the last page
}

Get a player

GET /v1/players/{id} 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,
  "tennis_ranking":        1,
  "tennis_ranking_points": 14350,
  "current_team_id":       null,
  "current_team_name":     null,
  "is_active":             true,
  "retired_at":            null,
  "image_url":             null           // lumify.ai/media/players/tennis/{hash}.png when available
}

List a player's events

GET /v1/players/{id}/events 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: 1100.

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. Defaults to Pinnacle only (1 credit). Use bookmaker=all or a comma-separated list to fetch multiple books at 2 credits. Data is updated every ~30 minutes and cached for 2 minutes. Always returns 200 when the event exists — check available to determine whether odds have been ingested yet. Returns 404 only if the event ID does not exist.

Soccer (FIFA World Cup): Filter events with league=fifa_world_cup. Soccer moneyline (h2h) is a 3-way market — outcomes include both team names plus Draw. Asian handicap spreads use goal lines (e.g. -0.5, +0.5). Odds are ingested for all known-team fixtures during the tournament window; TBD knockout placeholders (e.g. "Round of 32 Winner") will return available: false until teams are determined.

Path parameters

NameTypeDescription
idintegerLumify event ID.

Query parameters

NameTypeDefaultDescription
bookmaker string pinnacle Bookmaker filter. Accepted values: pinnacle, fanduel, draftkings, all, or a comma-separated combination (e.g. fanduel,draftkings).
1 credit for a single bookmaker  ·  2 credits for multiple or all.

Request — Pinnacle only (default, 1 credit)

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

Request — all bookmakers (2 credits)

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, fanduel. The set of available bookmakers depends on what odds have been ingested for that event.
marketsarrayMarkets for this bookmaker, ordered: moneyline → spread → totals.
keystringMarket key: h2h, spreads, or totals.
labelstringHuman-readable label: moneyline, spread, or totals.
outcomesarrayEach outcome has outcome (name), price (American odds integer), and point (spread/total line; null for moneyline). Soccer h2h includes three outcomes: home team, away team, and Draw.
captured_atstringUTC 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 only (1 credit). Use bookmaker=all or a comma-separated list for multiple books at 2 credits. Ordered newest-first. Useful for detecting sharp line movement. Not cached. Returns 404 if no movement has been recorded.

Path parameters

NameTypeDescription
idintegerLumify event ID.

Query parameters

NameTypeDefaultDescription
bookmaker string pinnacle Bookmaker filter. Accepted values: pinnacle, fanduel, draftkings, all, or a comma-separated combination.
1 credit for a single bookmaker  ·  2 credits for multiple or all.
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
}
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 automatically when the event finishes or after 5 minutes, whichever comes first. Use this instead of polling /v1/events/{id}/score when you want push-based updates.

Auth for EventSource clients. Browser EventSource cannot set custom headers, so this endpoint also accepts the key as ?api_key=lmfy-... in addition to the standard Authorization: Bearer header.

Path parameters

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());

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}

Concurrency limit. Each API key may hold a limited number of concurrent streams. Exceeding it returns 429 with error.code: "stream_limit_exceeded" — close an existing stream and retry.

Manage webhook subscriptions

Webhooks push score changes, status transitions, and line moves to your own endpoint — no polling or open connections required. Delivery is performed by the ingest pipeline; each payload is signed with the subscription's signing_secret (HMAC-SHA256) so you can verify authenticity.

Create a subscription

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, intelligence. Defaults to ["score", "status"].
sportstringoptionalRestrict to one sport slug. Omit to subscribe across sports.
event_idintegeroptionalRestrict to a single event. Omit to subscribe to all matching events.
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"
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.

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_exceededValid key, but credits block access. Envelope includes upgrade_url and often topup_url. The free-tier daily cap is a rolling 24-hour window; daily_credit_cap_exceeded includes resets_at and window_hours.
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_exceededRate limit exceeded. See error.retry_after and the Retry-After header.
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

SportLeague slugTypeLive scores
NFLnflTeam leagueYes
NBAnbaTeam leagueYes
MLBmlbTeam leagueYes
NHLnhlTeam leagueYes
NCAAFncaafTeam leagueYes (in season)
NCAABncaabTeam leagueYes (in season)
Tennisatp, wtaIndividual tourYes
Soccerfifa_world_cupTournamentYes (tournament dates only)
SoccermlsTeam leagueYes (in season)
Soccerepl, la_liga, serie_a, bundesliga, ligue_1Team leagueYes (in season)
SocceruclTournamentYes (in season)

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