Player ID login: sell without an app update
Players can sign in to your web store with the ID they see in your game. Your backend confirms the ID through one webhook, player.verify. You don't need an SDK, a signed token or a new app build to start selling.
Use it alongside player tokens, not instead of them:
- Player ID login lets players who type your store's address, or come from a social post, buy for their account.
- Signed tokens keep the smoothest path from inside your app: the player is already signed in, sees their balance and level, and can manage a membership or claim rewards.
Set it up in Studio
- Go to Integration → Player login and turn on Player ID.
- Enter your verify URL. It must be a public HTTPS address on port 443.
- Write the label and the hint players see, for example "Your Player ID · Settings → Profile in the game". Optionally add a screenshot showing where to find the ID.
- Generate the verify secret. It is shown once; store it on your server. You can rotate it any time.
- Press Send test with a real player ID. Studio shows the exact request, your response and the verdict. A store can publish with Player ID login only after its last test passed.
The request Appside sends
POST <your verify URL> with a JSON body:
{"type":"player.verify","tenant":"your-store","player_id":"wisp-123","request_id":"7f3c…","timestamp":1793360000}
| Header | Value |
|---|---|
x-appside-timestamp | Unix seconds, equal to timestamp in the body |
x-appside-signature | hex HMAC-SHA256 of "<timestamp>." + rawBody, keyed with the verify secret |
idempotency-key | equal to request_id |
The signature uses the same scheme as webhooks, with the verify secret. Reject requests older than 5 minutes.
Your response
Answer within 5 seconds with 200 and at most 16 KB of JSON:
{"valid":true,"display_name":"Wisp","attributes":{"level":12,"newPlayer":false}}
valid: must betrueto sign the player in.{"valid":false}or a404means no.display_name: optional. It is shown on the store ("Signed in with Player ID · Wisp").attributes: optional. They use the same keys as signed tokens (level,spenderTier,country,newPlayer), so offers and segments work the same way.
Redirects, other status codes, timeouts and invalid JSON all count as "not found".
Verify in Node
import { createHmac, timingSafeEqual } from 'node:crypto';
app.post('/appside/player.verify', express.raw({ type: '*/*' }), (req, res) => {
const ts = req.headers['x-appside-timestamp'], sig = req.headers['x-appside-signature'] || '';
const want = createHmac('sha256', process.env.APPSIDE_VERIFY_SECRET).update(`${ts}.`).update(req.body).digest();
const got = Buffer.from(sig, 'hex');
if (got.length !== want.length || !timingSafeEqual(got, want) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401);
const { player_id } = JSON.parse(req.body);
const player = findPlayer(player_id); // yours
res.json(player ? { valid: true, display_name: player.name, attributes: { level: player.level } } : { valid: false });
});
Security and privacy
- Uniform answers: unknown IDs, wrong IDs and timeouts all show the same message ("We couldn't find that Player ID"). Attempts are rate-limited per IP address, per store and per player ID.
- Weaker proof than a token: knowing someone's ID is weaker proof than a signed token. So a Player ID session can buy, but by default it does not show the player's balance or level from your player data endpoint, and it cannot manage memberships or claim daily rewards. Turn on "show player data to Player ID sessions" only if your IDs are hard to guess.
- Webhook flag: every
order.paidwebhook carriesidentity_method:tokenorplayer_id. You can apply your own policy, for example limits on large packs for ID sessions. - Delivery: items are delivered exactly as for token sessions, through the signed, deduplicated webhook.