What this is
Webhooks are a Premium-tier-only push channel. Instead of polling/v1/arb/live or holding a WebSocket connection open, you give us an
HTTPS URL and we POST the same opportunity payload to you the moment
the engine detects it — typically within ~100 ms of the underlying
snapshot arriving.
Use this if you’d rather not run a persistent process to receive
signals. A serverless function (Cloudflare Worker, Vercel function,
Lambda) wakes up only when a real signal fires.
Registering a webhook
- Open
/dashboard/webhooks. - Click Create webhook.
- Fill in:
- URL —
https://your-domain.com/webhook(must be HTTPS) - Label — anything memorable, e.g.
prod-bot - Tier filter —
logical,endgame, or both - Ticker filter — subset of
BTC,ETH,SOL(omit for all)
- URL —
- The success callout shows the signing secret ONCE
(starts with
whsec_…). Save it. You can’t retrieve it later — if you lose it, delete the webhook and recreate.
What each POST looks like
Header reference
| Header | Value | Why |
|---|---|---|
X-PQL-Signature | HMAC-SHA256 hex digest of the raw body, keyed by your secret | The only proof the request is from us |
X-PQL-Tier | logical or endgame | Lets you route different tiers to different handlers without parsing the body |
X-PQL-Ticker | BTC / ETH / SOL | Same — fast path for per-ticker routing |
Content-Type | application/json | Always JSON |
User-Agent | PolyQuantLab-Webhook/1 | Identifies our delivery worker |
Body fields
| Field | Type | Description |
|---|---|---|
type | string | Always "opportunity" for actionable signals |
tier | string | "logical" (math-guaranteed, yes+no < $1) or "endgame" (model-based, within τ of resolution) |
ticker | string | BTC, ETH, or SOL |
event_type | string | 5m, 15m, 1h, 4h, or daily_up_down |
market_id | string | Polymarket condition id |
polymarket_slug | string | The slug part of the Polymarket URL |
direction | string | BUY_YES, BUY_NO, or BUY_BOTH (logical only) |
fill_price | float | Best ask you’d actually pay walking the book |
fill_spread | float | Best ask − best bid at the moment of detection |
expected_pnl_per_share | float | Model EV after Polymarket taker fee, per share |
est_fee_per_share | float | The fee we already subtracted (audit transparency) |
tau_minutes | float | Minutes until market resolution |
confidence | string | stable (book held for ≥30 s at this price) or stale |
model_yes_prob | float | Our model’s probability that the YES token resolves true |
detected_at | string | ISO timestamp the opportunity was first detected |
polymarket_url | string | Deep link to the market |
Verifying the signature
Always verify. Without the check, anyone with your URL could POST fake opportunities. The signature is a hex-encoded HMAC-SHA256 over the raw request body bytes (not the parsed JSON) using your secret as the key.Delivery semantics
- Timeout: 5 seconds. Reply 2xx within that or we treat the delivery as failed.
- Auto-pause: 10 consecutive failures → the webhook is paused. You’ll see it in
/dashboard/webhookswith a “Failing” badge; click Resume once your endpoint is back. - Idempotency: Each opportunity carries a unique
(market_id, detected_at)pair. We may re-fire the same opportunity if our worker restarts; deduplicate on that pair if your downstream isn’t idempotent. - At-least-once: Treat the delivery as at-least-once, not exactly-once. The Redis pub/sub channel + the retry loop means a single fire event can land twice in rare cases.
- No backfill on register: If signals fired before you created the webhook, you won’t get them later. Use
GET /v1/arb/liveto catch up on currently-actionable signals.
Quick local testing
The simplest test setup needs no servers:- Open webhook.site — you get a unique URL like
https://webhook.site/abc-…. - Paste that URL into the create-webhook form.
- Leave both tabs open. When an opportunity fires, you’ll see the POST appear in real time on webhook.site.
localhost:
Common mistakes
- Parsing the body before verifying the signature — most web frameworks parse JSON middleware-style. The signature is over the raw bytes, so middleware that re-serializes the body breaks verification. Capture raw bytes first, verify, then parse.
- HTTP, not HTTPS — we reject
http://URLs at registration time. Signatures don’t replace TLS; both matter. - Replying slowly — if your handler does heavy work synchronously, we may timeout before you ack. Acknowledge immediately, do the work in the background.
- No tier check downstream — we deliver to webhooks registered when the user was on Premium even if they later downgrade (we re-check tier at register time only). If you build a multi-tenant system on top, gate at your own layer too.
Pricing
Webhook alerts are included with the Premium plan. Free / Pro / Plus users can register webhooks via the dashboard but registration returns403. See pricing.