Custom Auth Webhook
Custom login lets your own backend decide who a player is. When a game client calls POST /sdk/v1/authorization/custom, Rudder POSTs the client’s customData to an HTTP endpoint you operate, and your response determines the player identity.
The same endpoint also serves identity linking: when a logged-in player calls POST /sdk/v1/authorization/custom/link, Rudder calls the same webhook with the same request shape and attaches the returned subject to that player. The contract does not distinguish the two cases — your endpoint answers identically.
This page documents the webhook side of the contract. For the client side, see Authentication.
Configuration
Section titled “Configuration”Custom providers are configured per project, per environment, and there can be several of them, each under a name:
default— the provider used when the client omitsproviderin the login/link request. Its identities are bound as(custom, subject).- any other name — a named provider, e.g.
steam. Its identities are bound as(custom:<name>, subject), a separate namespace per provider. Names must match^[a-z0-9][a-z0-9_-]{0,31}$and cannot begoogle,apple, ordevice(reserved).
Each provider has three settings:
enabled— master switch. When off (or the provider is not configured), requests for it fail with401 custom auth disabled.url— the HTTP(S) endpoint Rudder calls.secret— a shared secret used to sign requests (see below). Each provider has its own secret.
Set them in the dashboard under Project Settings → Authentication (https://app.rudder.build/), or via the platform API: GET / PUT /platform/v1/projects/{projectId}/auth-providers. The PUT body is {environment, authProviders: {custom: {"<name>": {enabled, url, secret}}, ...}} (the same object also carries google and apple blocks). Changes take effect immediately — no deploy or restart needed.
Request
Section titled “Request”Rudder sends:
POST {url}Content-Type: application/jsonX-Signature: <hex HMAC-SHA256>
{"customData": { ... }}- The body is exactly
{"customData": <value>}where<value>is the JSON object the game client passed tologinWithCustom/LoginWithCustomAsync. The platform does not inspect or augment it. X-Signatureis the lowercase hex-encoded HMAC-SHA256 of the raw request body, keyed with the provider’ssecret. Always verify it before trusting the payload — compute HMAC over the exact bytes received, not a re-serialized JSON document.customDatacomes from the game client, so treat its contents as untrusted input even with a valid signature. Don’t put secrets into it on the client.
If your backend is written in Go, the server SDK ships a helper: rudder.VerifyWebhookSignature(secret, body, signature) returns true when the signature matches.
Response
Section titled “Response”Your endpoint must answer 200 OK with a JSON body:
{ "subject": "user-12345", "nickname": "Alice", "avatarUrl": "https://cdn.example.com/avatars/alice.png", "data": { "profile": { "tier": "gold", "level": 42 }, "entitlements": ["season-pass"] }}-
subject(required) — your stable, unique identifier for the player. Rudder binds the identity(project, environment, provider, subject)to a player account, whereprovideriscustomfor the default provider orcustom:<name>for a named one: the first login with a given subject creates the player, later logins with the same subject return the same player. The environment comes from the SDK key, so the same subject under the staging key and the prod key yields two separate players. Never reuse or change a subject — that would merge or orphan accounts. -
nickname(optional) — when non-empty, it overrides the nickname the client sent, and on later logins it also updates the existing player’s nickname. Omit it (or send an empty string) to leave nickname handling to the client. -
avatarUrl(optional) — when non-empty, it is stored as the player’s avatar URL: set on creation and updated on later logins whenever you send it. Omit it (or send an empty string) to leave the avatar alone. -
data(optional) — an object written straight into the player’s storage on every successful login. Each top-level key becomes a storage key and its JSON value is stored verbatim, so the example above writes the keyprofilewith value{"tier":"gold","level":42}and the keyentitlementswith value["season-pass"].
A 200 with an unparseable body or an empty subject is treated as 502 custom auth invalid response on the client.
Writing player storage from the webhook
Section titled “Writing player storage from the webhook”data is the only trusted way to seed or refresh player storage at login time — it comes from your backend, not from the game client.
- Merge, not replace. Only the keys present in
dataare touched. Keys the player already has and thatdataomits are left alone. - Same namespace as the SDK. Keys land in the player’s storage root, alongside anything the client wrote with
setStorage. A key present in both wins from the webhook — pick key names deliberately if your game also writes storage from the client. - Same limits as the storage API: key at most 128 characters, value at most 64 KiB, 1000 keys per player. Exceeding any of them fails the login with
401 custom auth invalid data— nothing is written. - The write is part of the login. If storage cannot be written, the player does not get a token. Omit
data(or send an empty object) when there is nothing to write. - Values are stored as raw JSON, so the client reads them back exactly as you sent them.
Error semantics
Section titled “Error semantics”What your endpoint answers decides what the game client gets:
| Your response | Retried? | Client receives |
|---|---|---|
200 + valid body | — | Token pair (login succeeds) |
200 + bad JSON / empty subject | No | 502 custom auth invalid response |
200 + data breaking storage limits | No | 401 custom auth invalid data |
Any 4xx | No | 401 custom auth rejected |
Any 5xx | Yes | 503 custom auth unavailable after retries are exhausted |
| Timeout / connection error | Yes | 503 custom auth unavailable after retries are exhausted |
Use a 4xx (typically 401 or 403) to deny login — for example when the credentials in customData are invalid. A 4xx is final: Rudder does not retry it and the player sees an authentication failure immediately.
Timeouts and retries
Section titled “Timeouts and retries”- Each attempt has a 3 second timeout (connect + response).
- Rudder makes up to 3 attempts, waiting 300 ms between them.
- Only network errors, timeouts, and
5xxresponses are retried.4xxresponses fail immediately. - Worst case, a failed login blocks the client for roughly 10 seconds (3 × 3s + 2 × 300ms). Keep your endpoint fast — it sits on the player’s login path. If your user lookup is slow, the whole game’s logins are slow.
After a successful webhook call
Section titled “After a successful webhook call”Once your endpoint vouches for the subject, the standard login flow runs:
- Identity lookup by
(projectId, environment, provider, subject)—provideriscustomfor the default provider orcustom:<name>for a named one. - If the player exists but is banned or deleted, login still fails (
403 player banned/403 player deleted) — your webhook approving the credentials does not override a ban. - If the subject is new, the player and identity are created in one transaction, the
player.createdkernel event is emitted, and the nickname (webhook’s, falling back to the client’s) plus the client’sregion/languageare stored on the profile. - If the response carried
data, its keys are written to the player’s storage — for both new and returning players. - The client receives an access token (24 h) and refresh token (7 days) — see Authentication.
On a link call (POST /sdk/v1/authorization/custom/link, with the player’s bearer token) steps differ: the returned subject is attached to the already-authenticated player — no player is created, no tokens are issued (the response is 204), and if the subject is already bound to another player the call fails with 409 identity already linked. A data payload is still written to that player’s storage.