---
title: Player ID login
nav: Player ID login
order: 4
---
# 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](/docs/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
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:

```json
{"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](/docs/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:

```json
{"valid":true,"display_name":"Wisp","attributes":{"level":12,"newPlayer":false}}
```

- **`valid`:** must be `true` to sign the player in. `{"valid":false}` or a `404` means 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

```js
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](/docs/player-data/), 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.paid` webhook carries `identity_method`: `token` or `player_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.
