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

# Gamification

## Gamification SDK: client integration

Embed the player's loyalty screen in an iframe. Your backend signs an authentication payload, and your frontend passes it to the iframe. The iframe handles authentication, API calls, and rendering.

### 1. Get integration details

Request these from the Wynta team:

* **Gamification UI URL** — the base URL for the iframe.
* **`client_id`** — your registered client identifier, linked to your site.
* **`client_secret`** — the signing secret. Keep it on your backend only.

Use HTTPS in production. You do not need to send `site_id`.

### 2. Backend: create a signed payload

Create an authenticated endpoint on your backend that returns `{ client_id, payload, hash }`. Use the logged-in player's ID from your server session.

#### Encoding and signing flow

Run all five steps on your backend:

```
Player claims → JSON string → UTF-8 bytes → Base64 string = payload
                                               │
                           client_secret + HMAC-SHA256
                                               │
                                       lowercase hex = hash

Return { client_id, payload, hash } to your frontend
```

1. **Build the claims object.** Include `user_id`, `timestamp` (current Unix seconds), and `transaction_id` (a new UUID for this attempt).
2. **Convert the object to a JSON string.** Use `JSON.stringify(claims)` in Node.js. For the fixed example below, the exact string is:

   ```
   {"user_id":"player_123","timestamp":1789430400,"transaction_id":"550e8400-e29b-41d4-a716-446655440000"}
   ```
3. **Encode that string as Base64.** Convert the JSON string to UTF-8 bytes, then encode those bytes using standard Base64, keeping any trailing `=` padding and adding no line breaks. This produces the `payload` field:

   ```
   eyJ1c2VyX2lkIjoicGxheWVyXzEyMyIsInRpbWVzdGFtcCI6MTc4OTQzMDQwMCwidHJhbnNhY3Rpb25faWQiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ==
   ```

   Base64 is reversible encoding, not encryption. It does not hide the player's ID.
4. **Sign the Base64 string.** Use HMAC-SHA256 with your `client_secret` as the key and the exact `payload` string from step 3 as the message. Convert the resulting digest to lowercase hexadecimal to create `hash`. Do not sign the raw JSON, decode the payload before signing, or include the surrounding JSON quotes.
5. **Return the request body.** Add your `client_id` alongside the unchanged `payload` and `hash`. The frontend passes this object to the iframe without re-encoding it. The secret is never included in the body.

#### Node.js implementation

```ts
import { createHmac, randomUUID } from 'node:crypto';

function createSdkAuthBody(userId: string, clientId: string, clientSecret: string) {
  const claims = {
    user_id: userId,
    timestamp: Math.floor(Date.now() / 1000),
    transaction_id: randomUUID(),
  };
  const json = JSON.stringify(claims);            // Object → JSON string
  const bytes = Buffer.from(json, 'utf8');        // JSON string → UTF-8 bytes
  const payload = bytes.toString('base64');       // UTF-8 bytes → Base64 string
  // Sign the exact Base64 string and return the digest as lowercase hex.
  const hash = createHmac('sha256', clientSecret).update(payload).digest('hex');

  return { client_id: clientId, payload, hash };
}
```

Example response using the dummy secret `example_secret`:

```json
{
  "client_id": "game_server",
  "payload": "eyJ1c2VyX2lkIjoicGxheWVyXzEyMyIsInRpbWVzdGFtcCI6MTc4OTQzMDQwMCwidHJhbnNhY3Rpb25faWQiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAifQ==",
  "hash": "4a8146401a72ea20fcab4091e5ae14db13d41aaf1955ba18de43b5b0047f0fab"
}
```

The `payload` decodes to:

```json
{
  "user_id": "player_123",
  "timestamp": 1789430400,
  "transaction_id": "550e8400-e29b-41d4-a716-446655440000"
}
```

The hash is `HMAC-SHA256(example_secret, payload)` in hex. These values are illustrative; use your registered credentials, the current timestamp, and a new transaction ID for actual requests.

Requirements:

* Sign the **Base64 string**, using HMAC-SHA256, and return the hash as hex.
* Generate the payload when requested; do not cache it. The default timestamp window is **60 seconds**, so keep your server clock accurate.
* Generate a new `transaction_id` and timestamp for every attempt, including retries.
* Return only the signed body to the frontend; never return `client_secret`.

### 3. Frontend: embed and authenticate

Register the message listener **before loading the iframe**. When it sends `PAGE_LOADED`, fetch a fresh signed body from your backend and reply with `SDK_AUTH_PAYLOAD`.

```html
<div id="gamification"></div>
<script>
  const gamificationUiUrl = 'https://<gamification-ui-host>';
  const gamificationUiOrigin = new URL(gamificationUiUrl).origin;
  const iframe = document.createElement('iframe');
  iframe.title = 'Loyalty progress';

  async function handleMessage(event) {
    if (event.origin !== gamificationUiOrigin) return;
    if (event.source !== iframe.contentWindow) return;
    if (event.data?.type !== 'PAGE_LOADED') return;

    try {
      // Replace this URL with your authenticated backend endpoint from step 2.
      const response = await fetch('/api/gamification/auth-payload', {
        method: 'POST',
        credentials: 'same-origin',
        cache: 'no-store',
      });
      if (!response.ok) throw new Error('Unable to authenticate gamification');
      const body = await response.json();

      iframe.contentWindow.postMessage(
        { type: 'SDK_AUTH_PAYLOAD', body },
        gamificationUiOrigin,
      );
    } catch (error) {
      console.error(error); // Show a retry option in your application's UI.
    }
  }

  window.addEventListener('message', handleMessage);
  iframe.src = `${gamificationUiUrl}/user-loyalty-points`;
  document.getElementById('gamification').appendChild(iframe);
  // Remove the message listener when your embedding component unmounts.
</script>
```

Do not put authentication data in the iframe URL. Always validate both `event.origin` and `event.source`, and send the reply to the exact iframe origin.

| Direction              | Message                                                            |
| ---------------------- | ------------------------------------------------------------------ |
| Iframe → your frontend | `{ type: 'PAGE_LOADED' }`                                          |
| Your frontend → iframe | `{ type: 'SDK_AUTH_PAYLOAD', body: { client_id, payload, hash } }` |

`PAGE_LOADED` means the iframe mounted; it does not confirm authentication or data loading. There is currently no ready/success message. The iframe displays its own loading and error states. Your client does not call the SDK auth endpoint or manage the player token.

### Troubleshooting

If iframe authentication returns `401 Invalid request`, check:

* The client is active and `client_id` / `client_secret` are correct.
* The HMAC signs the exact Base64 payload string.
* The timestamp is in Unix **seconds** and within the allowed window (default 60 seconds).
* The `transaction_id` has not already been used.

To retry, reload the iframe and let the handshake request a newly signed payload.
