Webhooks
Subscribe to platform events. We POST a signed JSON payload to your URL on every match.
How it works
- You register a subscription: a URL, a signing secret, and a list of events.
- When a matching event happens (e.g. a player opens a lootbox), we enqueue a delivery in the same DB transaction as the business write — so we never lose events on success.
- A background worker picks up pending deliveries, signs them HMAC-SHA256, and POSTs to your URL.
- Your endpoint replies
2xxto acknowledge.4xx(non-retryable) marks failed;5xx/ network → retried with exponential backoff.
Subscription shape
| Field | Type | Notes |
|---|---|---|
url | string | HTTPS endpoint. We send a single POST with JSON body. |
signing_secret | string | Shared symmetric secret. Used for HMAC. |
events | string[] | Exact event types, prefix wildcards ("lootbox.*"), or global ("*"). |
status | enum | active | paused | failed |
Event catalog
| Event type | Fires when | Data fields |
|---|---|---|
lootbox.opened | A player opens a lootbox (any source) | lootbox_id, lootbox_name, user, open_id, reward, server_seed_hash, opened_at |
raffle.drawn | A raffle draw completes | raffle_id, winner_username, prize_winz, total_tickets, server_seed_hash |
jackpot.dropped | A jackpot pool is paid out | jackpot_id, winner_username, amount, pool_before, pool_after, trigger_reason |
mini_game.played | A mini-game outcome resolves | mini_game_id, user, outcome, server_seed_hash |
tournament.closed | A tournament closes + prizes are paid | tournament_id, payouts[], total_paid |
journey.completed | A player completes a lifecycle journey | journey_id, journey_name, user, reward_winz_granted |
bonus.granted | A bonus is issued to a player | bonus_id, template_name, type, amount, user |
bonus.converted | A bonus wagering target is hit and converts to real | bonus_id, user, real_credited |
kyc.decision | KYC document is approved or rejected | document_id, user, decision, reason |
player.status_changed | Lifecycle status changes | user, from, to, reason |
Payload shape
{
"id": "…delivery uuid…",
"type": "lootbox.opened",
"delivered_at": "2025-05-19T20:14:33Z",
"data": {
"lootbox_id": "…",
"user": "StarlordV7",
"reward": { "asset": "WINZ", "amount": 1500, "segment_label": "big" }
}
}
Signature verification
Each delivery carries two headers:
X-Wowsino-Signature: t=<unixSeconds>,v1=<hex>
X-Wowsino-Event: <event_type>
Compute HMAC-SHA256(signing_secret, "<t>.<rawBody>") and constant-time compare against v1. Reject deliveries where |now − t| > 300s (replay window).
Node.js verification
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, header, rawBody) {
const m = /^t=(\d+),v1=([0-9a-f]+)$/.exec(header ?? '');
if (!m) return false;
if (Math.abs(Date.now()/1000 - Number(m[1])) > 300) return false;
const expected = createHmac('sha256', secret)
.update(`${m[1]}.${rawBody}`)
.digest('hex');
const a = Buffer.from(m[2], 'hex');
const b = Buffer.from(expected, 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}
Retry policy
- 2xx → delivered; we mark
delivered_at, reset failure counter. - 4xx (except 408/429) → permanent failure; we mark
failed_at. We don't retry — your receiver said no. - 5xx / 408 / 429 / network → exponential backoff: 30s, 2m, 10m, 30m, 2h, 6h, 24h. After 8 attempts → marked failed.
A subscription that fails many times in a row may auto-pause; check failure_count on the subscription.
Same-tx enqueue: we write the delivery row inside the business transaction that produced the event. If your business write succeeded, the webhook will at least be enqueued.