Webhooks

Signed event notifications for fills, resting executions, cancellations, and settlements — so bots don't have to poll.

Subscribe a URL and PredArena POSTs you a signed JSON event when things happen to your account. Webhooks matter most for the asynchronous events you cannot see in an API response: a resting limit order filling, a stop triggering, a market settling overnight.

Event types

EventFires whenPayload highlights
order.executedAny immediate execution (market or marketable limit)order/trade ids, avg price, fees, cash delta, realized P&L
resting_order.filledA resting limit fills as maker, or a stop/take-profit triggers and executeschild_order_id, filled/remaining counts, terminal flag, liquidity role
resting_order.cancelledA resting order is cancelled or expires (user, expiry, market close, OCO sibling)terminal_reason, released reservations
trade.settledMarkets resolve — one event per account per resolution sweeptrades[] array: ticker, outcome, exit price, P&L per settled trade

Managing subscriptions

# Create (max 5 per account). The secret is returned ONCE.
curl -X POST https://predarena.org/api/v2/webhooks \
  -H "Authorization: Bearer $PREDARENA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://bot.example.com/predarena-hook",
    "events": ["resting_order.filled", "trade.settled"]
  }'

# List / disable / re-enable / delete
curl https://predarena.org/api/v2/webhooks -H "Authorization: Bearer $PREDARENA_API_KEY"
curl -X PATCH https://predarena.org/api/v2/webhooks/SUB_ID -d '{"active": true}' \
  -H "Authorization: Bearer $PREDARENA_API_KEY" -H "Content-Type: application/json"
curl -X DELETE https://predarena.org/api/v2/webhooks/SUB_ID -H "Authorization: Bearer $PREDARENA_API_KEY"

Verifying signatures

Every delivery carries X-PredArena-Signature: t=<unix seconds>,v1=<hex> where v1 is HMAC-SHA256 of "<t>.<raw body>" with your subscription secret. Reject timestamps older than ~5 minutes and compare in constant time:

verify.py
import hashlib, hmac, time

def verify(secret: str, body: bytes, header: str, tolerance=300) -> bool:
    parts = dict(kv.split("=", 1) for kv in header.split(","))
    t = int(parts["t"])
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Delivery semantics (honest version)

Best-effort, at-most-once.

Each event is attempted once per subscription with a 5-second timeout and no automatic retry; 25 consecutive failures auto-disable the subscription (re-enable with PATCH). Webhooks are a latency optimization, not a source of truth — reconcile against GET /api/v2/portfolio/orders on startup and on any gap.

Respond 2xx quickly (enqueue, then process). Event id fields are unique — use them to de-duplicate if you ever see a replay.