# Identity verification

> Prove a user is who they say they are, so nobody can read someone else's history.

Source: https://conciergesdk.com/docs/identity/

`identify(userId:)` ties a device to one of your users. With a signed token from your server, their history follows them to every device. Without one, Concierge can't know the `userId` is real (your app key ships inside the app, so anyone can send any id), so an unverified identify stays on the device that made it.

New apps require verification by default.

## How it works

1. Copy the app's **identity secret** (`ids_...`) from the dashboard into your server's environment. It never ships in your app.
2. Your server signs an **HS256 JWT** whose `sub` (or `userId` claim) equals the user's id. `exp` is required and can be at most 30 days away.
3. The app passes that token to `identify`.
4. Keep **Require verified users** on (the default for new apps). Identify without a token answers `401 identity_verification_required`, and identified customers must be verified to send. Signed-out devices that never called `identify` can still chat unless you turn off **Signed-out chat**.

## Sign the token on your server

No library needed. Node:

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

const b64 = (value) => Buffer.from(JSON.stringify(value)).toString('base64url');

export function conciergeToken(userId) {
  const header = b64({ alg: 'HS256', typ: 'JWT' });
  const payload = b64({ sub: userId, exp: Math.floor(Date.now() / 1000) + 60 * 60 });
  const signature = createHmac('sha256', process.env.CONCIERGE_IDENTITY_SECRET)
    .update(`${header}.${payload}`)
    .digest('base64url');
  return `${header}.${payload}.${signature}`;
}
```

Any JWT library works too (`jose`, `PyJWT`, `golang-jwt`, `firebase/php-jwt`): algorithm HS256, key = the identity secret, `sub` = your user id.

## Pass it from the app

```swift
// iOS
Concierge.identify(userId: user.id, token: try await api.conciergeToken())
```

```kotlin
// Android
Concierge.identify(userId = user.id, token = jwtFromYourServer)
```

The iOS SDK keeps the token in memory only, so fetch a fresh one every launch. If the server rejects it, iOS emits `.identityRejected(error)` through `Concierge.onEvent`.

## Rules worth knowing

- A `userId` that was **ever verified** can only be claimed again with a valid token. Turning verification on later doesn't leave old users exposed.
- **Verified:** an anonymous device that identifies as a new `userId` is upgraded in place. One that identifies as an existing user is moved to that customer, and its anonymous messages are appended to the user's conversation.
- **Unverified** (verification off, no token): the claim belongs to that one device. Another device sending the same `userId` gets its own separate customer, never the first one's history or profile. The inbox shows these as **Unverified** with the claimed id. Sign the user in with a token later and the device's history joins their verified account.
- `logout()` starts a fresh anonymous customer on that device.
- The verified user id also drives the **store account token** that links App Store and Play purchases to the customer (refund desk, receipts, renewal heads ups). Unverified ids never match. See [Link purchases to customers](https://conciergesdk.com/docs/offers.md#link-purchases-to-customers).

## Rotating the secret

**Rotate** in the dashboard (or `POST /v1/apps/:appId/identity-secret/rotate`) returns a new secret, and only that response and the setup screen ever show it. The old secret keeps verifying tokens for 24 hours, and the dashboard shows both with the time the old one expires, so you can deploy the new secret to your server without breaking sign-ins. Rotating again inside those 24 hours retires the oldest secret right away.

> **Coming soon:** App Attest and Play Integrity, so a public app key can't be used for spam.
