Authentication
Players authenticate against the Rudder API (https://api.rudder.build) and receive a JWT pair. This is separate from dashboard users — your game’s players never have accounts on the platform.
There are four login methods:
- Device login — passwordless login keyed by a device identifier. The default for most games.
- Google login — the player signs in with Google; the platform verifies the ID token.
- Apple login — the player signs in with Apple; the platform verifies the ID token.
- Custom login — the platform calls an HTTP webhook on your backend to verify the player’s identity. Use this when you already have your own account system (or several — named providers are supported). See Custom Auth Webhook for the full contract.
Every login binds an identity (provider, subject) to the player. One player can carry several identities — see Linking and unlinking identities.
SDK key
Section titled “SDK key”Every login request carries your project’s SDK key. The key identifies both the project and the environment — a project has exactly two, staging and prod, and each has its own key. You find them in the dashboard under Project Settings (https://app.rudder.build/), for whichever environment the switcher is on.
The key is the only place the environment appears. Nothing in the client API takes an environment argument: the key is resolved at login and the environment is written into the tokens the player gets back. That also means a build pointing at the staging key can never touch production players — the same device or the same custom-auth subject creates two completely separate accounts under the two keys. See Projects & Environments.
import { createClient } from '@rudder/sdk';
const client = createClient({ projectKey: 'YOUR_SDK_KEY',});using RudderSdk;
var client = new RudderClient(new RudderClientOptions{ BaseUrl = "https://api.rudder.build", ProjectKey = "YOUR_SDK_KEY",});Device login
Section titled “Device login”POST /sdk/v1/authorization/device with body {key, deviceId, region?, language?, nickname?}.
The first login with a given device ID creates the player and binds the identity (device, deviceId) to them. Subsequent logins with the same device ID return the same player. If the player is banned or deleted, login is rejected.
// The device ID is auto-generated (crypto.randomUUID()) on first call and// persisted in localStorage under "rudder_device_id".const { accessToken, refreshToken } = await client.auth.loginWithDevice({ region: 'eu', // default: "global" language: 'en', // default: "en" nickname: 'Alice', // optional});var session = await client.Auth.LoginWithDeviceAsync( region: "eu", language: "en", nickname: "Alice"); // optionalBehavior notes:
- Nickname is only applied when the player doesn’t have one yet — device login never overwrites an existing nickname.
- Every login — new or returning player — records activity metrics. On a brand-new player, login additionally emits the
player.createdkernel event, which module event functions can handle. For returning players,lastSeenis touched separately (throttled to one write per 5 minutes). - C# device ID caveat: the default
GuidDeviceIdProvidergenerates a new GUID perRudderClientinstance and does not persist it. If you don’t provide your ownIDeviceIdProvider(e.g. backed byPlayerPrefsor a file), every app restart creates a new player. The TypeScript SDK persists the device ID inlocalStorageout of the box.
Google login
Section titled “Google login”POST /sdk/v1/authorization/google with body {key, idToken, region, language, nickname?}.
The player signs in with Google on your side (Google Sign-In / Google Identity Services) and hands the resulting ID token to the SDK. The backend verifies it against Google’s public keys (JWKS) — signature, expiry, issuer, and audience: the token’s aud must match one of the client IDs you configured. The verified sub claim becomes the identity subject: the first login with a given Google account creates the player, later logins return the same player.
const { accessToken, refreshToken } = await client.auth.loginWithGoogle({ idToken, // ID token from Google Sign-In region: 'eu', language: 'en', nickname: 'Alice', // optional});var session = await client.Auth.LoginWithGoogleAsync( idToken, region: "eu", language: "en", nickname: "Alice"); // optionalConfigure it per environment in the dashboard under Project Settings → Authentication: enable Google and list your OAuth client IDs (one per app — web, Android and iOS clients each have their own). When the provider is disabled or not configured, login fails with 401 auth provider disabled; a token that fails verification fails with 401 invalid provider token.
Apple login
Section titled “Apple login”POST /sdk/v1/authorization/apple with body {key, idToken, region, language, nickname?}. Identical to Google login, with Sign in with Apple tokens: the backend verifies the ID token against Apple’s JWKS (issuer https://appleid.apple.com) and uses sub as the subject.
const { accessToken, refreshToken } = await client.auth.loginWithApple({ idToken, // identity token from Sign in with Apple region: 'eu', language: 'en',});var session = await client.Auth.LoginWithAppleAsync( idToken, region: "eu", language: "en");Configure it next to Google in Project Settings → Authentication: enable Apple and list the client IDs you accept (your app’s bundle ID, and the services ID for web sign-in). The same 401 auth provider disabled / 401 invalid provider token errors apply.
Custom login
Section titled “Custom login”POST /sdk/v1/authorization/custom with body {key, provider?, customData, region, language, nickname?}.
The platform forwards customData to the webhook URL configured for the environment, and your backend decides who the player is by returning a subject. The full request/response contract, signature verification, and retry semantics are documented on the Custom Auth Webhook page.
provider selects which configured custom provider handles the request:
- omitted (or
"default") — the default provider. The identity is bound as(custom, subject). - any other name — a named provider. The identity is bound as
(custom:<name>, subject), so each provider gets its own subject namespace.
await client.auth.loginWithCustom({ customData: { sessionId: 'your-backend-session-token' }, region: 'eu', language: 'en',});
// Named provider (must be configured in the dashboard):await client.auth.loginWithCustom({ provider: 'steam', customData: { ticket: 'steam-session-ticket' }, region: 'eu', language: 'en',});using Newtonsoft.Json.Linq;
await client.Auth.LoginWithCustomAsync( new JObject { ["sessionId"] = "your-backend-session-token" }, region: "eu", language: "en");
// Named provider (must be configured in the dashboard):await client.Auth.LoginWithCustomAsync( new JObject { ["ticket"] = "steam-session-ticket" }, region: "eu", language: "en", provider: "steam");Custom login must be enabled per environment in Project Settings → Authentication, otherwise the call fails with 401 custom auth disabled.
Named custom providers
Section titled “Named custom providers”You can configure several custom providers per environment — one per external account system, say steam and epic. Each provider has its own webhook URL and secret, and its own identity namespace: the same subject under custom:steam and custom:epic is two different identities, which can even be linked to the same player.
Provider names must match ^[a-z0-9][a-z0-9_-]{0,31}$ (lowercase letters, digits, _, -, at most 32 characters), and google, apple, device are reserved. The webhook contract is the same for every provider — see Custom Auth Webhook.
Linking and unlinking identities
Section titled “Linking and unlinking identities”A player account can carry several identities — one per provider. The typical flow: log the player in with any method (usually device login on first launch), then link the other providers from the same session. Later, a login through any linked identity lands on the same player — this is how an anonymous device-only account becomes a permanent one that survives reinstalls and device switches.
Linking requires the player’s access token (Authorization: Bearer <token>):
POST /sdk/v1/authorization/google/linkwith{idToken}→204POST /sdk/v1/authorization/apple/linkwith{idToken}→204POST /sdk/v1/authorization/custom/linkwith{provider?, customData}→204
Google and Apple links verify the token exactly like the login endpoints. A custom link calls the provider’s webhook with the same contract — the returned subject is attached to the current player instead of resolving a session, and a data payload is still written to the player’s storage.
// Logged in already (any method), then link:await client.auth.linkWithGoogle(idToken);await client.auth.linkWithApple(idToken);await client.auth.linkWithCustom({ customData: { sessionId: '...' } });await client.auth.linkWithCustom({ provider: 'steam', customData: { ticket: '...' } });
// Detach an identity:await client.auth.unlinkIdentity('google');// Logged in already (any method), then link:await client.Auth.LinkWithGoogleAsync(idToken);await client.Auth.LinkWithAppleAsync(idToken);await client.Auth.LinkWithCustomAsync(new JObject { ["sessionId"] = "..." });await client.Auth.LinkWithCustomAsync(new JObject { ["ticket"] = "..." }, provider: "steam");
// Detach an identity:await client.Auth.UnlinkIdentityAsync("google");Conflict rules:
409 identity already linked— the identity is already bound to another player (someone else owns that Google account or custom subject), or the current player already has that provider linked. One provider per player, one player per identity.- Unlink takes the full provider string:
device,google,apple,custom, orcustom:<name>. 404 identity not linked— the player has no identity for that provider.400 cannot unlink last identity— the last remaining identity cannot be removed; the player would become unreachable.
Support can also unlink identities from the dashboard: the player page → Linked Accounts → Unlink.
Tokens
Section titled “Tokens”A successful login returns two HS256-signed JWTs:
- Access token — valid for 24 hours. Claims:
id(player ID),projectId, andenvironment. Sent asAuthorization: Bearer <token>on every SDK request. - Refresh token — valid for 7 days. Claims:
idandenvironment. Exchanged for a new token pair viaPOST /sdk/v1/authorization/refreshwith body{refreshToken}.
The environment claim is required. A token without it is rejected with 401.
Both login and refresh return a fresh pair — store both tokens from every response.
Storage
Section titled “Storage”- TypeScript: tokens are persisted in
localStorageunderrudder_access_token/rudder_refresh_token(with a silent in-memory fallback where localStorage is unavailable, e.g. SSR or private mode — sessions then live only for the page session). Pass your owntokenStoreinRudderClientOptionsto use another backend. - C#: the default
InMemoryTokenStorekeeps tokens in memory only — sessions do not survive an app restart. ImplementITokenStore(GetAccessToken/GetRefreshToken/SaveTokens/Clear) backed by persistent storage for production builds.
Refresh flow
Section titled “Refresh flow”Both SDKs refresh automatically: when any request returns 401, the client performs a single-flight refresh (concurrent requests share one refresh call) and retries the original request once. If the refresh fails — expired refresh token, a token with no environment claim, or the player was banned/deleted in the meantime — the stored tokens are cleared and the session ends. Handle the signed-out state by logging the player in again; that is the whole recovery path.
// Refresh is automatic. Observe the session state instead:const unsubscribe = client.auth.onAuthStateChange((state) => { // state: 'signed-in' | 'signed-out' — fires immediately with current state if (state === 'signed-out') showLoginScreen();});
client.auth.logout(); // clears tokens and stops the runtime// Refresh is automatic on 401. You can also force it:bool renewed = await client.Auth.RefreshAsync(); // false = session is dead, tokens cleared
client.Auth.AuthStateChanged += state =>{ // RudderAuthState.SignedIn / RudderAuthState.SignedOut};
client.Auth.Logout(); // drops the stored sessionPlayer profile
Section titled “Player profile”GET /sdk/v1/player/information returns the profile:
{ "player": { "id": "…", "projectId": "…", "nickname": "Alice", "region": "eu", "language": "en", "createdAt": "2026-01-01T00:00:00Z" }, "wallets": [], "identities": [ { "provider": "device", "subject": "…", "createdAt": "2026-01-01T00:00:00Z" }, { "provider": "google", "subject": "1080123456789…", "createdAt": "2026-01-02T00:00:00Z" } ]}identities lists every identity linked to the player — provider is one of device, google, apple, custom, custom:<name>, and subject is the provider-side identifier (device ID, Google/Apple sub, or the subject your webhook returned).
// client.player is a state object, loaded automatically after login.await client.player.load();console.log(client.player.value?.player?.nickname);var profile = await client.Player.LoadAsync();Console.WriteLine(profile.Player.Nickname);Bans and deleted players
Section titled “Bans and deleted players”A player is banned when bannedAt is set and bannedUntil is either empty (permanent ban) or in the future. A deleted player is anonymized (nickname removed, data wiped, all identities unlinked) — deletion is irreversible.
Bans and deletions are enforced everywhere:
- Login and token refresh are rejected (
403 player banned/403 player deleted). - Every authenticated SDK endpoint re-checks the player on each call, so a ban takes effect immediately for active sessions.
- Both SDKs surface this as a failed refresh → signed-out state.
Ban and unban players from the dashboard’s Players page, or from your server via the admin API (POST /game/v1/players/ban with the X-API-Key admin key for that environment, body {items: [{playerId, reason?, bannedUntil?}]}; omit bannedUntil for a permanent ban).
Your backend can also introspect a player’s access token without trusting the client: POST /game/v1/players/auth/verify (X-API-Key auth, body {accessToken}) returns {playerId, projectId, environment, status} where status is active, banned, or deleted. The admin key you use decides the environment, and a token from the other environment does not verify.
Error reference
Section titled “Error reference”Login endpoints return these errors:
| HTTP | Message | Cause |
|---|---|---|
400 | key is required / deviceId is required / idToken is required / customData is required / … | Missing request fields (device login requires only key and deviceId; region and language are required only for Google/Apple/custom login) |
400 | invalid provider name | Custom provider name doesn’t match ^[a-z0-9][a-z0-9_-]{0,31}$ |
401 | invalid project key | Unknown SDK key |
401 | auth provider disabled | Google/Apple login not enabled or not configured for this environment |
401 | invalid provider token | The Google/Apple ID token failed verification (signature, expiry, issuer, or audience) |
401 | custom auth disabled | The (named) custom provider is not enabled for this environment |
401 | custom auth rejected | Your webhook answered 4xx |
401 | custom auth invalid data | The webhook’s data payload breaks storage limits |
403 | player banned | Player is banned |
403 | player deleted | Player was deleted |
502 | custom auth invalid response | Webhook returned 200 with a malformed body or empty subject |
503 | custom auth unavailable | Webhook unreachable / 5xx after all retries |
Link and unlink endpoints return the same provider errors, plus:
| HTTP | Message | Cause |
|---|---|---|
400 | cannot unlink last identity | Tried to unlink the player’s last remaining identity |
404 | identity not linked | No identity for that provider on this player |
409 | identity already linked | Identity belongs to another player, or the player already has that provider |
Token refresh returns 401 invalid refresh token for an expired or malformed refresh token, and the same 403 errors for banned/deleted players.