For the complete documentation index, see llms.txt. This page is also available as Markdown.

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.

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_id exactly once.

  • Ordering — events may arrive out of order. The PAM applies signed deltas and compares against resulting_balance after 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 and wallet_effect holds 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:

  1. Read the raw request body (before any JSON parsing/re-serialization).

  2. Reject if |now − X-Timestamp| > 300 s (replay window).

  3. Build canonical = client_id + "\n" + timestamp + "\n" + raw_body, compute HMAC-SHA256(webhook_secret, canonical), and compare using a constant-time comparison.

  4. 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.

PAM handler requirements

  1. Authenticate: verify X-Client-Id, timestamp window, and the canonical-string HMAC signature on the raw body (constant-time compare).

  2. Dedupe on event_id.

  3. Apply wallet_effect to the chip-type bucket pair — except BONUS_CONSUMED / BONUS_CONSUMPTION_REVERTED, which are matched to the existing local debit/credit by consume_txn_id.

  4. Compare local balance to resulting_balance; on drift, flag (do not auto-correct outside reconciliation).

  5. 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?