Skip to main content

Webhooks

Webhooks deliver entity-change events from Race Platform to your URL with an HMAC signature so receivers can verify authenticity. Manage them in the Webhooks tab of Admin → Integrations (/admin/integrations).

The full delivery payload reference (event names, envelope shape, HMAC verification examples in TypeScript and Python) lives at Reference → Webhook Events.

Source:

  • API route: apps/api/src/routes/webhooks.ts
  • Dispatcher: apps/api/src/services/webhook_dispatcher.ts
  • Helper: apps/api/src/lib/webhook_helpers.ts (the fireWebhook(c, entityType, opType, payload) wrapper that entity routes call after a successful mutation)

Authoring a subscription

Click + New webhook in the Webhooks tab. Fill in:

FieldNotes
NameHuman-readable label shown in the admin grid
URLWhere the API POSTs deliveries — any HTTPS endpoint that can verify HMAC
SecretThe HMAC key. The UI generates a 48-char hex secret by default; you can paste your own if you have a secret manager
EnabledToggle without deleting (deliveries pause; existing history stays)
Filters → Entity typesWhitelist of entity types (run_sheet, board_card, …). Empty list = all entities
Filters → Op typesWhitelist of create / update / delete. Empty list = all ops

When you save, the subscription is enabled immediately and starts matching the next mutation that satisfies its filters.

HMAC signing

Every delivery carries an X-Race-Signature header of the form:

X-Race-Signature: sha256=<hex>

…where <hex> is HMAC-SHA256(secret, rawBody). The dispatcher also adds:

HeaderValue
X-Race-Event<entityType>.<opType> — e.g. run_sheet.create
X-Race-DeliveryUUID of the delivery row in the database
User-Agentrace-webhooks/1.0
Content-Typeapplication/json

Verify the signature with constant-time comparison — see Reference → Webhook Events for worked examples in TypeScript and Python.

Retry policy

Each delivery attempts up to 3 times with exponential backoff:

AttemptDelay before
1(immediate)
2300 ms
3600 ms

Each attempt has an 8 second timeout. After three failed attempts the delivery row carries succeededAt = null and the parent webhook row's lastDeliveryStatus reflects the last HTTP status code seen.

There's no durable queue in dev — fireWebhook is fire-and-forget on the same Node event loop. The delivery row is persisted before the first POST so a node restart mid-delivery leaves a recoverable row that you can manually replay (see below). In production on Cloudflare Workers + Queues, the same dispatcher is wrapped in a Queue consumer so retries survive deployments and worker restarts (see Architecture → Plugin host for the runtime split between Node dev and Workers prod).

Successful deliveries (any 2xx) stamp succeededAt = now() and break out of the retry loop.

Entity-type filters

Filters are inclusive whitelists with empty-means-everything semantics:

  • filters.entityTypes: [] — fire for any entity
  • filters.entityTypes: ["run_sheet"] — fire only on run-sheet events
  • filters.entityTypes: ["run_sheet", "board_card"] — fire on either

Same shape for filters.opTypes. The two lists are conjunctive: a delivery only fires if both the entity-type and op-type filters match.

The admin UI's chip pickers offer a fixed catalogue (run_sheet, board_card, lap, setup, session, event, issue, tyre, driver, car) but the schema accepts any string — entity-route authors can introduce new entityType strings without a migration.

What entities fire today

Webhook dispatch is wired into entity routes via the fireWebhook(c, entityType, opType, payload) helper. As of Phase 3 only two call sites are live:

TriggerEntity typeOp
POST /run-sheets succeedsrun_sheetcreate
PATCH /board-cards/:id succeedsboard_cardupdate

The helper pattern is established — adding runSheets PATCH/DELETE, boardCards POST/DELETE, and the rest of the entity routes is a one-liner per route. Subscribe today and you'll receive the two events above; expect more entity-op coverage in subsequent releases without any subscriber-side changes.

The webhook.test.create event is a synthetic event fired by the Test Delivery button (next section).

Test delivery

POST /webhooks/:id/test ships a synthetic event:

{
"message": "Race webhooks delivery test",
"testedByUserId": "<auth.userId>",
"_meta": {
"entityType": "webhook.test",
"opType": "create",
"accountId": "<accountId>",
"occurredAt": "<iso8601>"
}
}

The delivery is always fired against the targeted webhook — even when its filters.entityTypes would normally exclude webhook.test. This means the Test button never silently no-ops. Use it to confirm:

  1. Your URL is reachable from the API container.
  2. Your TLS / DNS / firewall is happy.
  3. Your HMAC verification accepts the signature.

The admin UI shows the resulting responseStatus / responseBody inline so you can debug subscriber bugs without crawling the deliveries log.

Delivery history + replay

The Webhooks tab's drill-down pane lists recent deliveries newest-first:

  • HTTP status code (red ≥400, green 2xx, grey null)
  • entityType.opType label
  • Attempt count + timestamp
  • Pretty-printed payload + truncated response body on expand
  • Replay button (refresh icon) — see below

Behind the scenes it calls GET /webhooks/:id/deliveries?limit=200. The cap is 500 rows per request.

Replay re-fires a stored payload via a fresh delivery row — the original history stays intact, so you can confirm a fix without losing the trace of the original failure. Endpoint: POST /webhooks/:id/deliveries/:deliveryId/replay.

Pause / disable

Switching the Enabled toggle off in the admin UI sets enabled: false on the webhook row. The dispatcher filters out disabled webhooks at lookup time, so no new deliveries are created while the toggle is off. Existing in-flight deliveries finish their retry loop. Re-enabling resumes new dispatch without losing any history.

What's coming

  • Durable queue in dev — today fireWebhook is fire-and-forget. A startup sweep that resends any webhook_deliveries rows with attempts < 3 and no succeededAt would close the loss window on Node restarts.
  • Per-account dispatch concurrency limits — protect the API if a subscriber URL is slow.
  • Subscription health summaries — last 50 success rate, p50 / p95 latency, surfaced on the admin grid.
  • Inbound webhook receivers — accepting webhooks from external systems (timing partners, paddock services) into the ingestion pipeline.