Player data: show the player their balance, level and name in the store
The store opens signed in from a player token. 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 theplayerquery 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
{
"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
playerin 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)
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 }] });
});