Skip to main content

Official Python SDK for the Lumify agent-ready sports intelligence API (schedules, live scores, odds, betting splits, and AI bet intelligence).

Project description

lumify-sdk

Official Python client for Lumify, the agent-ready sports intelligence API — schedules, live scores, odds, line movement, public betting splits, and AI bet intelligence across MLB, NFL, NCAAF, NCAAB, NBA, NHL, tennis, and soccer (FIFA World Cup + MLS, EPL, La Liga, Serie A, Bundesliga, Ligue 1, and UEFA Champions League).

Status: v0.2.0 — the data-plane slice (sports/seasons/events/teams/players, SSE streaming, webhooks with automatic delivery retries, agent onboarding, pre-call cost estimates), now with both sync (Lumify) and async (AsyncLumify) clients. Same data as the REST API, the TypeScript SDK, and the MCP server — this SDK is the typed REST path, not a third implementation.

Install

pip install lumify-sdk

Requires Python 3.8+. Zero runtime dependencies for the sync client — the transport is stdlib urllib and signatures use stdlib hmac/hashlib. The async client needs the optional httpx dependency (see below).

Quick start

import os
from lumify import Lumify

client = Lumify(api_key=os.environ["LUMIFY_API_KEY"])

sports = client.sports.list()

event = client.events.get(12345, include_odds=True, include_intelligence=True)
print(event["status"], event.get("intelligence"))

Create a key at https://lumify.ai/api-keys or programmatically via client.agent.keys.create().

Async client

lumify.aio.AsyncLumify mirrors Lumify method-for-method as async/await — same resources, same REST contract, same error types. It needs the optional httpx dependency for its default transport:

pip install "lumify-sdk[asyncio]"
import asyncio
import os
from lumify.aio import AsyncLumify

async def main():
    async with AsyncLumify(api_key=os.environ["LUMIFY_API_KEY"]) as client:
        sports, event = await asyncio.gather(
            client.sports.list(),
            client.events.get(12345, include_odds=True),
        )
        async for e in client.events.iterate(sport="nfl", status="scheduled"):
            print(e["id"], e["starts_at"])

asyncio.run(main())

It's a separate import path (lumify.aio, not exported from top-level lumify) so importing the zero-dependency sync client never pulls in httpx. Use async with (or await client.aclose()) to release the pooled HTTP connection. Pagination helpers (.paginate()/.iterate()) are async generators; client.events.stream(id) is an async iterator over SSE score updates; client.webhooks.verify(...) stays synchronous (it's pure local HMAC computation, nothing to await).

Resources

client.sports.list()  # client.seasons.list()
client.events.list(**filters)  # .get(id, include_odds=?, include_intelligence=?, bookmaker=?)
client.events.batch_get(event_ids)  # up to 25 ids in one round-trip; see below
client.events.query(text, limit=None)  # natural-language search, e.g. "live nfl games today"
client.events.odds(id)  # .odds_history(id) / .score(id) / .intelligence(id) / .splits(id)
client.events.stream(id)              # SSE iterator of live score updates
client.events.paginate(**filters)     # .iterate(**filters) — cursor pagination helpers
client.teams.list(**filters)  # .get(id)
client.players.list(**filters)  # .get(id) / .events(id, **filters)
client.webhooks.create(url=...)  # .list() / .delete(id) / .deliveries(id) / .verify(secret, header, raw_body)
client.agent.keys.create()  # .list() / .revoke(id)
client.agent.credits.get()  # .list_packs() / .topup(pack_id)
client.estimate.cost(calls)  # pre-call credit-cost estimate, always free — see below
client.estimate.list_tools()  # tool names .cost() understands

Estimating cost before you call

client.estimate.cost(...) tells you the credit range a planned call (or batch of calls) will cost without making it — costs are data-dependent (e.g. odds not yet posted are free), so each result is a min_credits / max_credits range, not a single number. Always free to call:

result = client.estimate.cost([
    {"tool": "get_event", "arguments": {"event_id": 12345, "include_odds": True}},
    {"tool": "batch_get_events", "arguments": {"event_ids": [1, 2, 3]}},
])
result["total_min_credits"]  # cheapest realistic total across all calls
result["total_max_credits"]  # priciest realistic total across all calls

Every method maps 1:1 to a REST endpoint and returns the same JSON shape you'd get from curl (a plain dict, typed as the matching model in lumify.models) — see https://lumify.ai/docs/reference for full field docs.

Pagination

List endpoints are cursor-paginated (after_id/limit, max 100). Use the iterator helpers instead of tracking cursors by hand:

for event in client.events.iterate(sport="nfl", status="scheduled"):
    print(event["id"], event["starts_at"])

# Or page-by-page (events pages use "events", not "data"):
for page in client.events.paginate(sport="nfl", limit=50):
    print(len(page.get("events", [])), "events, next cursor:", page.get("next_after_id"))

Standalone helpers paginate() / iterate_items() are also exported for custom fetchers.

Batch event lookup

Already have a list of event ids (e.g. from client.events.list()) and want full detail for each? batch_get fetches up to 25 in a single round-trip instead of one GET per event:

result = client.events.batch_get([101, 102, 999999999], include_odds=True)
print(result["total"], "found;", result["not_found"], "missing")
for event in result["events"]:
    print(event["id"], event["status"])

Duplicate ids are billed once; ids that don't exist are returned under not_found and cost nothing — credits are the sum of each event's normal GET /v1/events/{id} cost, with the same billing-fairness rules (unavailable odds/intelligence stay free).

Natural-language search

query maps free text to the same filters client.events.list() accepts — sport, status, and date/date-range — using a small, deterministic, rule-based mapper (not an LLM call). Costs 1 credit, same as list():

result = client.events.query("live nfl games today", limit=5)
print(result["interpreted"])          # {"sport": "nfl", "status": "inprogress", "date": "2026-07-15", ...}
print(result["equivalent_request"])   # "GET /v1/events?sport=nfl&status=inprogress&date=2026-07-15"
print(result["unrecognized_terms"])   # words that didn't map to a filter
for event in result["events"]:
    print(event["id"], event["status"])

Live score streaming (SSE)

for evt in client.events.stream(event_id):
    if evt.event == "score":
        print(evt.data["status"], evt.data.get("clock"))
    if evt.event == "done":
        break

Cheaper than polling client.events.score(id) — the server only emits on change, plus periodic keep-alives, and closes when the event finishes.

Webhooks

sub = client.webhooks.create(url="https://you.example.com/hooks/lumify")
# sub["signing_secret"] ("whsec_...") is returned once — store it.

# In your webhook handler, verify against the *raw* request body:
client.webhooks.verify(signing_secret, request.headers["Lumify-Signature"], raw_body)

verify() raises WebhookSignatureError (bad format, signature mismatch, or a stale/replayed timestamp) — treat that as "reject with 4xx", not a crash.

Deliveries that fail transiently (5xx, 429, or a timeout) are automatically retried with exponential backoff (30s/5m/30m/2h/6h). Check client.webhooks.deliveries(id) to see delivery history, including retries (linked via parent_delivery_id) and whether Lumify has given up (given_up):

history = client.webhooks.deliveries(sub["id"])
for d in history["data"]:  # newest first
    print(d["attempt"], d["status_code"], d["success"], d["given_up"])

Errors

Every non-2xx response raises a typed subclass of LumifyError — switch on err.code (the stable machine-readable slug), not str(err):

from lumify import NotFoundError, RateLimitError, ValidationError

try:
    client.events.get(999999999)
except NotFoundError:
    ...
except RateLimitError as err:
    print("retry after", err.retry_after, "s")
except ValidationError as err:
    print(err.field_errors)

AuthenticationError (401), PaymentError (402), PermissionError (403, has .upgrade_url on sport-scope denials), NotFoundError (404), ValidationError (422, has .field_errors), RateLimitError (429, has .retry_after from the envelope or Retry-After header), APIError (5xx), and ConnectionError (network/timeout, never reached the server) all extend LumifyError (.code, .status, .doc_url, .request_id).

GET requests are automatically retried (default: 2 attempts) with exponential backoff on 429/5xx/network failures, honoring Retry-After. Non-idempotent requests (POST/webhook & key creation) are never auto-retried. Configure with max_retries / timeout on the Lumify constructor.

Credits and rate limits

Every successful response carries X-Credits-Used, X-Credits-Remaining, and X-RateLimit-* headers. Read them via get_meta() (attached out-of-band on the returned object, so it never pollutes json.dumps or iteration):

from lumify import get_meta

odds = client.events.odds(event_id)
meta = get_meta(odds)
print(meta.credits_used, meta.rate_limit_remaining)

Queries for data that isn't available yet (e.g. odds not yet posted) return credits_used == 0 — you're never charged for a "not available" read.

Sync with REST, MCP, and the OpenAPI contract

This SDK's response types (lumify/models.py) are generated from the same filtered OpenAPI slice the TypeScript SDK uses (clients/lumify-sdk/openapi/openapi.sdk.json, produced by scripts/export_openapi_sdk.py at the repo root), not hand-maintained. CI fails if the models are stale, so the two SDKs can't disagree with each other or drift from what the API returns. A shared cross-surface probe matrix (tests/fixtures/agent_contract.json) additionally asserts this SDK builds the exact REST request that REST and MCP agree on. The client ergonomics (this README's resource shape, pagination/SSE/webhook helpers) are hand-written.

python scripts/export_openapi_sdk.py                       # regenerate the schema slice
python clients/lumify-sdk-python/scripts/gen_models.py      # regenerate the Python models

Development

python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
pytest

License

MIT © 2026 Lumify AI

Related

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lumify_sdk-0.2.0.tar.gz (41.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lumify_sdk-0.2.0-py3-none-any.whl (48.1 kB view details)

Uploaded Python 3

File details

Details for the file lumify_sdk-0.2.0.tar.gz.

File metadata

  • Download URL: lumify_sdk-0.2.0.tar.gz
  • Upload date:
  • Size: 41.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for lumify_sdk-0.2.0.tar.gz
Algorithm Hash digest
SHA256 6c94fa937aec49244689997ffb770582c944bb39c525501567733cbced7d26c7
MD5 ece96745e684b6332e972f75f9f2bb79
BLAKE2b-256 de90172a7c97b75a9683a96b43b77474294e7cddce4e8809cbedc979462d2ce9

See more details on using hashes here.

File details

Details for the file lumify_sdk-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: lumify_sdk-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 48.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for lumify_sdk-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 92250bc23072dcb9ad61aa1c24cee5bc4bc85e49eb4b9c43e706e1643a81a58a
MD5 255f079eb3ccad44e5f60e49625024f7
BLAKE2b-256 c39eef075de096bc2cfb30c43a7ddc3410363a99f9f36f7f7c40bd9f3b649cc5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page