---
title: Player data endpoint
nav: Player data
order: 5
---
# Player data: show the player their balance, level and name in the store

The store opens signed in from a [player token](/docs/player-tokens/). To show the player who they are (name, avatar, level, currency balances) and to refresh those after a purchase and after a free reward is claimed, your backend exposes one read-only endpoint that the store calls. It is optional: without it the store shows the level from the token, and no balances. The spender tier is never shown to the player.

## The endpoint

Register the URL in Studio → Integration → **Player data URL** (HTTPS, your domain). The store calls:

```
GET <playerDataUrl>?player=<player_id>
x-appside-timestamp: 1793268000
x-appside-signature: <lowercase hex HMAC-SHA256>
Accept: application/json
```

The store calls with two headers, the same scheme as webhooks:

- `x-appside-timestamp`: Unix time in **seconds** (integer, as a decimal string).
- `x-appside-signature`: **lowercase hex** HMAC-SHA256, keyed with your store's webhook secret, over the UTF-8 string `"<timestamp>.player-data.<player_id>"`, where `<player_id>` is exactly the `player` query value.

Verify with a constant-time comparison, and reject timestamps more than 300 seconds from your clock. There is no `Authorization` header. The `player-data.` prefix keeps these signatures apart from webhook signatures made with the same secret: a webhook signature covers `"<ts>." + rawBody`, and a JSON body never starts with `player-data.`.

## The response

```json
{
  "name": "Wisp",
  "avatar": "https://game.example/avatars/wisp.png",
  "level": 12,
  "balances": [
    { "currency": "shards", "label": "Star Shards", "amount": 550, "icon": "https://game.example/icons/shard.png" }
  ]
}
```

| Field | Type | Shown |
|---|---|---|
| `name` | string ≤ 40 | Header |
| `avatar` | HTTPS image URL | Header |
| `level` | integer 0–1,000,000 | Header only |
| `balances[]` | up to 4 currencies (`currency`, `label`, `amount`, `icon`) | Header; the first is the primary balance |

Offer targeting uses only the attributes in the signed player token (level, spender tier, country, new player). Data returned by this endpoint is displayed and never changes which offers a player can buy. `tier` and `segments` fields, if present, are ignored.

Every field is optional. Unknown fields are ignored. The response must be `application/json`, under 8 KB, and answer within 2 seconds; on error or timeout the store falls back to the token attributes and shows no balances.

## When the store calls it

- When the store opens (after the player token is verified).
- After your backend acknowledges `order.paid` (a 2xx response to the signed webhook). Until then the store shows "Delivering…". The balance shown is whatever this endpoint returns; the store never computes balances itself, and showing a balance is not confirmation of a payment or a grant.
- At most once per 60 seconds per player otherwise (cached).

## Security notes

- Return data only for the `player` in the query, and only after verifying the signature.
- Never include secrets, emails or payment details.
- The endpoint is read-only: nothing the store sends changes the wallet. Grants happen only through your signed webhook handler.

## Example (Node)

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyPlayerDataSignature(req, secret) {
  const ts = String(req.get('x-appside-timestamp') || ''), sig = String(req.get('x-appside-signature') || '');
  if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = createHmac('sha256', secret).update(`${ts}.player-data.${req.query.player}`).digest();
  const given = Buffer.from(sig, 'hex');
  return given.length === expected.length && timingSafeEqual(given, expected);
}

app.get('/appside/player', async (req, res) => {
  if (!verifyPlayerDataSignature(req, process.env.APPSIDE_WEBHOOK_SECRET)) return res.status(401).end();
  const p = await players.get(req.query.player);
  if (!p) return res.status(404).end();
  res.json({ name: p.displayName, avatar: p.avatarUrl, level: p.level,
    balances: [{ currency: 'shards', label: 'Star Shards', amount: p.shards }] });
});
```
