API reference
Everything for the ScoreTape v1 API — authentication, every endpoint, the response contract, the verified market-to-game join, and the injury webhook channel. Base URL https://api.scoretape.com/v1.
Contents▾
Introduction
ScoreTape is sports prediction-market data with the join already done. We record sports markets on Polymarket and Kalshi — moneylines, spreads, totals, props, with settlement — and entity-resolve every market to its real-world game: the final score, the multi-sportsbook odds, and the injury report as it stood before the game.
The mapping is verified continuously: settled market winners agree with the real-world result in 99.96% of checked moneylines. Live order books for sports markets and a real-time injury webhook feed (~60-second detection) round out the stack.
Coverage spans 369k+ markets across 13 leagues (nfl · nba · mlb · nhl · wnba · cfb · cbb · epl · ucl · la-liga · soccer · tennis · ufc). Delivery is a metered REST API plus signed webhooks.
Quickstart
1. Request a free API key — we send them the same day. Keys look like st_… and are shown once.
2. Make your first call — the coverage map:
curl https://api.scoretape.com/v1/leagues \
-H "X-API-Key: st_your_key_here"3. Pull one game as one joined document:
curl https://api.scoretape.com/v1/games/nba-sas-nyk-2026-06-10 \
-H "X-API-Key: st_your_key_here"{
"data": {
"game_slug": "nba-sas-nyk-2026-06-10", "league": "nba", "matched": true,
"result": { "home_team": "New York Knicks", "home_score": 107,
"away_team": "San Antonio Spurs", "away_score": 106, "winner": "home" },
"odds": [ { "provider": "DraftKings", "spread": -2.5, "over_under": 216.5,
"home_ml": -135, "away_ml": 114 } ],
"markets": [ { "kind": "moneyline", "outcomes": ["Spurs", "Knicks"],
"resolved": true, "winner_outcome": "Knicks" }, "… 118 markets" ],
"injuries": [ "… the change-log for both teams up to start + 24h" ]
},
"meta": { "request_id": "req_…", "timestamp": "2026-06-12T09:17:08.301Z" }
}4. Subscribe to injury alerts — see Webhooks.
Authentication
Every endpoint except /v1/health requires an API key. Pass it either way — both schemes are accepted:
# X-API-Key header
curl https://api.scoretape.com/v1/leagues -H "X-API-Key: st_your_key"
# or Authorization: Bearer
curl https://api.scoretape.com/v1/leagues -H "Authorization: Bearer st_your_key"Keys carry a plan that sets your rate limit and history window. Keys are stored SHA-256-hashed at rest — the raw value is shown exactly once at creation. Quotas are per user, not per key — extra keys do not multiply your limit.
| Failure | Status | code |
|---|---|---|
| No key supplied | 401 | AUTH_MISSING |
| Unknown / revoked / malformed key | 401 | AUTH_INVALID |
Base URL & versioning
All endpoints live under a single versioned base. Breaking changes get a new version prefix; v1 responses only gain fields, never lose them.
https://api.scoretape.com/v1/…https://api.scoretape.com/v1/healthno auth/v1/health reports ClickHouse liveness and the last entity-resolution build — useful as an uptime probe.
Response envelope
Every response is the same envelope. Success:
{
"data": …, // object (single resource) or array (listing)
"pagination": { // listings only
"next_cursor": "…" | null,
"has_more": true,
"limit": 50,
"count": 50
},
"meta": { "request_id": "req_…", "timestamp": "2026-06-12T09:17:08.301Z" }
}Errors:
{
"error": { "code": "GAME_NOT_FOUND", "message": "unknown game_slug" },
"meta": { "request_id": "req_…", "timestamp": "…" }
}Branch on error.code, not the message. Every response carries meta.request_id — include it when reporting an issue. Timestamps are ISO-8601 UTC; raw epoch fields end in _ms.
Pagination
Listings use opaque keyset cursors — O(1) at any depth, stable under concurrent writes. Pass pagination.next_cursor back as ?cursor=; stop when has_more is false.
curl "https://api.scoretape.com/v1/markets?league=nba&kind=moneyline&limit=100" -H "X-API-Key: $KEY"
# → "pagination": { "next_cursor": "31373831…", "has_more": true, … }
curl "https://api.scoretape.com/v1/markets?league=nba&kind=moneyline&limit=100&cursor=31373831…" \
-H "X-API-Key: $KEY"| Endpoint | Default limit | Max |
|---|---|---|
/v1/{league}/games | 50 | 200 |
/v1/markets | 50 | 200 |
/v1/games/{slug}/markets | 200 | 1000 |
/v1/{league}/injuries | 100 | 500 |
Error codes
| code | Status | Meaning |
|---|---|---|
AUTH_MISSING | 401 | No API key supplied |
AUTH_INVALID | 401 | Unknown or revoked key |
INVALID_PARAMETER | 400 | Malformed path/query value |
INVALID_CURSOR | 400 | Cursor failed to decode |
HISTORY_LIMIT_EXCEEDED | 403 | Requested data older than your plan's window |
GAME_NOT_FOUND | 404 | Unknown game_slug |
MARKET_NOT_FOUND | 404 | Unknown condition_id |
WEBHOOK_NOT_FOUND | 404 | Unknown / not your sub_id |
SNAPSHOT_NOT_FOUND | 404 | No captured book for this asset_id |
ENDPOINT_NOT_FOUND | 404 | Unknown route |
RATE_LIMIT_BURST | 429 | Per-second limit exceeded |
RATE_LIMIT_SUSTAINED | 429 | Per-minute limit exceeded |
INTERNAL_ERROR | 500 | Query failure — retry, then report with request_id |
429 responses set Retry-After; every authenticated response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-Request-Id.
Plans & rate limits
Plans gate throughput and two history windows: the tape window — how far back Kalshi and Polymarket markets, props, and order books are servable (forward-captured; it cannot be backfilled, which is why it is the product) — and the sports window (results, box scores, player logs, the sportsbook odds archive back to 1999). Injury webhook subscriptions are available on paid plans.
| Plan | Price | Burst | Sustained | Tape | Sports history | Webhooks |
|---|---|---|---|---|---|---|
| Free | $0 | 1 req/s | — | 7 days | 7 days | — |
| Pro | $29/mo | 25 req/s | 1,000 req/min | 30 days | 90 days | ✓ |
| Scale | $79/mo | 50 req/s | 3,000 req/min | 60 days | 365 days | ✓ |
| Enterprise | $200/mo | 100 req/s | — | unlimited | full archive | ✓ |
List endpoints silently clamp to your window's floor; an explicit from= (or a single game / market) older than the window returns 403 HISTORY_LIMIT_EXCEEDED. Futures markets with no game date stay visible on every plan. Both token buckets must pass — burst and sustained.
The join (entity resolution)
The spine of ScoreTape: every prediction market is resolved to a game, keyed by game_slug (e.g. nba-sas-nyk-2026-06-10 — league, teams, US/ET date). Markets are matched to ESPN games by league, date, and team identity, with the exact start time as a tie-breaker; tennis and UFC match by competitor names.
Each matched game carries the evidence:
| Field | Meaning |
|---|---|
matched | Whether an ESPN entity was resolved (unmatched games still serve) |
espn.match_method | time+names (exact start time) · day+names · surnames (tennis/UFC) |
espn.confidence | Name-match score, 0–1 |
espn.team0_side | Which ESPN side (home/away) the market's first team maps to — the key for joining moneylines to results |
Leagues & overview
/v1/leaguesCoverage map: every league with game counts, how many are matched to real-world results, total markets, and the first/last game day.
{ "data": [
{ "league": "tennis", "games": 13247, "matched": 1041, "markets": 89541,
"first_day": "2024-08-30", "last_day": "2026-06-12" },
{ "league": "mlb", "games": 3751, "matched": 1277, "markets": 20905, … }, …
] }/v1/overviewRow counts per data layer plus your plan and history window — a cheap freshness probe.
Games
/v1/{league}/gamesGames for one league (one of: nfl · nba · mlb · nhl · wnba · cfb · cbb · epl · ucl · la-liga · soccer · tennis · ufc), newest first, with the joined result inline when matched.
| Param | Type | Notes |
|---|---|---|
from | YYYY-MM-DD | Inclusive lower bound on game start |
to | YYYY-MM-DD | Inclusive upper bound |
matched | true|false | Only games with / without a resolved ESPN entity |
team | string | Case-insensitive team-name match on either side (“knicks”, “New York”) |
limit | int | Default 50, max 200 |
cursor | string | Keyset cursor from the previous page |
curl "https://api.scoretape.com/v1/nba/games?from=2026-06-01&matched=true&limit=2" -H "X-API-Key: $KEY"{ "data": [ {
"game_slug": "nba-sas-nyk-2026-06-10",
"league": "nba",
"start": "2026-06-11T00:30:00.000Z",
"title": "Spurs vs. Knicks",
"team0": "Spurs", "team1": "Knicks",
"markets": 118,
"matched": true,
"espn": { "league": "basketball/nba", "event_id": "401859966",
"team0": "San Antonio Spurs", "team1": "New York Knicks",
"team0_side": "away", "match_method": "time+names", "confidence": 1.0 },
"result": { "day": "2026-06-10", "completed": true, "winner": "home",
"home_team": "New York Knicks", "home_score": 107,
"away_team": "San Antonio Spurs", "away_score": 106 }
} ], "pagination": { … }, "meta": { … } }Game document (flagship)
/v1/games/{game_slug}One game as one document: the entity-resolution evidence, the final result (with linescores and venue), sportsbook odds (up to 15 books — spread, total, moneylines, favorite, and which side won each bet), every prediction market with settlement (up to 1,000, sorted by volume), and the injury change-log for both teams up to 24h after start.
For tennis and UFC the result is a competitors array (per-athlete winner flags), and UFC carries per-fight moneylines.
/v1/games/{game_slug}/marketsJust the markets, with kind= (e.g. moneyline, spreads, totals) and resolved= filters. Limit up to 1,000.
/v1/games/{game_slug}/boxPer-athlete box-score lines for the game — every player, every stat group, with ESPN's labels/stats arrays (PTS, REB, AST… per league).
/v1/games/{game_slug}/playsPlay-by-play in game order (cursor, limit ≤ 500): period, clock, play text, running score, and which plays scored.
/v1/games/{game_slug}/winprobESPN's per-play home win probability — replay how the game's likelihood moved against your market ticks (cursor, limit ≤ 500).
curl "https://api.scoretape.com/v1/games/nba-sas-nyk-2026-06-10/markets?kind=moneyline" -H "X-API-Key: $KEY"{ "data": [ {
"condition_id": "0xcc5ffe6f0dd2eb…",
"kind": "moneyline",
"title": "Spurs vs. Knicks",
"outcomes": ["Spurs", "Knicks"],
"tokens": ["38917726…", "…"],
"game_start": "2026-06-11T00:30:00.000Z",
"resolved": true,
"winner_outcome": "Knicks",
"winner_token": "…",
"closed_time": "2026-06-11T03:54:42.000Z",
"volume": …
} ], … }Markets
/v1/marketsBrowse all 369k+ markets. Filters: league=, kind=, resolved=true|false, plus cursor pagination. Sorted by game start, newest first; futures (no game date) sort last and are visible on every plan.
/v1/markets/{condition_id}One market by its condition_id, with its game context attached — game_slug and the full joined game header when the spine has resolved it.
Injuries
/v1/{league}/injuriesThe forward-captured injury change-log for nba · wnba · mlb · nhl · nfl · cfb · cbb — one row per status change, newest first. since=YYYY-MM-DD filters, cursor paginates. To reconstruct the report as of instant T, take each athlete's latest row with captured_ms ≤ T.
{ "data": [ {
"league": "basketball/nba",
"team": "Oklahoma City Thunder",
"athlete_id": "530853",
"athlete_name": "Jalen Williams",
"position": "G",
"status": "Out",
"injury_type": "out",
"short_comment": "Williams (hamstring) has been ruled out for Saturday's Game 7 …",
"report_date": "2026-05-29T22:22Z",
"captured": "2026-06-12T08:25:22.902Z"
} ], … }Players & props
Player identity is the ESPN athlete_id. The props↔athlete spine links each player-prop market (points, rebounds, assists, receiving_yards, anytime_touchdowns, baseball_player_home_runs, …) to the real athlete — matched against the game's own box-score roster first, settlement included.
/v1/{league}/playersEvery athlete the data knows for a league (box scores ∪ rosters ∪ injury log). q= searches by name; cursor paginates.
curl "https://api.scoretape.com/v1/nba/players?q=brunson" -H "X-API-Key: $KEY"/v1/players/{athlete_id}Profile: roster row (when carried), latest injury status, box-score game counts per league, and how many prop markets link to this player.
{ "data": {
"athlete_id": "3934672",
"athlete_name": "Jalen Brunson",
"box": [ { "league": "basketball/nba", "games": 101 } ],
"latest_injury": null,
"prop_markets": 235
} }/v1/players/{athlete_id}/injuriesThe player's own forward-captured injury change-log — replay every status flip.
/v1/players/{athlete_id}/boxGame log: box lines joined with the game's date, opponent, and final score.
/v1/players/{athlete_id}/marketsThe player's prop markets with settlement — kind= and resolved=filter, cursor paginates. Backtest a player's overs against the box line that settled them.
Historical odds (1999+)
/v1/{league}/odds-historyTwo open archives behind one endpoint. NFL (nflverse): results + spread/total/moneylines back to 1999 — filter by season=, team= (abbreviation), from=/to=; cursor paginates. Soccer (football-data.co.uk) via epl, la-liga, or soccer + div= (22 division codes): multi-book 1X2, totals, and Asian-handicap odds back to 2000/01 — filter by season= code (2324 = 2023/24) and/or team=.
curl "https://api.scoretape.com/v1/nfl/odds-history?season=1999&limit=1" -H "X-API-Key: $KEY"{ "data": [ {
"game_id": "1999_21_STL_TEN", "game_type": "SB", "gameday": "2000-01-30",
"away_team": "STL", "away_score": 23, "home_team": "TEN", "home_score": 16,
"spread_line": -7, "total_line": 48, "stadium": "Georgia Dome", …
} ], … }Order books: live & history
/v1/books/{asset_id}/latestThe newest captured order book for one outcome token (asset_id from a market's tokens array): full bid_prices/bid_sizes/ask_prices/ask_sizes ladders, best bid/ask, and exchange + receive timestamps.
/v1/books/{asset_id}/historyThe snapshot time-series for the same token, newest first — from=/to= accept YYYY-MM-DD or epoch-millis, cursor paginates (limit ≤ 500). The hot window covers roughly the last 30 days of capture; deeper history from the cold archive is on the roadmap.
# tokens[] comes from any market response
curl "https://api.scoretape.com/v1/books/3891772625554242…/history?limit=100" -H "X-API-Key: $KEY"Injury webhooks
The fastest path to injury news in your system: register a URL, get a signed POST for every status change (Questionable → Out) within ~60 seconds of detection. Filter by league and by status substring per subscription.
/v1/webhookscurl -X POST https://api.scoretape.com/v1/webhooks \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{
"url": "https://your.app/hooks/scoretape",
"leagues": ["nba", "nfl"],
"statuses": ["out", "doubtful", "questionable"]
}'{ "data": {
"sub_id": "wh_d5acf5e1091fb7ce",
"secret": "whsec_…", // shown ONCE — store it to verify signatures
"url": "https://your.app/hooks/scoretape",
"leagues": ["nba", "nfl"],
"statuses": ["out", "doubtful", "questionable"]
} }Empty leagues/statusesmean “everything”. Status filters are case-insensitive substrings — "injured reserve" matches Injured Reserve (DTR).
The event
POST https://your.app/hooks/scoretape
X-ScoreTape-Event: injury.status_change
X-ScoreTape-Signature: sha256=2f1d… // HMAC-SHA256 of the raw body, keyed by your secret
{
"event": "injury.status_change",
"league": "nba",
"team": "Oklahoma City Thunder",
"athlete": "Jalen Williams", "athlete_id": "530853", "position": "G",
"old_status": "Questionable",
"new_status": "Out",
"injury_type": "out",
"comment": "Williams (hamstring) has been ruled out …",
"report_date": "2026-05-29T22:22Z",
"captured_ms": 1781252722902,
"sent_ms": 1781252725000
}Verify the signature
import hashlib, hmac
def verify(raw_body: bytes, header: str, secret: str) -> bool:
want = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, header)Manage subscriptions
/v1/webhooksYour subscriptions with 24h delivery health (delivered_24h, failed_24h, last_delivery).
/v1/webhooks/{sub_id}/testFires a signed webhook.test event at your URL right now and returns the delivery result — verify your receiver before a real alert matters.
/v1/webhooks/{sub_id}2xx fast and process async. A storm guard caps any single pass — an upstream bulk edit can never blast your endpoint with hundreds of events.Code examples
Walk a season of joined games (Python)
import requests
BASE = "https://api.scoretape.com/v1"
H = {"X-API-Key": "st_your_key"}
cursor, games = None, []
while True:
q = f"{BASE}/nba/games?matched=true&limit=200"
if cursor: q += f"&cursor={cursor}"
page = requests.get(q, headers=H).json()
games += page["data"]
if not page["pagination"]["has_more"]: break
cursor = page["pagination"]["next_cursor"]
# joined: each game carries result + espn linkage; markets are one call away
print(len(games), "games")Did the market beat the closing line? (Python)
g = requests.get(f"{BASE}/games/nba-sas-nyk-2026-06-10", headers=H).json()["data"]
ml = next(m for m in g["markets"] if m["kind"] == "moneyline")
dk = next(o for o in g["odds"] if o["provider"] == "DraftKings")
print("market winner:", ml["winner_outcome"]) # Knicks
print("closing line:", dk["home_ml"], dk["away_ml"]) # -135 / +114
print("final:", g["result"]["home_score"], "-", g["result"]["away_score"])Injury alert receiver (Node)
import { createHmac, timingSafeEqual } from "node:crypto";
export async function POST(req) {
const raw = Buffer.from(await req.arrayBuffer());
const sig = req.headers.get("x-scoretape-signature") ?? "";
const want = "sha256=" + createHmac("sha256", process.env.SCORETAPE_WHSEC).update(raw).digest("hex");
if (!timingSafeEqual(Buffer.from(want), Buffer.from(sig))) return new Response("bad sig", { status: 401 });
const ev = JSON.parse(raw.toString());
if (ev.event === "injury.status_change" && ev.new_status === "Out") {
// re-price before the line moves
}
return new Response("ok");
}Status & changelog
What's live, newest first:
- Players & props. Player search, profiles, per-player injury history, game logs, and prop markets entity-resolved to athletes (25k+ links, box-roster verified).
- Game layers. Box scores, play-by-play, and per-play win probability on every matched game.
- Historical odds. NFL closing lines to 1999 (nflverse) and 22 divisions of European soccer odds to 2000/01 (football-data) behind
/v1/{league}/odds-history. - Book history. Order-book snapshot time-series over the hot window, plus
team=filtering on games. - Injury webhooks. Per-user signed subscriptions, 60s detection, league/status filters, test endpoint, delivery health.
- Full v1 API. Joined game documents, markets with settlement, multi-book odds, injury change-log, live books, cursor pagination, plan history enforcement.
- The verified join. 369k markets entity-resolved to games; settlement agreement 99.96% at last audit.
- Public HTTPS.
https://api.scoretape.comover TLS.
On the roadmap: order-book history past the 30-day hot window (cold-archive views), account dashboard with self-serve keys and billing, and additional event types on the webhook channel (line movement). Questions or a coverage request? Get in touch.