Skip to content

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.

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 omits provider in 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 be google, apple, or device (reserved).

Each provider has three settings:

  • enabled — master switch. When off (or the provider is not configured), requests for it fail with 401 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.

Rudder sends:

POST {url}
Content-Type: application/json
X-Signature: <hex HMAC-SHA256>
{"customData": { ... }}
  • The body is exactly {"customData": <value>} where <value> is the JSON object the game client passed to loginWithCustom / LoginWithCustomAsync. The platform does not inspect or augment it.
  • X-Signature is the lowercase hex-encoded HMAC-SHA256 of the raw request body, keyed with the provider’s secret. Always verify it before trusting the payload — compute HMAC over the exact bytes received, not a re-serialized JSON document.
  • customData comes 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.

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, where provider is custom for the default provider or custom:<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 key profile with value {"tier":"gold","level":42} and the key entitlements with value ["season-pass"].

A 200 with an unparseable body or an empty subject is treated as 502 custom auth invalid response on the client.

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 data are touched. Keys the player already has and that data omits 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.

What your endpoint answers decides what the game client gets:

Your responseRetried?Client receives
200 + valid bodyToken pair (login succeeds)
200 + bad JSON / empty subjectNo502 custom auth invalid response
200 + data breaking storage limitsNo401 custom auth invalid data
Any 4xxNo401 custom auth rejected
Any 5xxYes503 custom auth unavailable after retries are exhausted
Timeout / connection errorYes503 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.

  • 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 5xx responses are retried. 4xx responses 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.

Once your endpoint vouches for the subject, the standard login flow runs:

  1. Identity lookup by (projectId, environment, provider, subject)provider is custom for the default provider or custom:<name> for a named one.
  2. 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.
  3. If the subject is new, the player and identity are created in one transaction, the player.created kernel event is emitted, and the nickname (webhook’s, falling back to the client’s) plus the client’s region / language are stored on the profile.
  4. If the response carried data, its keys are written to the player’s storage — for both new and returning players.
  5. 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.