Webhooks
HMAC-signed event deliveries to your HTTPS endpoint — Stripe-style signatures, exponential retries.
Event catalog status
The webhook infrastructure (subscriptions, signing, retries, test
deliveries) is live. The production event emitters for trader.fill,
market.resolved, and signal.smart_money are coming soon — today you
can create subscriptions and exercise your receiver end-to-end with
polyrank.test events. Watch the changelog.
Manage subscriptions
Session-cookie auth; webhook quota is plan-gated (Free = 0 events →
403 plan_required, see Billing).
| Endpoint | What it does |
|---|---|
GET /v1/webhooks | List subscriptions |
POST /v1/webhooks | Create — https:// URLs only (SSRF-guarded) |
PUT /v1/webhooks/{id} | Update URL / event types / active flag |
DELETE /v1/webhooks/{id} | Remove |
POST /v1/webhooks/{id}/test | Send a synthetic polyrank.test event now |
Creating a subscription returns the signing secret (whsec_…) once —
store it like a password.
Verify signatures
Every delivery is signed Stripe-style:
X-Polyrank-Signature: t=1765391234,v1=hex(hmac_sha256(secret, "{t}.{raw_body}"))
X-Polyrank-Event-Id: 01JXYZ…
X-Polyrank-Event-Type: polyrank.test
User-Agent: Polyrank-Webhook/1.0import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(header: string, rawBody: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
// Reject stale timestamps (replay defense)
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}import hashlib, hmac, time
def verify(header: str, raw_body: bytes, secret: str) -> bool:
parts = dict(kv.split("=", 1) for kv in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(
secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Always verify over the raw request body, before any JSON parsing.
Delivery & retries
- 10-second POST timeout; any 2xx counts as delivered.
- Failures retry with backoff: 5s → 30s → 5m → 1h → 6h → 24h (6 attempts max).
- Deduplicate by
X-Polyrank-Event-Id— retries reuse the id. - Deliveries count against your plan's monthly webhook-event quota.
Event types
| Type | Status | Payload |
|---|---|---|
polyrank.test | live | Synthetic event from the /test endpoint |
trader.fill | coming soon | A followed wallet's fill |
market.resolved | coming soon | Market resolution |
signal.smart_money | coming soon | Smart-money signal event |