> 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/overview.md).

# Conventions & Authentication

**Base path:** `/api/v1/user-bonuses`

The User Bonus API allows game clients and back-office integrations (the PAM) to query a user's applicable bonuses, record bonus consumption against wager events, revert erroneous consumptions, and inspect the full bonus transaction history.

## Authentication — S2S HMAC signature

All requests are server-to-server (S2S). Every API endpoint requires these headers.

The signature check runs at the router level. No endpoint is unauthenticated. This includes `GET /applicable-codes`, `POST /validate-code`, `POST /consume/{consume_txn_id}/revert`, and `GET /{user_id}/referral-code`.

| Header        | Type   | Description                                            |
| ------------- | ------ | ------------------------------------------------------ |
| `X-Client-Id` | string | Your registered client identifier                      |
| `X-Timestamp` | string | Current Unix epoch seconds, such as `1716112200`       |
| `X-Signature` | string | HMAC-SHA256 hexadecimal digest of the canonical string |

Requests more than 300 seconds from the server clock are rejected. This prevents replay attacks.

### Build the canonical string

```
{client_id}\n{timestamp}\n{query_string}\n{raw_body}
```

* `client_id` is the `X-Client-Id` value.
* `timestamp` is the exact `X-Timestamp` string.
* `query_string` is the raw URL query string. Use an empty string without parameters.
* `raw_body` is the raw UTF-8 request body. Use an empty string for `GET` requests.

Sign the canonical string:

```
signature = HMAC-SHA256(client_secret, canonical)
X-Signature = hex(signature)
```

Client secrets are provisioned per client. A Redis-cached lookup resolves them. Contact the platform team to register your `client_id` and `client_secret`.

### Applicable APIs

The S2S HMAC signature is required for these endpoints:

* `GET /applicable-codes`
* `POST /validate-code`
* `POST /consume`
* `POST /consume/{consume_txn_id}/revert`
* `GET /{user_id}/summary`
* `GET /{user_id}/transactions`
* `GET /{user_id}/transactions/{txn_id}`
* `GET /{user_id}/referral-code`

### Code examples

#### Python — POST with body

```python
import hashlib, hmac, time, json, requests

client_id = "game_server"
client_secret = "your_shared_secret"
timestamp = str(int(time.time()))

body = json.dumps({
    "user_id": "USER_001",
    "consume_txn_id": "TXN20250519001",
    "wager_amount": "300.00",
    "bonus_amount": "100.00",
    "wager_tnx_id": "WAGER_REF_001"
}, separators=(',', ':'))

query_string = ""  # No query parameters on this endpoint.
canonical = f"{client_id}\n{timestamp}\n{query_string}\n{body}".encode()
signature = hmac.new(client_secret.encode(), canonical, hashlib.sha256).hexdigest()

resp = requests.post(
    "http://localhost:8010/api/v1/user-bonuses/consume",
    data=body,
    headers={
        "Content-Type": "application/json",
        "X-Client-Id": client_id,
        "X-Timestamp": timestamp,
        "X-Signature": signature,
    }
)
```

#### Node.js — POST with body

```javascript
const crypto = require("crypto");

const clientId = "game_server";
const clientSecret = "your_shared_secret";
const timestamp = String(Math.floor(Date.now() / 1000));

const body = JSON.stringify({
  user_id: "USER_001",
  consume_txn_id: "TXN20250519001",
  wager_amount: "300.00",
  bonus_amount: "100.00",
  wager_tnx_id: "WAGER_REF_001",
});

const queryString = ""; // No query parameters on this endpoint.
const canonical = `${clientId}\n${timestamp}\n${queryString}\n${body}`;
const signature = crypto
  .createHmac("sha256", clientSecret)
  .update(canonical)
  .digest("hex");

fetch("http://localhost:8010/api/v1/user-bonuses/consume", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Client-Id": clientId,
    "X-Timestamp": timestamp,
    "X-Signature": signature,
  },
  body,
});
```

#### Python — GET with query parameters

```python
import hashlib, hmac, time, requests
from urllib.parse import urlencode

client_id = "game_server"
client_secret = "your_shared_secret"
timestamp = str(int(time.time()))

params = {"user_id": "USER_001", "chip_type": "cash"}
query_string = urlencode(params)  # "user_id=USER_001&chip_type=cash"

canonical = f"{client_id}\n{timestamp}\n{query_string}\n".encode()
signature = hmac.new(client_secret.encode(), canonical, hashlib.sha256).hexdigest()

requests.get(
    "http://localhost:8010/api/v1/user-bonuses/applicable-codes",
    params=params,
    headers={
        "X-Client-Id": client_id,
        "X-Timestamp": timestamp,
        "X-Signature": signature,
    },
)
```

## Common conventions

* **Monetary values are decimal strings** (e.g. `"300.00"`) to avoid floating-point precision loss. Parse with a `Decimal` or equivalent type.
* **Timestamps are UTC** in ISO 8601 format (`YYYY-MM-DDTHH:MM:SS`).
* **Chip type** — player-facing operations take `chip_type`: `"cash"` or `"in_app_purchase"`.
* **Idempotency (`consume_txn_id`)** — generate it on your side (e.g. a transaction UUID) and **store it before calling the API**. A retry with the same `consume_txn_id` returns **`409 Conflict`** — it is your duplicate-detection signal, not a silent no-op. Pass the same value to the revert endpoint to cancel a consumption.
* **Chunk status lifecycle:** `PENDING → RELEASE → CONSUMED / EXPIRED`. A chunk must be in `RELEASE` status before it can be consumed.
* **Pagination** — `limit` (1–200, default 50) + `offset` (default 0, ≥ 0).
* **PII** — `email`, `phone`, `ip` must be hashed (SHA-256 with project salt) before persistence; raw values may transit `api-service` over TLS but must never be logged or written to ClickHouse / MongoDB unhashed.

## Error responses

All errors follow a consistent envelope:

```json
{
  "detail": "Human-readable error message"
}
```

| HTTP Status                 | Scenario                                                                                                                                  |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `400 Bad Request`           | Request validation failed (missing fields, out-of-range values)                                                                           |
| `401 Unauthorized`          | An authentication header is missing, the client is unregistered, the signature is invalid, or the timestamp is outside the allowed window |
| `404 Not Found`             | The referenced bonus grant, chunk, or consumption record does not exist, or does not belong to the given user                             |
| `409 Conflict`              | Duplicate consumption (`consume_txn_id` already recorded), no released chunk available for user, or consumption already reverted          |
| `500 Internal Server Error` | Unexpected database error                                                                                                                 |

## Admin configuration conventions

Back-office configuration operations (heads, subheads, configures — see [Configuration APIs](broken://pages/K44hCnFoLjBc1OK0LM0t)) additionally use:

* **Actor fields** — every write carries `created_by` / `updated_by` for the audit trail; history endpoints return the full change log.
* **JSON-string parameters** for budgets, limits, and owners:
  * `budget_json`: `'[{"period_type": "DAILY", "budget_limit": 5000}]'` — `period_type` ∈ `DAILY|WEEKLY|MONTHLY`, `budget_limit: null` = uncapped
  * `limits_json`: `'{"limits": [{"period_type": "MONTHLY", "budget_limit": 50000}], "updated_by": "admin"}'`
  * `owners_json`: `'{"owners": [{"username": "alice", "role": "OPS_LEAD", "active": true}], "updated_by": "admin"}'` — role ∈ `OPS_LEAD|CAMPAIGN_MANAGER|FINANCE_APPROVER|ESCALATION_CONTACT`
