APIHorseRacing

Labs / SDKs

Four client libraries, one API.

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.

View on GitHub MIT licensed · free key works with all four

Who this is for

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.

Worth using if
  • 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
Skip it if
  • You are calling two endpoints and already have an HTTP client you like
  • You are in a language none of these cover — use the OpenAPI spec and generate one
  • You want an assistant to use the data, in which case the MCP server is the better route

Install and call

Same shape in all four. The method names are identical apart from the casing each language expects, so a snippet in one translates directly.

JavaScript TypeScript types included
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

Python Standard library only
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+

PHP cURL, with a stream fallback
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 net/http, nothing else
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+

How keys work

One key, one header, every endpoint. What changes between plans is which dates you can read, not which methods exist.

01 Create one free

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.

Get a free key

02 Keep it out of your source

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_…

03 Ask what it reaches

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()

A date outside your window is an error, not an empty list.

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.

What comes back

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 */ }
}
Payloads are deliberately untyped

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.

The cursor is opaque

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.

Errors carry a stable code

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.

Generated, not written

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.