Guest sign-in
Guest sign-in gives a player a permanent player identity without asking for an email or a password. The player returns to the same account thanks to a secret stored on their device; later, if they want, they move to a permanent account with email or Google. This page describes the HTTP endpoint, the secret’s lifecycle, and SDK behavior.
How does it work?
- The client generates a random 32-byte secret and writes it as 64 lowercase hexadecimal characters (
bootstrap_token). This secret is the key to the player’s account. - The client sends the secret with
POST /v1/auth/sign-in/guest. The server stores only the SHA-256 digest of the secret; the raw secret is not recorded or written to logs. - If the secret arrives for the first time, a new guest player (
display_nameGuest) and a new session are created (is_new_player: true). If the same secret arrives again, a new session is opened for the same player (is_new_player: false). - The secret has a 180-day lifetime that slides as it is used. A secret that has expired or been retired (for example because the account was upgraded to permanent) is rejected with
403.
Warning
The guest secret is an authority: a player who knows the secret enters the account. Do not pass it to other players, logs, or analytics.
Request
First get a CSRF token (Sessions and CSRF); then:
POST /v1/auth/sign-in/guest
content-type: application/json
x-crx-env: <environment-id>
x-csrf-token: <csrf-token>
cookie: __Secure-crx.csrf=<csrf-cookie>
{"bootstrap_token":"<64 lowercase hexadecimal characters>"}
The body carries only the bootstrap_token field.
Response
200 and the shared sign-in envelope:
{
"player": { "id": "0192…", "display_name": "Guest", "email": null, "email_verified": false, "is_guest": true, "image": null, "created_at": "2026-11-03T10:00:00+00:00" },
"tokens": { "session_token": "<64 hex>", "refresh_token": "<64 hex>", "access_token": "<jwt>", "expires_in_secs": 900 },
"is_new_player": false
}
Two Set-Cookie headers (__Secure-crx.session, __Secure-crx.refresh) and the new CSRF token (the x-csrf-token header) are also returned.
| Status | Meaning |
|---|---|
400 | The bootstrap_token format is broken (not 64 lowercase hexadecimal characters) or the JSON is broken |
401 | x-crx-env is missing or not a UUID |
403 | The secret was retired/expired, the account was deleted, or a CSRF/origin rejection (plain text); JSON for a banned owner {"code":"account_banned"} |
429 | 30 per 60 s per IP exceeded (re-sign-ins count too) |
A banned guest: if the request carries an old session cookie/Bearer that belongs to a banned player, or the secret belongs to a banned player, no new guest is created and 403 account_banned is returned; this response does not delete the cookies. See Banned accounts.
Display name
The guest’s name is Guest. The player can change their own name with POST /v1/auth/session/display-name (body {"displayName":"…"}): the name is converted to NFC, trimmed, and inner whitespace is collapsed to single spaces; control/direction/zero-width characters are rejected; the result must be 2–24 characters; 10 changes per hour per player. In Unity it is Session.RenameAsync; the TypeScript SDK has no method for this operation, so use raw REST.
TypeScript SDK
session.signInGuest() generates the secret for you, stores it before the request, and manages the CSRF token; so even if the response is lost or the cookie is deleted, the same secret returns to the same guest. session.restore() only restores an existing session and does not create a guest.
Secret storage options (the guestBootstrap setting of createPlayerClient):
| Option | Behavior |
|---|---|
| Default | localStorage in the browser (memory if it is inaccessible); memory in SSR/Node |
false | Nothing is stored; every signInGuest creates a new guest |
GuestBootstrapStore | Your own store (get/set/clear; Keychain, IndexedDB, file). Timeout is 3 s; if the store throws, the SDK falls back to memory |
Behavior rules:
- The secret is deleted after
logout, a successful email sign-in/sign-up,upgradeGuest, and a session seen withisGuest: false. Alogoutfrom the banned state does not delete the secret (so a banned player cannot escape by opening a new guest). - If the server rejects the secret (the endpoint handler’s
403; a CSRF/origin403and401do not count), the SDK deletes the secret once and makes one retry with a new secret. - In the
bannedstate there is no retry; no new guest is created.
Warning
The default
localStoragestore is readable by every script on the same origin; an XSS hole can steal the secret and take over the guest account. For high-value accounts, useguestBootstrap: falseor provide the platform’s secure store.
import {
ACCOUNT_BANNED_CODE,
CrxError,
createMemoryGuestBootstrapStore,
createPlayerClient,
type PlayerClient,
} from "@ferdiunal/open-play-ts";
/** Varsayılan davranış: misafir kimliği tarayıcıda `localStorage`'da saklanır ve aynı oyuncuya döner. */
export function createDefaultClient(environmentId: string): PlayerClient {
return createPlayerClient({ baseUrl: window.location.origin, environmentId });
}
/** Kalıcı kimlik istemeyen oyun: her `signInGuest` yeni bir misafir oyuncu açar. */
export function createEphemeralGuestClient(environmentId: string): PlayerClient {
return createPlayerClient({ baseUrl: window.location.origin, environmentId, guestBootstrap: false });
}
/** Sunucu tarafında çalışan kod için (SSR/Node) isteğe özel bellek deposu; sır sunucu belleğinden çıkmaz. */
export function createRequestScopedClient(baseUrl: string, environmentId: string): PlayerClient {
return createPlayerClient({ baseUrl, environmentId, guestBootstrap: createMemoryGuestBootstrapStore() });
}
/** Misafir girişi; banlı hesabı ayrı bir ekranla karşılar. */
export async function signInGuestOrShowBan(client: PlayerClient, showBanned: () => void): Promise<void> {
try {
await client.session.signInGuest();
} catch (error) {
if (error instanceof CrxError && error.code === ACCOUNT_BANNED_CODE) {
showBanned(); // client.session.state.status === "banned"; yeniden deneme döngüsü kurmayın.
return;
}
throw error;
}
}
In SSR (server-side rendering) the SDK does not use the environment’s localStorage and the secret is written to memory. An SSR app that wants a permanent guest identity must provide its own per-request store through guestBootstrap; the createRequestScopedClient example above uses only a process-lifetime memory store.
Unity SDK
Session.SignInGuestAsync() implements the same flow; the secret is kept in OpenPlayOptions.SecretStore. The default PlayerPrefs store is plain text and is not a secure store; in production provide your own ISecretStore implementation backed by Keychain/Keystore (Unity SDK). Session.RestoreAsync() does not create a guest. On a banned account the state becomes SessionStatus.Banned.
Turning a guest into a permanent account
The player can move to a permanent account without losing progress; player.id does not change:
- Email and password:
upgradeGuest - Google:
linkOAuth
After the upgrade the guest secret is retired; the secret on the device is no longer useful.