API reference
Sportily's horse-racing model output as JSON. Six endpoints, one auth header, one error envelope.
The API is invitation-only — you'll need a key before anything answers. Request access →
Authentication
Every request carries your secret key as a bearer token. Keys are issued per account, stored hashed, and can be rotated or revoked from your account. Never expose a key in a browser or a public repository.
Authorization: Bearer sk_live_… Base URL
All requests go to https://sportily.app/api/v1. Dates are a local racing
day in YYYY-MM-DD, and data reaches back a rolling 365 days —
enforced server-side, so a date outside it answers 422 out_of_window.
Every runner carries a stable horseId. With raceId, that pair is
the key the whole API speaks: store it, join on it, and hand it back to /races/{raceId} or match it against /lenses/matches.
Endpoint index
GET /scores 1 unit Every scored race for one racing day, with per-runner model output.GET /races/{raceId} 1 unit One race card by id — the drill-down for stored ids and lens matches.GET /lenses free The account's Race Lens definitions — the id dictionary for /lenses/matches.GET /lenses/matches Race Lens allowance Races where your lenses matched, served as the FULL field with each runner flagged by the lenses it passed. Precomputed before publish and refreshed through the racing day — a lens saved mid-day updates within seconds, and a stale read queues its own recompute (see `stale[].refresh` and `retryAfterSeconds`). Up to 6 lenses per call, batched far cheaper than separate calls.GET /usage free Current spend against both allowances, so you never poll a metered endpoint for it.GET /status free Key check plus today's publish clock.Scores
/api/v1/scores?date=YYYY-MM-DDEvery scored race for one racing day, each with its runners and their model output.
Optional region=GB|IRE|AUS and course= filters narrow the day to
the market you watch.
Before a day's scores reach their publish time the endpoint answers 409 not_yet_published rather than serving partial data.
curl -H "Authorization: Bearer sk_live_…" \
"https://sportily.app/api/v1/scores?date=2026-08-12" { "date": "2026-08-12", "publishedAt": "2026-08-12T07:00:00Z", "races": [ { "raceId": "sr_9Qk2m", "course": "Newmarket", "offTime": "2026-08-12T13:50:00Z", "raceName": "Sportily Handicap", "fieldSize": 8, "runners": [ { "horseId": "hrs_4Tn8v", "runnerNumber": 1, "horseName": "Example Runner", "jockeyName": "A. Jockey", "trainerName": "B. Trainer", "modelWinPct": 28.4, "modelRank": 1, "modelGapNext": 6.1, "speedFigure": 92.5 } ] } ]}Race by id
/api/v1/races/{raceId}One race card by id — identical to the race object inside the day search. Use it to drill into a stored id or a lens match without re-fetching the whole day.
Gated on the race's own day: tomorrow's card answers 409 until its publish time,
and a race older than the rolling window is out of reach by id too.
curl -H "Authorization: Bearer sk_live_…" \
"https://sportily.app/api/v1/races/sr_9Qk2m" { "date": "2026-08-12", "race": { "raceId": "sr_9Qk2m", "course": "Newmarket", "offTime": "2026-08-12T13:50:00Z", "raceName": "Sportily Handicap", "fieldSize": 8, "runners": [ { "horseId": "hrs_4Tn8v", "runnerNumber": 1, "horseName": "Example Runner", "jockeyName": "A. Jockey", "trainerName": "B. Trainer", "modelWinPct": 28.4, "modelRank": 1, "modelGapNext": 6.1, "speedFigure": 92.5 } ] }}Race Lens
Your Race Lens filters, evaluated for you. Lenses are created and edited in the Race Lens app — the API never defines or runs one on demand. Every lens on the account is evaluated before the day publishes, then re-evaluated through the racing day, so matches track lens edits, non-runners and re-scored races. Save a lens and today's answer refreshes within seconds; a stale read queues its own recompute and says so.
/api/v1/lensesThe account's lens definitions — the id dictionary for /lenses/matches.
Free.
curl -H "Authorization: Bearer sk_live_…" \
"https://sportily.app/api/v1/lenses" { "lenses": [ { "id": "lns_7Hd3k", "name": "Front-runners at 5f" }, { "id": "lns_2Rw9p", "name": "Top pick, big gap" } ]}/api/v1/lenses/matchesThe races where your lenses matched, served as the full field: every runner in the same
shape as /scores, each carrying a lenses array of the lens ids
it passed — empty for the rest of the field, so a hit is always read in context of the
field it beat. lens selects up to 6 lenses by id and defaults to every lens on
the account.
Read stale before trusting an empty races: a lens listed there
has not been evaluated for that day (or was edited since it was), which is a
different answer from "nothing qualified". computedAt is the oldest of the
served snapshots; per-lens times and names are in lenses.
A stale lens on today's date repairs itself: the read queues a recompute, each
stale entry carries refresh (queued, retry_shortly or unavailable), and retryAfterSeconds says when to poll again —
usually one retry is enough. Past days are never recomputed: a lens only has history from
the day it was first evaluated, and we won't backfill it with hindsight the morning run
didn't have.
curl -H "Authorization: Bearer sk_live_…" \
"https://sportily.app/api/v1/lenses/matches?date=2026-08-12&lens=lns_7Hd3k,lns_2Rw9p" { "date": "2026-08-12", "computedAt": "2026-08-12T06:45:12Z", "lenses": [ { "lensId": "lns_7Hd3k", "name": "Front-runners at 5f", "computedAt": "2026-08-12T06:45:12Z" }, { "lensId": "lns_2Rw9p", "name": "Top pick, big gap", "computedAt": "2026-08-12T06:45:14Z" } ], "races": [ { "raceId": "sr_9Qk2m", "course": "Newmarket", "offTime": "2026-08-12T13:50:00Z", "raceName": "Sportily Handicap", "fieldSize": 8, "runners": [ { "horseId": "hrs_4Tn8v", "runnerNumber": 1, "horseName": "Example Runner", "jockeyName": "A. Jockey", "trainerName": "B. Trainer", "modelWinPct": 28.4, "modelRank": 1, "modelGapNext": 6.1, "speedFigure": 92.5, "lenses": [ "lns_7Hd3k", "lns_2Rw9p" ] }, { "horseId": "hrs_8Kc2j", "runnerNumber": 2, "horseName": "Rest Of Field", "jockeyName": "C. Jockey", "trainerName": "D. Trainer", "modelWinPct": 22.3, "modelRank": 2, "modelGapNext": 4.9, "speedFigure": 88.1, "lenses": [] } ] } ], "stale": [ { "lensId": "lns_5Xb1q", "reason": "ast_changed", "refresh": "queued" } ], "retryAfterSeconds": 10}Usage & status
Both are free, so checking your budget or the publish clock never costs budget.
/api/v1/usageCurrent spend on both allowances. Race Lens reads are metered separately from everything
else, so exhausting them never blocks /scores.
curl -H "Authorization: Bearer sk_live_…" \
"https://sportily.app/api/v1/usage" { "general": { "minute": { "used": 4, "limit": 300, "resetsAt": "2026-08-12T09:31:00Z" }, "day": { "used": 812, "limit": 20000, "resetsAt": "2026-08-13T00:00:00Z" } }, "raceLens": { "minute": { "used": 6, "limit": 30, "resetsAt": "2026-08-12T09:31:00Z" }, "day": { "used": 45, "limit": 600, "resetsAt": "2026-08-13T00:00:00Z" } }}/api/v1/statusConfirms the key works and reports when today's data publishes — poll this at startup
instead of retrying /scores against a 409.
curl -H "Authorization: Bearer sk_live_…" \
"https://sportily.app/api/v1/status" { "ok": true, "lifecycle": "preview", "version": "v1", "keyPrefix": "sk_live_ab12cd34", "now": "2026-08-12T09:30:41Z", "today": "2026-08-12", "todayPublishesAt": "2026-08-12T07:00:00Z", "todayPublished": true}Fields
Every field a race and a runner can carry, in every endpoint that serves them. A ? on the type means the field can be null.
Race
raceId string Stable Sportily race id. Use it to key your own records.course string Racecourse name.offTime string Scheduled off time, ISO 8601.raceName string Published race title.fieldSize integer Number of declared runners.Runner
horseId string Stable horse id. With raceId, the key for your own records and for /lenses/matches.runnerNumber integer Saddlecloth number.horseName string Horse name.jockeyName string? Jockey.trainerName string? Trainer.modelWinPct number? Win probability as a percentage, 0–100. Null when the runner is not scored.modelRank integer? Rank within the field by model win probability, 1 = most favoured.modelGapNext number? Model win-probability gap to the next-ranked runner. A separation measure.speedFigure number? Sportily speed figure for the runner's recent form.edgePct number? Model probability minus market-implied probability, in points.impliedProbPct number? Market-implied probability as a percentage, 0–100.Errors
One envelope for every error: { "error": { "code", "message" } }.
unauthorized Missing or unknown API key.forbidden Key is valid but the account is not enabled for the API, or access was switched off.rate_limited Per-key minute or daily allowance exceeded. See Retry-After.not_yet_published The card is declared but scores for the requested date have not reached their publish time yet.out_of_window The requested date is outside the rolling 365-day window.not_found No races for the requested date.invalid_request Malformed parameter, e.g. a date that is not YYYY-MM-DD.internal_error Something went wrong on our side. Safe to retry.Rate limits
Usage is metered per key against a per-minute and a daily allowance. Exceed one and the API
responds 429 rate_limited with a Retry-After header. Race Lens reads
are metered on their own, separate allowance — exhausting it never blocks the rest of the API
— and one call reading several lenses costs much less than separate calls.
Every metered response reports what is left on the lane it spent from, so you never have to
interleave /usage calls: x-quota-remaining-minute and x-quota-remaining-day. GET /usage reports both allowances in full and
is free.
Licence
Own use only. No redistribution, resale, or onward sharing of Sportily data or any derived value. Access is per-account and non-transferable.