Skip to main content

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

  1. Open /dashboard/webhooks.
  2. Click Create webhook.
  3. Fill in:
    • URLhttps://your-domain.com/webhook (must be HTTPS)
    • Label — anything memorable, e.g. prod-bot
    • Tier filterlogical, endgame, or both
    • Ticker filter — subset of BTC, ETH, SOL (omit for all)
  4. 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

POST /webhook HTTP/1.1
Host: your-domain.com
Content-Type: application/json
X-PQL-Signature: 8b3c9d2e1a45f8c7d12a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5
X-PQL-Tier: logical
X-PQL-Ticker: BTC
User-Agent: PolyQuantLab-Webhook/1

{
  "type": "opportunity",
  "tier": "logical",
  "ticker": "BTC",
  "event_type": "5m",
  "market_id": "0x...",
  "polymarket_slug": "btc-up-or-down-at-2-15pm-jun-08",
  "direction": "BUY_NO",
  "fill_price": 0.61,
  "fill_spread": 0.02,
  "expected_pnl_per_share": 0.047,
  "est_fee_per_share": 0.017,
  "tau_minutes": 2.3,
  "confidence": "stable",
  "model_yes_prob": 0.42,
  "detected_at": "2026-06-08T07:42:11.205Z",
  "polymarket_url": "https://polymarket.com/event/..."
}

Header reference

HeaderValueWhy
X-PQL-SignatureHMAC-SHA256 hex digest of the raw body, keyed by your secretThe only proof the request is from us
X-PQL-Tierlogical or endgameLets you route different tiers to different handlers without parsing the body
X-PQL-TickerBTC / ETH / SOLSame — fast path for per-ticker routing
Content-Typeapplication/jsonAlways JSON
User-AgentPolyQuantLab-Webhook/1Identifies our delivery worker

Body fields

FieldTypeDescription
typestringAlways "opportunity" for actionable signals
tierstring"logical" (math-guaranteed, yes+no < $1) or "endgame" (model-based, within τ of resolution)
tickerstringBTC, ETH, or SOL
event_typestring5m, 15m, 1h, 4h, or daily_up_down
market_idstringPolymarket condition id
polymarket_slugstringThe slug part of the Polymarket URL
directionstringBUY_YES, BUY_NO, or BUY_BOTH (logical only)
fill_pricefloatBest ask you’d actually pay walking the book
fill_spreadfloatBest ask − best bid at the moment of detection
expected_pnl_per_sharefloatModel EV after Polymarket taker fee, per share
est_fee_per_sharefloatThe fee we already subtracted (audit transparency)
tau_minutesfloatMinutes until market resolution
confidencestringstable (book held for ≥30 s at this price) or stale
model_yes_probfloatOur model’s probability that the YES token resolves true
detected_atstringISO timestamp the opportunity was first detected
polymarket_urlstringDeep 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.
import crypto from "crypto";
import express from "express";

const SECRET = process.env.PQL_WEBHOOK_SECRET; // whsec_...

const app = express();
app.post(
  "/webhook",
  // IMPORTANT: capture the raw body — express.json() parses it
  // and changes the bytes, breaking the signature check.
  express.raw({ type: "application/json" }),
  (req, res) => {
    const got = req.headers["x-pql-signature"];
    const expected = crypto
      .createHmac("sha256", SECRET)
      .update(req.body)
      .digest("hex");

    if (got !== expected) {
      return res.status(401).send("bad signature");
    }

    const opp = JSON.parse(req.body);
    console.log(opp.ticker, opp.direction, opp.fill_price);

    // Reply 2xx within 5 s, or we retry.
    res.send("ok");
  }
);

app.listen(3000);

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/webhooks with 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/live to catch up on currently-actionable signals.

Quick local testing

The simplest test setup needs no servers:
  1. Open webhook.site — you get a unique URL like https://webhook.site/abc-….
  2. Paste that URL into the create-webhook form.
  3. Leave both tabs open. When an opportunity fires, you’ll see the POST appear in real time on webhook.site.
For development against your own code, use ngrok to expose localhost:
ngrok http 3000
# Use the https://*.ngrok.io URL it prints as your webhook target

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 returns 403. See pricing.