Documentation / Guides
Webhooks
Four events, signed deliveries, and no quota cost.
Instead of asking every thirty seconds whether a race has settled, be told when it does. Deliveries arrive sooner than a poll would find them and cost nothing against your quota.
The arithmetic is stark. A process polling every thirty seconds is about ninety thousand requests a month. A webhook is none, and it still tells you first.
The four events
| Event | Fires when |
|---|---|
result.received | A race settles. The one nearly everybody wants. |
race.abandoned | A race is called off. The event that stops a scheduler waiting forever for a result that will never come. |
meeting.abandoned | A whole card is lost. |
racecard.published | Declarations land for a future day. |
Subscribing
POST /v1/webhooks/subscribe
X-API-Key: ahr_...
Content-Type: application/json
{
"url": "https://yours.example/hooks/racing",
"events": ["result.received", "race.abandoned"],
"region": "GB"
}
Set region unless you want twelve jurisdictions. Most consumers
care about one or two, and filtering at our end is free while filtering at yours is not.
The response returns a signing secret once and never again. Store it before you close the tab. Rotating means subscribing afresh.
Verifying a delivery
Every delivery carries X-AHR-Signature: an HMAC-SHA256 of the raw request body
using your secret.
$raw = file_get_contents('php://input');
$mine = hash_hmac('sha256', $raw, getenv('AHR_WEBHOOK_SECRET'));
if (!hash_equals($mine, $_SERVER['HTTP_X_AHR_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
Two details that matter. Sign the raw body, not a re-encoded version of the
parsed JSON, because key order will differ. And compare with hash_equals rather than
===, so the comparison is constant time.
Delivery is at-least-once
Not exactly-once. Your handler must tolerate seeing the same race twice. Key on the race identifier and it will.
A delivery that does not return a 2xx is retried with a widening gap for about a day, then given up on. Return 200 as soon as you have the payload and do the work afterwards; a handler that does five seconds of processing before answering will time out and be retried, and you will process the same race repeatedly.
When it goes wrong
GET /v1/webhooks/deliveries?status=failed shows what we sent, when, how many
attempts, what your endpoint returned and the first part of your own response body. A five
hundred with your stack trace in it is usually the fastest answer to why it failed.
We do not replay. After an outage, take the race identifiers from the failed deliveries and fetch them directly. Deliveries are kept for thirty days.
Requirements
- https only. We will not post to plain http; the payload identifies your account.
- Answer quickly. Under a couple of seconds. Queue the work.
- Be idempotent. See above.
- The subscription belongs to the account, not the key, so rotating your key does not stop deliveries.