Polyrankdocs

Webhooks

HMAC-signed event deliveries to your HTTPS endpoint — Stripe-style signatures, exponential retries.

Event catalog status

market.resolved and signal.smart_money are live, alongside the synthetic polyrank.test event. trader.fill is still coming soon (it is high-volume; use the realtime WebSocket for per-fill streaming today). Watch the changelog.

Manage subscriptions

Session-cookie auth; webhook quota is plan-gated (Free = 0 events → 403 plan_required, see Billing).

EndpointWhat it does
GET /v1/webhooksList subscriptions
POST /v1/webhooksCreate — https:// URLs only (SSRF-guarded)
PUT /v1/webhooks/{id}Update URL / event types / active flag
DELETE /v1/webhooks/{id}Remove
POST /v1/webhooks/{id}/testSend 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.0
import { 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

TypeStatusPayload
polyrank.testliveSynthetic event from the /test endpoint
market.resolvedliveA Polymarket market resolved on-chain
signal.smart_moneyliveA smart-money-tagged wallet made a large trade
trader.fillcoming soonA followed wallet's fill

Every delivery body is the same envelope:

{ "type": "<event type>", "emitted_at": "<ISO-8601>", "data": {  } }

market.resolved

Emitted within ~2 minutes of the on-chain ConditionResolution event, for conditions that are Polymarket exchange markets.

{
  "type": "market.resolved",
  "emitted_at": "2026-06-11T18:04:12.512Z",
  "data": {
    "condition_id": "0x5f65…",
    "question_id": "0x9c01…",
    "title": "Will X happen by June 30?",
    "slug": "will-x-happen-by-june-30",
    "category": "Politics",
    "market_kind": "binary",
    "winner_outcome": "YES",
    "winner_index": 0,
    "outcome_slot_count": 2,
    "resolved_time": "2026-06-11 18:02:55.000",
    "tx_hash": "0xab12…"
  }
}

winner_outcome is YES / NO for binary markets, may be INVALID, and is null with winner_index = -1 on ties/multi-outcome payouts. title and slug can briefly be empty for very young markets whose metadata hasn't synced yet.

signal.smart_money

Emitted in real time when a wallet carrying at least one positive smart-money tag (whale, sharp, early_bird, contrarian) takes a fill of $5,000+ notional.

{
  "type": "signal.smart_money",
  "emitted_at": "2026-06-11T18:04:12.512Z",
  "data": {
    "signal_type": "large_buy",
    "trader_proxy": "0x1f4a…",
    "smart_money_tags": ["whale", "sharp"],
    "condition_id": "0x5f65…",
    "title": "Will X happen by June 30?",
    "outcome_side": "YES",
    "price": 0.34,
    "size_usdc": 12500.0,
    "tx_hash": "0xab12…"
  }
}

signal_type is large_buy (taker bought the outcome token) or exit (taker sold).

Per-subscription filters

filter_json (set at POST /v1/webhooks or PUT /v1/webhooks/{id}) narrows which events a subscription receives. All keys are optional and ANDed; unknown keys are ignored.

KeyTypeMatches
condition_idstring or string[]data.condition_id (case-insensitive)
trader_proxystring or string[]data.trader_proxy
signal_typesstring[]data.signal_type is in the list
tags_anystring[]data.smart_money_tags intersects the list
min_size_usdcnumberdata.size_usdc is at least this

Example — only $25k+ buys from sharp wallets:

{ "min_size_usdc": 25000, "signal_types": ["large_buy"], "tags_any": ["sharp"] }

On this page