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:

Set it up in Studio

  1. Go to Integration → Player login and turn on Player ID.
  2. Enter your verify URL. It must be a public HTTPS address on port 443.
  3. 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.
  4. Generate the verify secret. It is shown once; store it on your server. You can rotate it any time.
  5. 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}
HeaderValue
x-appside-timestampUnix seconds, equal to timestamp in the body
x-appside-signaturehex HMAC-SHA256 of "<timestamp>." + rawBody, keyed with the verify secret
idempotency-keyequal 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}}

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