Bonus Wallet Update Webhook
Every lifecycle transition in Wynta converges on a single webhook contract pushed to the PAM. The PAM — the external client system — maintains the player Bonus Wallet and updates it based on these webhooks: all credit-side changes (grant, release, expiry, forfeit) are applied to the client wallet exclusively from webhook events. The exceptions are BONUS_CONSUMED / BONUS_CONSUMPTION_REVERTED, which confirm debits/credits the PAM already applied locally (see Bonus Consumption) — the PAM matches these by consume_txn_id instead of re-applying the delta.
Status: integration contract proposal. Field vocabulary aligns with the Wynta API (chip_type, consume_txn_id, chunk fields); the envelope shape below is pending sign-off between the Wynta and PAM (SR) teams.
Event types
event_type
Emitted when
pending_delta
available_delta
PAM applies delta?
BONUS_GRANTED
Grant trigger fires, chunks created
+amount
0
✔ apply
BONUS_RELEASED
Chunk threshold crossed by wagering
−chunk amount(s)
+chunk amount(s)
✔ apply
BONUS_CONSUMED
bonus_consume_bonus accepted
0
−amount
✖ confirm by consume_txn_id
BONUS_CONSUMPTION_REVERTED
bonus_revert_bonus_consumption
0
+amount
✖ confirm by consume_txn_id
BONUS_EXPIRED
Chunk or bonus expiry job
−pending remainder
−available remainder
✔ apply
BONUS_FORFEITED
Player/operator forfeit
−pending remainder
−available remainder
✔ apply
WAGERING_PROGRESS (optional)
Progress update for UX
0
0
✖ UX only
Payload schema
{
"event_id": "uuid", // idempotency key — PAM dedupes on this
"event_type": "BONUS_RELEASED",
"occurred_at": "2026-07-13T10:42:07Z",
"player_id": "player-123",
"site_id": 1,
"txn_id": 15, // grant record ID (transactions API)
"consume_txn_id": null, // set on CONSUMED / REVERTED events
"bonus_code": "WELCOME100",
"chip_type": "cash", // "cash" | "in_app_purchase"
"amount": "166.67", // decimal string, like the API
"wallet_effect": {
"pending_delta": "-166.67", // signed decimal strings
"available_delta": "166.67"
},
"chunks": [ // chunk-level detail; releases may batch
{
"chunk_ref": "CHUNK_002", // matches transaction-detail chunk_ref
"sequence": 2,
"chunk_count": 3,
"amount": "166.67",
"wager_amount": "600.0000", // chunk wagering progress at event time
"status": "RELEASE",
"expires_at": "2025-05-08T08:00:00" // chunk expiry (UTC) — lets the PAM show countdowns
}
],
"resulting_balance": { // Wynta's authoritative view — field names
"pending_bonus": "333.33", // match GET /{user_id}/summary
"bonus_balance": "166.67",
"wagering_required": "1000.00"
}
}Example event payloads
BONUS_GRANTED
BONUS_RELEASED
BONUS_EXPIRED
BONUS_FORFEITED
BONUS_CONSUMED
BONUS_CONSUMPTION_REVERTED
Delivery semantics
At-least-once delivery with exponential-backoff retries for ≥ 24 h; undeliverable events go to a dead-letter queue and are swept into the next reconciliation run.
Deduplication — the PAM applies each
event_idexactly once.Ordering — events may arrive out of order. The PAM applies signed deltas and compares against
resulting_balanceafter each apply; a mismatch flags the player for targeted reconciliation. If Wynta provides a per-player sequence number, ordered application is preferred.Batching — a single wager can release multiple chunks; the
chunks[]array then carries several entries andwallet_effectholds the summed deltas. The PAM must handle 1..N chunks per event.
Authentication
Every webhook request from Wynta to the PAM endpoint is authenticated with the following layers:
1. Identity header. Each request carries X-Client-Id identifying the Wynta site/tenant emitting the event (mirroring the header convention of the User Bonus API), so a multi-brand PAM can route and scope events.
2. HMAC-SHA256 signature (proposed). The User Bonus API authenticates with the X-Client-Id header alone, but the inbound webhook endpoint on the PAM is exposed to receive calls — an unsigned identifier is not sufficient there. We propose signing every webhook with a shared webhook secret provisioned at onboarding:
Verification on the PAM side:
Read the raw request body (before any JSON parsing/re-serialization).
Reject if
|now − X-Timestamp| > 300 s(replay window).Build
canonical = client_id + "\n" + timestamp + "\n" + raw_body, computeHMAC-SHA256(webhook_secret, canonical), and compare using a constant-time comparison.Only then parse the body and process the event.
3. Secret rotation. Secrets rotate without downtime: during rotation the PAM verifies against both current and next secret and accepts if either matches, then drops the old secret after cutover.
4. Transport controls. HTTPS/TLS 1.2+ only; Wynta publishes egress IP ranges for PAM-side allow-listing; mTLS available per environment where the PAM requires it.
Never apply a wallet update from an unverified request. A failed signature or expired timestamp must return 401 without processing — and must not be retried into the wallet via logs or queues.
PAM handler requirements
Authenticate: verify
X-Client-Id, timestamp window, and the canonical-string HMAC signature on the raw body (constant-time compare).Dedupe on
event_id.Apply
wallet_effectto the chip-type bucket pair — exceptBONUS_CONSUMED/BONUS_CONSUMPTION_REVERTED, which are matched to the existing local debit/credit byconsume_txn_id.Compare local balance to
resulting_balance; on drift, flag (do not auto-correct outside reconciliation).Store the receipt; acknowledge with 2xx quickly and do heavy processing async. Non-2xx or timeout triggers Wynta's retry.
Last updated
Was this helpful?