Webhooks
Register a URL on your server, tell us which events you care about. When those events fire, we POST a signed JSON payload to your URL. No polling, no SSE connection to maintain, just events when they happen.
Available on the Pro tier and above. 5 credits per delivered event.
Supported events
| event_type | Fires when |
|---|---|
arb_flagged | An arbitrage opportunity exists across two books for any market |
ev_alert | A bet shows positive expected value vs Pinnacle’s devigged line |
line_move | A book’s line moves more than a configurable threshold (default 0.5 points / 5 cents) |
live_arb | Same as arb_flagged but only during in-play windows |
Injury event coming soon. The underlying injury data feed is being wired up; until that ships, injury as an event_type returns 400. Subscribe to one of the four above in the meantime.
Register a webhook
POST /v1/webhooks
{
"url": "https://yourapp.com/parlay-webhook",
"events": ["arb_flagged", "ev_alert"],
"sport_filter": ["basketball_nba", "americanfootball_nfl"]
}
Body fields:
| field | type | description |
|---|---|---|
url | string (https URL) | Your endpoint that will receive the signed POST. |
events | array of strings | Subscribe to one or more of the supported event_types above. |
sport_filter | array of sport_keys (optional) | If set, only events for these sports fire. Omit for all sports. |
Response includes a secret. Save it. You’ll need it to verify signatures. We never show it again.
{
"id": 13,
"url": "https://yourapp.com/parlay-webhook",
"events": ["arb_flagged", "ev_alert"],
"sport_filter": ["basketball_nba", "americanfootball_nfl"],
"secret": "whsec_8f2a1b9c4d6e0f7a3b5c8d1e",
"is_active": true,
"created_at": "2026-05-09T05:00:00Z"
}
Payload shape
{
"id": "evt_9c3d8a1f",
"event_type": "arb_flagged",
"timestamp_ms": 1778214028111,
"data": {
"sport_key": "basketball_nba",
"match_id": "0042500222",
"home_team": "Lakers",
"away_team": "Thunder",
"market": "h2h",
"edge_pct": 0.014,
"legs": [
{"book": "draftkings", "side": "Lakers", "price": +145},
{"book": "fanduel", "side": "Thunder", "price": -130}
]
}
}
Signature verification
Every webhook request includes a header:
X-Parlay-Signature: t=1778214028,v1=8a3f2d1c4b6e9f0a8d2c5b3f1e9c7d6b4a8f0e2c5b3d8a1f9e7c4b6d2a8f3e1c
To verify (Python):
import hmac, hashlib, time
def verify_webhook(secret: str, raw_body: bytes, header: str, max_age=300) -> bool:
parts = dict(p.split('=', 1) for p in header.split(','))
t = int(parts['t'])
if abs(time.time() - t) > max_age:
return False
expected = hmac.new(
secret.encode(),
f'{t}.'.encode() + raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts['v1'])
JavaScript:
import crypto from 'crypto';
function verifyWebhook(secret, rawBody, header, maxAge = 300) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const t = parseInt(parts.t, 10);
if (Math.abs(Date.now() / 1000 - t) > maxAge) return false;
const expected = crypto.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1)
);
}
Retries and disabling
Failed delivery (any non-2xx response) is retried 3 times: immediate, +30s, +5min.
5 consecutive total failures auto-disables the webhook and emails the owner. You can re-enable from the dashboard once you’ve fixed the receiving end.
Test a webhook
POST /v1/webhooks/{webhook_id}/test
Sends a synthetic payload with "event_type": "test" so you can verify your endpoint and signature logic without waiting for real events.
List, update, delete
GET /v1/webhooks # list yours
GET /v1/webhooks/{id} # one
PATCH /v1/webhooks/{id} # update url, events, sport_filter, active
DELETE /v1/webhooks/{id} # remove
POST /v1/webhooks/{id}/test # send a real test payload to the URL; returns delivered + upstream status
Tier limits
| Tier | Max webhooks | Max events/min |
|---|---|---|
| Pro | 5 | 60 |
| Business | 25 | 300 |
| Enterprise | contact | contact |