Subscribe an HTTPS endpoint to signed event pushes. Events that fire on their own: `race.odds_open` (a race's first bookmaker prices landed), `race.result` (a race's result went final, from 2026-10-13) and `arb.opportunity` (a cross-book arb opened). `odds.change` is the event POST /v1/webhooks/{id}/test sends and nothing else does. An unknown event name is a 422 that lists the events that fire, and nothing is created; `market.suspend`, withdrawn on 2026-10-13 because it never fired, is refused the same way. Each delivery is a POST of the JSON payload with `X-PuntersEdge-Event` naming the event and `X-Webhook-Signature: sha256=<hex>`, the HMAC-SHA256 of the raw body keyed with the `secret` in this response. The secret is returned once, here (POST /v1/webhooks/{id}/rotate-secret issues a new one when the delivery queue is enabled). Delivery is at-least-once: the same event can arrive more than once, so deduplicate on `event_id` (in the body, and sent as the `X-PuntersEdge-Id` and `Idempotency-Key` headers). There is no ordering guarantee between events. Each delivery carries `X-PuntersEdge-Timestamp`, `X-PuntersEdge-Attempt` and two signatures: `X-Webhook-Signature: sha256=<hex>` (HMAC-SHA256 of the raw body with your secret) and `X-PuntersEdge-Signature: t=<unix seconds>,v1=<hex>` (HMAC-SHA256 of `<t>.<raw body>`; refuse a `t` more than 5 minutes from now to block replays). Any 2xx answer is success. A 5xx, 408, 429, timeout or connection failure is retried after 1 min, 5 min, 30 min, 2 h, 6 h (6 tries in all, about 8.5 hours), honouring `Retry-After` up to the next step; any other answer is final. After 10 consecutive undeliverable events, or a day of continuous failure, the subscription is paused and the owner is emailed; POST /v1/webhooks/{id}/resume restarts it with the same secret, and POST /v1/webhooks/{id}/replay re-sends what was missed. The delivery log returns the newest 50 rows unless `limit` says otherwise.
X-API-Key
curl -X POST 'https://api.puntersedge.online/v1/webhooks' \
-H 'X-API-Key: YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{"url": "https://example.com/hooks/puntersedge", "events": ["arb.opportunity"], "min_edge_pct": 1.5, "sports": ["nrl", "afl"]}'
| Name | In | Type | Required | Description |
|---|---|---|---|---|
url |
body | string | yes | HTTPS endpoint that receives the POST, on port 443 or 8443. Plain http://, a host that does not resolve, and a host that resolves to a private, loopback, link-local or other non-public address are rejected with 422. At most 5 subscriptions per API key (409 beyond that). |
events |
body | array | no | Events to subscribe to. Three events have live producers: `arb.opportunity` (a cross-book arb opened), `race.odds_open` and `race.result`. `race.odds_open`: the first bookmaker prices for a race just landed — fires once per race, so you can fetch the card on push instead of schedule-polling. Since the early-market lanes of 2026-09-01 it fires HOURS ahead, not minutes: measured over the 48h to 2026-09-03 11:00 UTC, the first price landed a median 10.1h before the jump on AU thoroughbreds, 5.3h on greyhounds, 6.8h on harness - typically overnight or early morning for the day's card, from one or two early books; the full board fills inside about an hour of the jump. `race.result` (from 2026-10-13): a race's result just went `final` — fires once per race, carrying the race (`race_id`, `result_id`, venue, race number, start, country, category), the placings ({position, name, number, horse_ref}), the `dividends` where the settling source declares them, and `source`. `race_id` is the id race.odds_open and /v1/racing/next-to-go carried for the race, and is null on the rare result we could not link to a priced race (10 of 2,038 final results in the 7 days to 2026-10-12); `result_id` is always set. A result goes final once its placings have been stable for 30 minutes, and the event follows within about five minutes of that: measured over the 3 days to 2026-10-12, results went final a median 36 minutes after the advertised start on AU and NZ races (p90 39), 38 on British and Irish thoroughbreds, and about 4 on British and Irish greyhounds, whose source publishes the result already final. It is the same row /v1/racing/results serves, so settle on it exactly as you would on that endpoint. `odds.change` is delivered solely by POST /v1/webhooks/{id}/test. `market.suspend` was withdrawn on 2026-10-13 — it never had a producer — and is now refused with 422. |
min_edge_pct |
body | number | no | Only deliver arb.opportunity when the theoretical edge is at least this percentage. Omit for every opportunity. |
sports |
body | array | no | Only deliver events for these sports. Sport keys as returned by GET /v1/sports; racing events (arbs, race.odds_open and race.result) arrive as racing_horse, racing_greyhound or racing_harness. Omit for all sports. |
Status codes: 200, 401, 402, 403, 409, 422, 429, 500. Response bodies are JSON; the full schema is in /openapi.json.
200 application/json
Field names and types are as the API returns them; values are a real sample, trimmed to a few items.
{
"id": "wh_7c1d2e9a4b3f",
"url": "https://example.com/hooks/puntersedge",
"events": [
"arb.opportunity",
"race.odds_open",
"race.result"
],
"min_edge_pct": 1.5,
"sports": [
"racing_horse",
"racing_greyhound",
"nrl"
],
"is_active": true,
"created_at": "2026-09-07T00:52:10",
"last_delivery_at": null,
"failure_count": 0,
"secret": "whsec_EXAMPLE_ONLY_shown_once_at_creation",
"delivery_entitled": true,
"delivery_paused_reason": null
}
GET /v1/webhooks — List WebhooksDELETE /v1/webhooks/{webhook_id} — Delete WebhookGET /v1/webhooks/{webhook_id}/deliveries — List Webhook DeliveriesPOST /v1/webhooks/{webhook_id}/replay — Replay WebhookPOST /v1/webhooks/{webhook_id}/resume — Resume WebhookPOST /v1/webhooks/{webhook_id}/rotate-secret — Rotate Webhook SecretPOST /v1/webhooks/{webhook_id}/test — Test WebhookThe free tier needs no credit card, and the sandbox endpoints need no key at all.
Get a free API key Quickstart