Subscribe to HMAC-signed events (source-quality breaches, edge-detection triggers, odds-movement alerts) and route them to Slack, Discord, or a custom endpoint. The full webhook spec is at /v1/meta/webhooks; this page walks you through end-to-end setup.
| Event type | Trigger |
|---|---|
source_quality.breach | A book's freshness goes from ok → breach or stale (see /v1/meta/per-book-sla thresholds). |
source_quality.recovery | A previously-breached book returns to ok. |
arbitrage.opened | A new cross-book ML arb with edge ≥ threshold opens. |
arbitrage.closed | A previously-flagged arb closes (price moved). |
ev.flagged | A +EV play above edge threshold appears. |
line_move.steam | Coordinated multi-book move within seconds (steam alert). |
parser.degraded | One of our ingest parsers reports degraded performance. |
In Slack: Apps → Manage → Build → Your Apps → Create New App (or use an existing one). Enable Incoming Webhooks in the app's features. Click Add New Webhook to Workspace and pick the channel where alerts should land. Slack returns a URL of the form https://hooks.slack.com/services/T.../B.../....
curl -X POST 'https://parlay-api.com/v1/webhooks' \
-H "X-API-Key: $PARLAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination_url": "https://hooks.slack.com/services/T.../B.../...",
"events": ["arbitrage.opened", "source_quality.breach"],
"format": "slack",
"filters": {
"sport_keys": ["baseball_mlb", "basketball_nba"],
"min_edge_pct": 1.5
}
}'
The format: "slack" hint tells us to send a Slack-compatible message shape (text + optional attachments). The webhook subscription is returned with an id + signing_secret; save the secret.
curl -X POST 'https://parlay-api.com/v1/webhooks/{id}/test' \
-H "X-API-Key: $PARLAY_API_KEY"
You should see a test message land in your Slack channel within a few seconds. If not, check the response body for delivery errors (e.g. Slack rejected the URL).
In Discord: Channel settings → Integrations → Webhooks → New Webhook. Copy the URL of the form https://discord.com/api/webhooks/.../....
curl -X POST 'https://parlay-api.com/v1/webhooks' \
-H "X-API-Key: $PARLAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination_url": "https://discord.com/api/webhooks/.../...",
"events": ["ev.flagged"],
"format": "discord",
"filters": { "min_edge_pct": 2.0 }
}'
Discord format uses the embeds payload shape. Looks clean in-channel with embedded link previews.
If you're routing to your own server (PagerDuty, OpsGenie, internal Slack proxy, custom dashboard), use format: "json" (default) and verify the X-Parlay-Signature header on receipt.
timestamp.body; the secret is returned at subscription creation and is rotated on demand via POST /v1/webhooks/{id}/rotate-secret.
Headers on every webhook delivery:
X-Parlay-Signature: hex-encoded HMAC-SHA256 of "{timestamp}.{body}"X-Parlay-Timestamp: Unix seconds when we sent itX-Parlay-Event: event type (e.g. arbitrage.opened)X-Parlay-Delivery-Id: unique per delivery; safe to dedupe onimport hmac, hashlib, os, time
SIGNING_SECRET = os.environ["PARLAY_WEBHOOK_SECRET"]
def verify(headers, raw_body: bytes) -> bool:
sig = headers.get("X-Parlay-Signature", "")
ts = headers.get("X-Parlay-Timestamp", "")
if not sig or not ts: return False
# Reject requests older than 5 minutes (replay protection)
if abs(int(time.time()) - int(ts)) > 300: return False
msg = f"{ts}.".encode() + raw_body
expected = hmac.new(SIGNING_SECRET.encode(), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(sig, expected)
const crypto = require("crypto");
const SECRET = process.env.PARLAY_WEBHOOK_SECRET;
function verify(headers, rawBody) {
const sig = headers["x-parlay-signature"] || "";
const ts = headers["x-parlay-timestamp"] || "";
if (!sig || !ts) return false;
if (Math.abs(Math.floor(Date.now()/1000) - parseInt(ts, 10)) > 300) return false;
const msg = Buffer.concat([Buffer.from(`${ts}.`), rawBody]);
const expected = crypto.createHmac("sha256", SECRET).update(msg).digest("hex");
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"os"
"strconv"
"time"
)
var secret = []byte(os.Getenv("PARLAY_WEBHOOK_SECRET"))
func verify(sig, ts string, body []byte) bool {
if sig == "" || ts == "" { return false }
tsInt, err := strconv.ParseInt(ts, 10, 64)
if err != nil { return false }
if t := time.Now().Unix(); t - tsInt > 300 || tsInt - t > 300 { return false }
msg := append([]byte(ts + "."), body...)
mac := hmac.New(sha256.New, secret)
mac.Write(msg)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(sig), []byte(expected))
}
| Action | Endpoint |
|---|---|
| List subscriptions | GET /v1/webhooks |
| Create subscription | POST /v1/webhooks |
| Get one | GET /v1/webhooks/{id} |
| Update events / filters | PATCH /v1/webhooks/{id} |
| Delete | DELETE /v1/webhooks/{id} |
| Send a test event | POST /v1/webhooks/{id}/test |
| Rotate the signing secret | POST /v1/webhooks/{id}/rotate-secret |
| Replay a missed delivery | POST /v1/webhooks/{id}/replay?delivery_id=... |
X-Parlay-Delivery-Id. Same id means same event; safe to ignore.PATCH.For the canonical case of "I want my source-quality breaches to land in Slack with formatting we control," we ship a tested relay at examples/source_quality_alert_relay.py. ~100 LOC, single file, runs as a tiny FastAPI server you point your webhook subscription at.
/docs/best-practices (full HMAC spec) · /v1/meta/webhooks (machine-readable webhook spec) · /cookbook source-quality monitor (cron-based alternative)