A homepage or an app card list
One call gives you the whole day. Cache it for a few minutes rather than per race; declarations change, but not by the second.
Documentation / Racecards & meetings
/v1/racecards/today
Every meeting and race today, grouped by course.
Send your key in the X-API-Key header, or as
?key= if your tool cannot set one. A date outside your plan's window returns
403 outside_window naming the window, never an empty array.
None. This endpoint takes nothing beyond your key.
curl https://api.apihorseracing.com/v1/racecards/today \ -H "X-API-Key: $AHR_KEY"
$ch = curl_init('https://api.apihorseracing.com/v1/racecards/today'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['X-API-Key: ' . getenv('AHR_KEY')], ]); $data = json_decode(curl_exec($ch), true)['data'];
const res = await fetch( 'https://api.apihorseracing.com/v1/racecards/today', { headers: { 'X-API-Key': process.env.AHR_KEY } } ); const { meta, data } = await res.json();
import os, requests r = requests.get( "https://api.apihorseracing.com/v1/racecards/today", headers={"X-API-Key": os.environ["AHR_KEY"]}, ).json()
curl https://api.apihorseracing.com/v1/racecards/today?key=$AHR_KEY
$url = 'https://api.apihorseracing.com/v1/racecards/today' . '?key=$AHR_KEY'; $data = json_decode(file_get_contents($url), true)['data'];
const res = await fetch( 'https://api.apihorseracing.com/v1/racecards/today?key=$AHR_KEY' ); const { meta, data } = await res.json();
import os, requests r = requests.get( "https://api.apihorseracing.com/v1/racecards/today?key=$AHR_KEY", ).json()
{ "meta": { "request_id": "574b1a1ca58eadd7", "data_as_of": "2026-09-12T05:10:27+00:00", "plan": "analyst", "window": "2017 → upcoming", "date": "2026-09-13", "meetings": 0, "count": 0 }, "data": [] }
A real response from /v1/racecards/today,
captured by calling the live API rather than written by hand.
| Code | Status | When |
|---|---|---|
| unauthorized | 401 | No key, or one we do not recognise. Check the header name and that the key has not been rotated from your account page. |
| outside_window | 403 | You asked for a date your plan cannot see. The body names the window and the date you asked for. Search endpoints clamp instead of refusing, and report what they actually searched in meta. |
| upgrade_required | 403 | The endpoint returns computed statistics and your plan does not include them. The body names every plan that does reach it, so you never have to look it up. |
| rate_limited | 429 | Past your per-minute rate. Retry-After is set, and X-RateLimit-Remaining tells you where you stand on every successful response too. |
| server_error | 500 | Ours, not yours. Every response carries a request_id; quote it and we can find the exact request. |
A real request from your browser straight to
api.apihorseracing.com with
your own key. Nothing is proxied and nothing is logged by this page.
Remembered in this browser so you do not paste it on every page. Forget it
Every meeting and race carded today, grouped by course, in the order they are due off. This is the first call most consumers make and the one they make most often.
A meeting is one course on one day, and races arrive nested inside it because that is how a card is read. Nobody wants a flat list of forty races across nine tracks to group themselves.
The times are local to the course, deliberately. A 14:35 at Musselburgh and a 14:35 at Meydan are both the time on the racecourse clock, which is what a card shows and what your users expect. Everything else in the API is UTC.
One call gives you the whole day. Cache it for a few minutes rather than per race; declarations change, but not by the second.
Take the race identifiers from here, then follow individual races as they run. Polling this endpoint repeatedly to detect a result is the wrong shape; a webhook on result.received costs nothing against your quota.
Use /v1/racecards/upcoming for tomorrow onwards. This endpoint is today only, and returns an empty list rather than yesterday if nothing is carded.
Needs a plan with a live window. A free key reads a delayed window and gets outside_window here, which is honest: it is the one endpoint a free key genuinely cannot serve. While the backfill is still running this may be empty even on a paid plan; /v1/meta/coverage tells you how far the archive has reached.