Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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?

  1. 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.
  2. 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.
  3. If the secret arrives for the first time, a new guest player (display_name Guest) 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).
  4. 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.

StatusMeaning
400The bootstrap_token format is broken (not 64 lowercase hexadecimal characters) or the JSON is broken
401x-crx-env is missing or not a UUID
403The secret was retired/expired, the account was deleted, or a CSRF/origin rejection (plain text); JSON for a banned owner {"code":"account_banned"}
42930 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):

OptionBehavior
DefaultlocalStorage in the browser (memory if it is inaccessible); memory in SSR/Node
falseNothing is stored; every signInGuest creates a new guest
GuestBootstrapStoreYour 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 with isGuest: false. A logout from 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/origin 403 and 401 do not count), the SDK deletes the secret once and makes one retry with a new secret.
  • In the banned state there is no retry; no new guest is created.

Warning

The default localStorage store 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, use guestBootstrap: false or 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:

After the upgrade the guest secret is retired; the secret on the device is no longer useful.