> For the complete documentation index, see [llms.txt](https://docs.wynta.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.wynta.com/bonus/api-reference/player-and-s2s.md).

# User Bonus API

Base path: `/api/v1/user-bonuses`. Endpoints marked 🔐 require the [`X-Client-Id` header](/bonus/api-reference/overview.md#authentication-client-id-header) (resolved server-side to a `site_id`); the others need no auth header.

## 1. List applicable bonus codes

Returns all active bonus codes available to a user on a given site, ordered by `display_order` ascending — used to render a bonus selection UI.

Codes flagged `system_auto_apply` are **never returned here** — those are applied automatically by the backend when a `BONUS_RELEASE` event fires and are not meant for manual selection.

```
GET /api/v1/user-bonuses/applicable-codes
```

| Query parameter | Type   | Required | Default   | Description                                                                                                                                     |
| --------------- | ------ | -------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `user_id`       | string | Yes      | —         | Platform user identifier (max 50 chars)                                                                                                         |
| `chip_type`     | string | Yes      | —         | `cash` or `in_app_purchase`                                                                                                                     |
| `display_on`    | string | No       | `DEPOSIT` | Filter by display flow (e.g. `DEPOSIT`, `REGISTRATION`). A code matches if this value appears anywhere in its comma-separated `display_on` list |

**Response `200 OK`**

```json
[
  {
    "promo_id": 12,
    "code": "WELCOME100",
    "max_amount": "500.00",
    "valid_from": "2025-01-01T00:00:00",
    "valid_to": "2025-12-31T23:59:59",
    "display_title": "Welcome Bonus",
    "display_description": "Get 100% on your first deposit up to ₹500",
    "terms_url": "https://example.com/terms/welcome",
    "banner_image_url": "https://cdn.example.com/banners/welcome.jpg",
    "badge_text": "Hot",
    "cta_text": "Claim Now",
    "auto_apply": false,
    "display_order": 1,
    "display_on": "DEPOSIT",
    "min_display_amount": "100.00",
    "wager_multiplier": "3.00",
    "no_of_chunks": 3,
    "applicability_frequency": "ONCE"
  }
]
```

Key response fields: `promo_id` (code record ID), presentation fields (`display_title`, `display_description`, `terms_url`, `banner_image_url`, `badge_text`, `cta_text`, `display_order`, `display_on`), `auto_apply` (if `true`, apply without user selection), `min_display_amount` (minimum transaction amount to show this bonus), plus the bonus mechanics the client can surface: `wager_multiplier`, `no_of_chunks`, `applicability_frequency` (e.g. `ONCE`, `DAILY`), `max_amount`, validity window (UTC).

## 2. Validate a bonus code

Validates whether a promo code is applicable for a user **without consuming it**. Use this to give the user confirmation before they commit to a code (e.g. on a deposit screen). No auth header required.

```
POST /api/v1/user-bonuses/validate-code
```

**Request body** `application/json`

```json
{
  "user_id": "USER_001",
  "chip_type": "cash",
  "code": "WELCOME100"
}
```

**Response `200 OK`**

```json
{
  "valid": true,
  "code": "WELCOME100",
  "reason": null,
  "promo_id": 12,
  "display_title": "Welcome Bonus",
  "wager_multiplier": "3.00",
  "no_of_chunks": 3
}
```

| Field              | Type            | Description                                                      |
| ------------------ | --------------- | ---------------------------------------------------------------- |
| `valid`            | boolean         | `true` if the code is applicable for this user and chip type     |
| `code`             | string          | Echo of the submitted promo code                                 |
| `reason`           | string \| null  | Human-readable reason when `valid` is `false`; `null` when valid |
| `promo_id`         | integer \| null | Bonus configure promo ID; `null` when invalid                    |
| `display_title`    | string \| null  | Marketing title; `null` when invalid                             |
| `wager_multiplier` | decimal \| null | Wagering requirement multiplier; `null` when invalid             |
| `no_of_chunks`     | integer \| null | Number of release chunks; `null` when invalid                    |

{% hint style="warning" %}
**Validation never grants.** There is no code-application endpoint — the grant itself is created internally when the configured **release trigger event** fires (deposit, registration, etc.). Show `reason` to the user when `valid` is `false`.
{% endhint %}

## 3. Consume a bonus chunk 🔐

Records that bonus was consumed against a wager event. Call this from the game server / PAM **immediately after** debiting the client wallet for a wager that draws from the user's bonus. The service **auto-selects the user's next available released chunk** — the client never picks chunks.

```
POST /api/v1/user-bonuses/consume
```

**Request body** `application/json`

```json
{
  "user_id": "USER_001",
  "consume_txn_id": "TXN20250519001",
  "wager_amount": "300.00",
  "bonus_amount": "100.00",
  "chip_type": "cash",
  "wager_tnx_id": "WAGER_REF_001",
  "game_id": "GAME_ROULETTE_01",
  "round_id": "ROUND_999"
}
```

| Field            | Type           | Required | Description                                                       |
| ---------------- | -------------- | -------- | ----------------------------------------------------------------- |
| `user_id`        | string         | Yes      | Platform user identifier (max 50 chars)                           |
| `consume_txn_id` | string         | Yes      | **Idempotency key** — unique per consumption event (max 50 chars) |
| `wager_amount`   | decimal string | Yes      | Total wager amount placed (≥ 0)                                   |
| `bonus_amount`   | decimal string | Yes      | Bonus amount being consumed (≥ 0)                                 |
| `chip_type`      | string         | Yes      | `cash` or `in_app_purchase`                                       |
| `wager_tnx_id`   | string         | Yes      | Reference ID of the wager transaction (max 50 chars)              |
| `game_id`        | string \| null | No       | Game identifier (max 50 chars)                                    |
| `round_id`       | string \| null | No       | Game round identifier (max 50 chars)                              |

**Response `201 Created`**

```json
{
  "txn_id": 88,
  "consume_txn_id": "TXN20250519001",
  "bonus_amount": "100.00",
  "consumed_amount": "100.00",
  "chip_type": "cash"
}
```

{% hint style="info" %}
**Idempotency:** a retry with the same `consume_txn_id` returns **`409 Conflict`**. Store `consume_txn_id` values on your side before calling so you can safely detect duplicates — treat a `409` for a known ID as "already recorded", not as an error to surface. `409` is also returned when **no released chunk is available** for the user — in that case the client wallet has drifted and the local debit must be reversed.
{% endhint %}

## 4. Revert a bonus consumption

Cancels a previously recorded consumption — e.g., when a wager is voided or rolled back. Decrements the user's consumed balance by the original amount. No auth header required — the `consume_txn_id` path parameter scopes the operation.

```
POST /api/v1/user-bonuses/consume/{consume_txn_id}/revert
```

**Response `200 OK`**

```json
{
  "txn_id": 88,
  "consume_txn_id": "TXN20250519001",
  "amount": "100.00",
  "chip_type": "cash"
}
```

Reverting an already-reverted consumption returns `409 Conflict`.

## 5. Get user bonus summary 🔐

Bonus figures per chip type — Wynta's authoritative view, used for wallet drift checks and to power the wagering progress bar.

```
GET /api/v1/user-bonuses/{user_id}/summary
```

**Response `200 OK`**

```json
[
  {
    "chip_type": "cash",
    "bonus_balance": "600.00",
    "pending_bonus": "400.00",
    "wagering_required": "1200.00"
  },
  {
    "chip_type": "in_app_purchase",
    "bonus_balance": "200.00",
    "pending_bonus": "100.00",
    "wagering_required": "300.00"
  }
]
```

| Field               | Description                                                     |
| ------------------- | --------------------------------------------------------------- |
| `bonus_balance`     | **Available** bonus (total released minus total consumed)       |
| `pending_bonus`     | Total bonus amount in chunks not yet released                   |
| `wagering_required` | Remaining wager amount needed to release **all** pending chunks |

## 6. List user transactions 🔐

Paginated flat ledger of bonus events, most recent first. **Every event — grant, chunk release, consumption, expiry, or forfeit — is its own entry.**

```
GET /api/v1/user-bonuses/{user_id}/transactions?chip_type=cash&limit=50&offset=0
```

| Query parameter | Type    | Required | Default | Description                 |
| --------------- | ------- | -------- | ------- | --------------------------- |
| `chip_type`     | string  | Yes      | —       | `cash` or `in_app_purchase` |
| `limit`         | integer | No       | `50`    | Results per page (1–200)    |
| `offset`        | integer | No       | `0`     | Pagination offset (≥ 0)     |

**Response `200 OK`**

```json
[
  { "txn_id": 15, "bonus_code": "WELCOME100", "amount": "500.00", "type": "grant",     "created_at": "2025-05-19T08:00:00" },
  { "txn_id": 20, "bonus_code": null,         "amount": "166.67", "type": "released",  "created_at": "2025-05-19T09:00:00" },
  { "txn_id": 21, "bonus_code": null,         "amount": "166.67", "type": "consumed",  "created_at": "2025-05-19T10:15:00" },
  { "txn_id": 22, "bonus_code": null,         "amount": "166.67", "type": "expiry",    "created_at": "2025-05-20T08:00:00" },
  { "txn_id": 23, "bonus_code": null,         "amount": "166.67", "type": "forfeited", "created_at": "2025-05-21T11:00:00" }
]
```

| `type`      | Meaning                                                                |
| ----------- | ---------------------------------------------------------------------- |
| `grant`     | Bonus was granted to the user (`bonus_code` populated for grants only) |
| `released`  | A bonus chunk moved from pending to released                           |
| `consumed`  | A released chunk was consumed against a wager                          |
| `expiry`    | A chunk or bonus expired before being consumed                         |
| `forfeited` | The bonus was forfeited before full consumption                        |

## 7. Get transaction detail 🔐

Full detail of a single bonus grant including all chunks, forfeit record (if any), and expiry events — the ground truth for disputes and reconciliation.

```
GET /api/v1/user-bonuses/{user_id}/transactions/{txn_id}
```

**Response `200 OK`**

```json
{
  "txn_id": 15,
  "user_id": "USER_001",
  "bonus_code": "WELCOME100",
  "wager_multiplier": "3.00",
  "no_of_chunks": 3,
  "chunk_expiry_days": 7,
  "bonus_expiry_days": 30,
  "wager_chip_type": "BONUS",
  "credit_chip_type": "CASH",
  "grant_amount": "500.00",
  "release_amount": "300.00",
  "bonus_consumed": "200.00",
  "status": "PARTIALLY_RELEASED",
  "created_at": "2025-05-01T08:00:00",
  "chunks": [
    {
      "id": 101,
      "chunk_ref": "CHUNK_001",
      "chunk_amount": "166.67",
      "wager_multiplier": "3.00",
      "status": "CONSUMED",
      "wager_amount": "500.00",
      "created_at": "2025-05-01T08:00:00",
      "updated_at": "2025-05-03T14:22:00"
    }
  ],
  "forfeit": null,
  "expiry_events": []
}
```

**Grant-level fields:** `grant_amount` (total granted), `release_amount` (total released), `bonus_consumed` (total consumed), derived lifecycle `status` (e.g. `PARTIALLY_RELEASED`), configuration snapshot (`wager_multiplier`, `no_of_chunks`, `chunk_expiry_days`, `bonus_expiry_days`, `wager_chip_type`, `credit_chip_type`).

**Chunk object:**

| Field                       | Description                                      |
| --------------------------- | ------------------------------------------------ |
| `id` / `chunk_ref`          | Chunk record ID / human-readable reference       |
| `chunk_amount`              | Bonus amount in this chunk                       |
| `wager_multiplier`          | Wagering multiplier for this chunk               |
| `status`                    | `PENDING`, `RELEASE`, `CONSUMED`, or `EXPIRED`   |
| `wager_amount`              | **Wagering progress toward release** (per chunk) |
| `created_at` / `updated_at` | Creation / last status change timestamps         |

**Forfeit object:** `id`, `amount`, `type` (e.g. `MANUAL`, `RULE_VIOLATION`), `operator` (nullable), `forfeited_at`.

**Expiry event object:** `id`, `chunk_id`, `amount`, `type` (`CHUNK_EXPIRY` or `BONUS_EXPIRY`), `operator` (nullable, if manual), `expired_at`.

## 8. Get user referral code

```
GET /api/v1/user-bonuses/{user_id}/referral-code
```

**Response `200 OK`**

```json
{
  "user_id": "USER_001",
  "referral_code": "REF_XYZ99",
  "created_at": "2025-05-01T08:00:00"
}
```

`created_at` is when the referral code was generated (UTC). Returns `404` if no referral code exists for the user. No auth header required. The code is shared with friends and drives `FRIEND_SIGNUP` trigger eligibility.

## Integration flow

```
1. User initiates a deposit / game round
       │
       ▼
2. GET /applicable-codes?user_id=&chip_type=
   → Display eligible bonus offers
       │
       ▼
3. User enters a code
   → POST /validate-code  { user_id, chip_type, code }
   → Check valid=true before proceeding; show reason to user if false
       │
       ▼
4. Release triggers fire (event-driven, internal)
   → Grant created; chunks move PENDING → RELEASE as the user meets
     trigger conditions and wagering progress accrues
       │
       ▼
5. User wagers using released bonus
   → PAM debits client wallet, then:
     POST /consume  { user_id, consume_txn_id, bonus_amount, wager_amount, wager_tnx_id, … }   🔐
       │
       ▼
6. If wager is voided:
   → POST /consume/{consume_txn_id}/revert
       │
       ▼
7. Display wallet / history:                                                                    🔐
     GET /{user_id}/summary
     GET /{user_id}/transactions?chip_type=cash
     GET /{user_id}/transactions/{txn_id}
```
