Skip to content
Developer docsREST · JSON and PNG · /v1

Code examples

Copy, set env vars, run.

Every example reads the key from RACINGAPI_KEY on your server. Never embed keys in browser code or commit them.

JavaScript examples run on Node.js 18+: save them as .mjs files to enable imports and top-level await. Python examples use requests (python -m pip install requests); PHP examples require the cURL extension. Replace example IDs with IDs from a racecard in the same daily feed.

Today's UK and Irish racecards

curl --fail-with-body "https://racingapi.net/v1/racecards?region=uk" \
  -H "Authorization: Bearer $RACINGAPI_KEY"

Dated UK and Irish results

This example uses a sampled day in the combined feed. Replace the date with the day you need; results only include races with published finishers. Archive completeness is not guaranteed.

curl --fail-with-body "https://racingapi.net/v1/results?region=uk&date=2026-10-26" \
  -H "Authorization: Bearer $RACINGAPI_KEY"

One race by ID

Set RACE_ID to an ID from a racecard on the same day. For a dated or tomorrow racecard, carry that filter through to the detail request; these basic examples target today's feed.

curl --fail-with-body --get "https://racingapi.net/v1/races/$RACE_ID" \
  --data-urlencode "region=uk" \
  -H "Authorization: Bearer $RACINGAPI_KEY"

Horse form summary statistics

Set HORSE_ID and RACE_ID from the same racecard. The raceId query is optional in the API, but these examples include it to select the correct declaration. Keep the same date or tomorrow=true if discovery used one.

curl --fail-with-body --get "https://racingapi.net/v1/horses/$HORSE_ID/form" \
  --data-urlencode "region=uk" \
  --data-urlencode "raceId=$RACE_ID" \
  -H "Authorization: Bearer $RACINGAPI_KEY"

Group every data.form row by section and display name, runs, wins, places and winPercentage as supplied strings. Empty strings mean not supplied, not zero; an empty form array means no summary statistics were supplied. The response identifies the chosen race, course and feed date and lists matching declarations. These are summary statistics, not individual past-race records.

Download a silk

Binary PNG, never parsed as JSON. Take HORSE_ID and RACE_ID from the same racecard; add the matching date or tomorrow=true for non-current cards. A 404 means the runner or source image was not found; nothing is substituted.

curl --fail --get "https://racingapi.net/v1/silks/$HORSE_ID" \
  --data-urlencode "region=uk" \
  --data-urlencode "raceId=$RACE_ID" \
  -H "Authorization: Bearer $RACINGAPI_KEY" \
  --output silk.png

Racecourse weather

Read a course ID from racecards or racecourses, then use the same feed date. Weather is available on every REST plan. It is a local forecast, not an on-track observation. Preserve meta.source credit and links when rendering it.

curl --fail-with-body "https://racingapi.net/v1/racecourses/COURSE_ID/weather?region=uk" \
  -H "Authorization: Bearer $RACINGAPI_KEY"

Read data.forecast[]: temperatureC is °C, windSpeedMps is m/s, and precipitationMm covers the next precipitationPeriodHours after time. Times are UTC; values may be null when not supplied. Cache until meta.expiresAt. See weather field semantics before matching forecasts to races.

Paginate a list

const headers = { Authorization: `Bearer ${process.env.RACINGAPI_KEY}` };
const all = [];
for (let page = 1; ; page++) {
  const url = new URL("https://racingapi.net/v1/racecourses");
  url.searchParams.set("region", "uk");
  url.searchParams.set("page", String(page));
  url.searchParams.set("limit", "100");
  const res = await fetch(url, { headers });
  if (!res.ok) throw new Error(`RacingAPI ${res.status}`);
  const { meta, data } = await res.json();
  all.push(...data);
  if (page * meta.limit >= meta.total) break;
  await new Promise((r) => setTimeout(r, 600)); // stay under the per-second limit
}

Errors and retries

These JSON-only helpers make at most three attempts, with a 15-second timeout per request. They retry 429 and a temporary source-unavailable response, but stop rather than sleeping for more than 30 seconds between attempts. This matters when Retry-After points to next month's quota reset. Transport, timeout and JSON-parsing failures are surfaced to your application rather than retried automatically. An inactive endpoint (endpoint_unavailable) requires activation, not immediate retries. Never repeatedly retry 400, 401, 403, 404 or 422 without correcting their cause. These helpers do not replace account-wide request scheduling.

const key = process.env.RACINGAPI_KEY;
if (!key) throw new Error("Set RACINGAPI_KEY on your server");
const base = process.env.API_BASE_URL || "https://racingapi.net";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function racingGet(path, params = {}) {
  const url = new URL(path, base);
  for (const [name, value] of Object.entries(params)) {
    url.searchParams.set(name, String(value));
  }
  for (let attempt = 0; attempt < 3; attempt++) {
    const res = await fetch(url, {
      headers: { "X-API-Key": key },
      signal: AbortSignal.timeout(15000),
    });
    const body = await res.json(); // JSON endpoints only; not silk PNGs
    if (res.ok) return body;

    const retryAfter = Number(res.headers.get("Retry-After") || 0);
    const transientSource = res.status === 503 &&
      ["provider_unavailable", "image_source_unavailable"].includes(body.code);
    const retryable = res.status === 429 || transientSource;
    const delay = Math.max(
      Number.isFinite(retryAfter) ? retryAfter * 1000 : 0,
      1000 * (2 ** attempt),
    );
    if (!retryable || attempt === 2 || delay > 30000) {
      throw Object.assign(new Error(body.error || `HTTP ${res.status}`), {
        status: res.status, code: body.code,
        requestId: body.requestId, retryAfter,
      });
    }
    await sleep(delay + Math.random() * 200);
  }
}

const { meta, data } = await racingGet("/v1/racecards", {
  region: "uk", limit: "10",
});
console.log(meta, data);