- You want pagination handled, because the cursor is opaque and easy to get wrong
- You want errors as exceptions with a stable code rather than status-code branching
- You would rather not maintain your own wrapper as endpoints are added
Labs / SDKs
JavaScript, Python, PHP and Go. All 63 endpoints in each, and zero dependencies in any of them. A racing API is not worth a dependency tree.
Anyone who would otherwise spend an afternoon writing the same HTTP wrapper everybody writes. You do not need it — the API is plain REST with a header for the key, and curl works fine. These exist to save you the boring part.
Same shape in all four. The method names are identical apart from the casing each language expects, so a snippet in one translates directly.
npm install @apihorseracing/sdk
import { HorseRacingAPI } from "@apihorseracing/sdk";
const api = new HorseRacingAPI({ apiKey: process.env.AHR_KEY });
const race = await api.races("rc_1JCCEF7", { include: "report" });
console.log(race.data.report.headline);
Node 18+, or any current browser
pip install apihorseracing
from apihorseracing import HorseRacingAPI
api = HorseRacingAPI(api_key="ahr_...")
race = api.races("rc_1JCCEF7", include="report")
print(race["data"]["report"]["headline"])
Python 3.9+
composer require apihorseracing/sdk
$api = new \ApiHorseRacing\Client(getenv('AHR_KEY'));
$race = $api->races('rc_1JCCEF7', ['include' => 'report']);
echo $race['data']['report']['headline'];
PHP 8.1+, runs on shared hosting
go get github.com/apihorseracing/apihorseracing-go
api := apihorseracing.New(os.Getenv("AHR_KEY"))
race, err := api.Races("rc_1JCCEF7", map[string]string{
"include": "report",
})
Go 1.21+
One key, one header, every endpoint. What changes between plans is which dates you can read, not which methods exist.
No card. It reads the same production database every paid plan reads, held back to between seven days and twenty-four hours old. It does not expire.
Read it from an environment variable. In browser code a key is visible to anyone who looks — proxy through your own server instead.
AHR_KEY=ahr_…
Every response carries your plan and window in meta, so the client can
check at startup rather than discovering it through an empty result.
api.metaCoverage()
Requesting today's racing on a free key returns outside_window with the
exact range your key reads. An empty array would look like missing data, and this is not
missing data — it is a plan boundary, and the difference matters when you are
debugging at two in the morning.
Every response, from every endpoint, has the same two keys.
{ "meta": { "request_id": "9aa33686fe2cc46a", "data_as_of": "2026-09-14T12:10:46+00:00", "plan": "complete", "window": "2017 → upcoming", "next_cursor": "c_8fA2…" }, "data": { /* the answer */ } }
63 endpoints return 63 shapes. Hand-written
types for each would drift from the API within a month, so data is
unknown in TypeScript and json.RawMessage in Go. Narrow it
where you use it.
It is not an offset and arithmetic on it will not work. Each client has a helper
that walks it: pages() in JavaScript, Python and PHP,
NextCursor() in Go.
Switch on code, never on message. The code is part of
the contract; the message is written for a person and may be reworded. The
request_id is what support needs.
Every method in every client is produced from the API's own endpoint registry — the same source the routes, the documentation and the OpenAPI spec come from.
Sixty-three endpoints across four languages is 252 methods. Hand-written, that is a fourth copy of the API surface, and the fourth copy is always the one that is wrong. A method exists in these libraries because the endpoint exists, not because somebody remembered to add it.
Found a bug in a client? Open an issue on the repo. For the API itself, support reaches a person.