SDK

Node.js SDK

The official racehooks npm package is a TypeScript-first SDK for the RaceHooks API. It handles OAuth token management (including one automatic retry when a token expires mid-request) and HMAC signature verification, so you can focus on building. The SDK works across all sports supported by RaceHooks; no changes are needed when IndyCar or NASCAR feeds go live.

Node.js ≥ 18TypeScript 5.xESM + CJSZero runtime deps
GitHub repository →npm package →

Installation

bash
npm install racehooks
# or
yarn add racehooks
# or
pnpm add racehooks

Quick start

index.ts
import { RaceHooks } from "racehooks";

const rh = new RaceHooks({
  clientId:     process.env.RACEHOOKS_CLIENT_ID!,
  clientSecret: process.env.RACEHOOKS_CLIENT_SECRET!,
});

// Subscribe to all race events
const { webhook, webhookSecret } = await rh.webhooks.create({
  feedId:     "events.race",
  webhookUrl: "https://yourserver.com/f1-hook",
});

// webhookSecret signs deliveries; retrievable anytime via
// rh.webhooks.getSecret(webhook.webhookId) (and rotatable)
console.log("Webhook id:", webhook.webhookId);
console.log("Signing secret:", webhookSecret);

Verify incoming deliveries

server.ts
import { verifySignatureBoolean } from "racehooks";
import express from "express";

const app = express();
app.use("/f1-hook", express.raw({ type: "application/json" }));

app.post("/f1-hook", (req, res) => {
  const sig = req.headers["x-racehooks-signature"];

  // Returns true/false. verifySignature() is the throwing variant — it raises a
  // WebhookSignatureError on a missing or bad signature instead of returning false.
  if (!verifySignatureBoolean(req.body, sig, process.env.WEBHOOK_SECRET!)) {
    return res.status(401).send("Bad signature");
  }

  const payload = JSON.parse(req.body.toString());
  console.log("Feed:", payload.feed, payload.sessionId);
  res.sendStatus(200);
});

Verify and parse in one step

server.ts
import express from "express";
import { webhookHandler, safeConstructEvent } from "racehooks";

const app = express();

// webhookHandler verifies the signature against the raw body, parses the event,
// calls your function, and replies 200 — or 400 on a bad signature, 500 if your
// handler throws.
app.post(
  "/f1-hook",
  express.raw({ type: "application/json" }),
  webhookHandler(process.env.WEBHOOK_SECRET!, async (event) => {
    if (event.feed === "events.race") {
      // ...
    }
  }),
);

// Or verify + parse yourself without throwing:
app.post("/f1-hook-manual", express.raw({ type: "application/json" }), (req, res) => {
  const result = safeConstructEvent(
    req.body,
    req.headers["x-racehooks-signature"],
    process.env.WEBHOOK_SECRET!,
  );
  if (!result.valid) return res.status(400).send(result.error.message);
  console.log(result.event.feed);
  res.sendStatus(200);
});

// constructEvent(body, signature, secret) is the throwing variant. The same three
// helpers exist on the client (rh.webhookHandler / rh.constructEvent /
// rh.safeConstructEvent) when you pass `secret` to new RaceHooks({ ... }).

RaceHooks constructor

FieldTypeDescription
clientIdstringYour RaceHooks OAuth client ID.
clientSecretstringYour RaceHooks OAuth client secret.
baseUrlstring?Override the API base URL. Defaults to https://api.racehooks.io.
timeoutMsnumber?Request timeout in milliseconds. Defaults to 15,000.
secretstring?Webhook signing secret. Enables rh.constructEvent, rh.safeConstructEvent, and rh.webhookHandler.
toleranceSecondsnumber?Reject deliveries older than this many seconds. 0 / omitted disables it. In the published SDK (0.4.0) it reads X-RaceHooks-Sent-At, which the body-only X-RaceHooks-Signature does not cover. It is a staleness hint, not replay protection; dedupe on X-RaceHooks-Delivery-Id. For replay protection today, verify X-RaceHooks-Signature-V1 and check its signed t (see Webhooks → HMAC signatures); the next SDK release verifies that header and applies toleranceSeconds to the signed t.
ℹ
The SDK auto-refreshes your OAuth Bearer token 60 seconds before expiry and deduplicates concurrent token requests, so you need no caching or token management on your side.

rh.webhooks

typescript
// Create with filters (create() takes a single feedId; for feedIds /
// subscriptions on one endpoint, call POST /v1/webhooks directly)
const { webhook, webhookSecret } = await rh.webhooks.create({
  feedId:     "timing.data",
  webhookUrl: "https://your-app.com/hook",
  filters: {
    constructors: ["ferrari"],
    positions:    { min: 1, max: 5 },
  },
});

const id = webhook.webhookId; // e.g. "V1StGXR8_Z5jdHi6B-myT"

// Pause (and later resume with { active: true })
await rh.webhooks.update(id, { active: false });

// Send a test delivery — single attempt, 5s timeout, no retries
const { delivered, statusCode } = await rh.webhooks.test(id);

// Fetch or rotate the signing secret
const secret = await rh.webhooks.getSecret(id);
const { webhookSecret: newSecret } = await rh.webhooks.rotateSecret(id);

// Delivery logs (paginated)
const { data: logs, total } = await rh.webhooks.logs(id, { limit: 50 });

// Delete
await rh.webhooks.delete(id);

rh.usage

typescript
// Current subscription tier and limits
const sub = await rh.usage.subscription();
console.log(sub.tier, sub.limits.maxWebhooks);

// This month's delivery count
const today = await rh.usage.current();
console.log(today.deliveryCount, today.bucketRemaining);

// Breakdown by feed
const { byFeed } = await rh.usage.byFeed();
byFeed.forEach(f => console.log(f.feedId, f.deliveryCount));

// Monthly history (one entry per YYYY-MM period, up to the last 90 periods)
const history = await rh.usage.history();

rh.simulate

Replays use a prepare-then-stream flow: prepare the session on the main API, then start playback on the simulate host. The SDK's older rh.simulate.start / get / list / cancel methods called the retired replay engine, and those endpoints now return 404. Call the replay endpoints directly as shown below. A replay delivers only the feeds your plan includes; the curated demo race 2026-canada_r is the exception and delivers the full analytics feed set on any plan.

typescript
// Replays run in two steps: prepare the session on the main API, then play it
// into your registered webhooks from the simulate host.
const TOKEN = process.env.RACEHOOKS_TOKEN!; // Bearer token from POST /v1/oauth
const sessionId = "2026-canada_r";          // the curated demo race — always warm

// 1. Prepare (main API, Bearer header). Cold sessions generate on first prepare —
//    poll GET /v1/simulate/status?session=<id> until status is "ready".
await fetch("https://api.racehooks.io/v1/simulate/prepare", {
  method: "POST",
  headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify({ sessionId }),
});

// 2. Deliver to your active webhooks. speed is clamped to 0.1–120 (default 10).
//    The simulate host takes the token as a query param, not a header.
const qs = new URLSearchParams({ session: sessionId, speed: "10", sinks: "webhook", token: TOKEN });
const res = await fetch(`https://simulate.racehooks.io/v1/simulate/stream?${qs}`);
console.log(res.status, await res.json());

rh.fantasy

Fantasy scoring for a race weekend: estimated points per driver, a live in-session running score, and a pit-lane leaderboard. These endpoints require a paid plan (Developer tier or higher). See the Fantasy guide for the scoring model.

typescript
// Estimated fantasy points for a completed race (Grand Prix by default)
const gp = await rh.fantasy.getRaceScores("2026-italy-r16");
gp.scores.forEach(s => console.log(s.tla, s.breakdown.total));

// Score the Sprint instead (compressed points table)
const sprint = await rh.fantasy.getRaceScores("2026-italy-r16", { session: "sprint" });
console.log(sprint.sessionType); // "sprint"

// Running, provisional score while a session is live
const live = await rh.fantasy.getSessionScores("2026-italy_r");
if (live.live) {
  console.log(`Lap ${live.currentLap}/${live.totalLaps}`);
  live.scores.forEach(s => console.log(s.tla, s.breakdown.total)); // provisional
}

// Pit-stop leaderboard for a session (fastest → slowest stationary time)
const pit = await rh.fantasy.getSessionPitTimes("2026-italy_r");
// The API returns tla + pitStopTimeSec on each stop (seconds stationary in the box).
const fastest = pit.fastestStop as { tla?: string; pitStopTimeSec?: number } | null;
console.log("Fastest stop:", fastest?.tla, fastest?.pitStopTimeSec);
✦
Building a full fantasy game? The companion racehooks-f1-fantasy-scorer package (GitHub · npm) turns events.race webhooks into a live leaderboard under official-style, DFS-style, or custom rules. Walkthrough on the Fantasy guide.

Analytics payloads

On Custom plans, model output is delivered as its own feeds (analytics.strategy, analytics.race-odds, analytics.gap-projection, …), each with the standard { feed, sessionId, utc, ... } envelope plus raceId and lap. Subscribe an endpoint to the ones you need and route on feed. Field reference: Analytics feeds.

typescript
import express from "express";
import { webhookHandler } from "racehooks";

const app = express();

// On Custom plans, model output arrives as its own analytics.* feeds — subscribe
// an endpoint to e.g. analytics.strategy and route on event.feed.
app.post(
  "/hook",
  express.raw({ type: "application/json" }),
  webhookHandler(process.env.WEBHOOK_SECRET!, async (event) => {
    if (event.feed !== "analytics.strategy") return;

    const p = event as unknown as {
      lap: number;
      raceState?: { pSafetyCar?: number };
      drivers: Array<{
        tla: string;
        pit?: { pNextLap: number };
        pace?: { tyre?: { health?: number; cliffLapPredicted?: number } };
      }>;
    };

    for (const d of p.drivers) {
      console.log(p.lap, d.tla, {
        pitNextLap: d.pit?.pNextLap,                    // 0–1, per-lap, green-flag stops only
        tyreHealth: d.pace?.tyre?.health,               // 0–1
        cliffLap:   d.pace?.tyre?.cliffLapPredicted,
      });
    }
    console.log("P(SC next lap):", p.raceState?.pSafetyCar);
  }),
);

Each analytics feed maps to a model page that explains the numbers: for example pit stop probability and safety car probability both arrive on analytics.strategy.

CommonJS

The package ships with both ESM (dist/index.js) and CommonJS (dist/index.cjs) builds. Node.js resolves the correct format automatically based on your project's package.json type field.

javascript
// CommonJS
const { RaceHooks, verifySignatureBoolean } = require("racehooks");

const rh = new RaceHooks({
  clientId:     process.env.RACEHOOKS_CLIENT_ID,
  clientSecret: process.env.RACEHOOKS_CLIENT_SECRET,
});

Error handling

The SDK throws typed errors you can catch and handle:

typescript
import { RaceHooks, RaceHooksAuthError, RaceHooksAPIError } from "racehooks";

try {
  await rh.webhooks.create({ feedId: "timing.data", webhookUrl: "..." });
} catch (err) {
  if (err instanceof RaceHooksAuthError) {
    // OAuth token request failed — check credentials
    console.error("Auth failed:", err.message);
  } else if (err instanceof RaceHooksAPIError) {
    // API returned an error response
    console.error(`API error ${err.statusCode}:`, err.body);
  }
}
← Race EventsMCP Server →