Webhook setup

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.

What you can subscribe to

Event typeTrigger
source_quality.breachA book's freshness goes from ok → breach or stale (see /v1/meta/per-book-sla thresholds).
source_quality.recoveryA previously-breached book returns to ok.
arbitrage.openedA new cross-book ML arb with edge ≥ threshold opens.
arbitrage.closedA previously-flagged arb closes (price moved).
ev.flaggedA +EV play above edge threshold appears.
line_move.steamCoordinated multi-book move within seconds (steam alert).
parser.degradedOne of our ingest parsers reports degraded performance.

Setup for Slack

1 Create an Incoming Webhook in Slack

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.../....

2 Subscribe via the ParlayAPI webhooks endpoint

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.

3 Test it

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).

Setup for Discord

1 Create a Discord webhook

In Discord: Channel settings → Integrations → Webhooks → New Webhook. Copy the URL of the form https://discord.com/api/webhooks/.../....

2 Subscribe with format="discord"

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.

Setup for custom endpoints (with signature verification)

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.

Always verify signatures. Without verification, anyone who knows your webhook URL can spoof events. Our signing scheme is HMAC-SHA256 over timestamp.body; the secret is returned at subscription creation and is rotated on demand via POST /v1/webhooks/{id}/rotate-secret.

1 Receive the event

Headers on every webhook delivery:

2 Verify the signature in your language

import 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))
}

Managing subscriptions

ActionEndpoint
List subscriptionsGET /v1/webhooks
Create subscriptionPOST /v1/webhooks
Get oneGET /v1/webhooks/{id}
Update events / filtersPATCH /v1/webhooks/{id}
DeleteDELETE /v1/webhooks/{id}
Send a test eventPOST /v1/webhooks/{id}/test
Rotate the signing secretPOST /v1/webhooks/{id}/rotate-secret
Replay a missed deliveryPOST /v1/webhooks/{id}/replay?delivery_id=...

Delivery semantics

Reference relay implementation

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.

Related

/docs/best-practices (full HMAC spec) · /v1/meta/webhooks (machine-readable webhook spec) · /cookbook source-quality monitor (cron-based alternative)