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

Email and password

Email and password give a player a permanent account across devices. On this page you will find the sign-up, sign-in, guest-to-permanent upgrade, password change, and reset flows, with request and response examples and SDK calls. All calls follow the shared rules (environment header, CSRF).

Rules

  • The email is trimmed and lowercased; it must contain exactly one @ and a dot in the domain part. An address can belong to only one account in the same environment.
  • The password is 8–128 characters (counted in Unicode characters, not bytes). The password is stored with Argon2id.
  • The display name is required; use 1–100 characters (the server enforces this limit on guest upgrade). The player can later change their name under the 2–24 character rule (Guest sign-in › Display name).
  • Today there is no email verification endpoint; sign-up returns email_verified as false.
  • The SDKs do a plain bounds check before sending the request (email 3–320, password at most 1024, display name at most 256 units; no control characters). The real policy is on the server.

Sign-up

POST /v1/auth/sign-up/email
content-type: application/json
x-crx-env: <environment-id>
x-csrf-token: <csrf-token>

{"email":"[email protected]","password":"<at least 8 characters>","display_name":"Alex"}

201 and the shared sign-in envelope (is_new_player: true, player.is_guest: false, player.email filled) + session cookies.

StatusMeaning
400Email format or password policy violation
409The address is registered in this environment (the body does not repeat the address)
42910 / 60 s per IP or 5 / 15 min per address exceeded

Sign-in

POST /v1/auth/sign-in/email
content-type: application/json
x-crx-env: <environment-id>
x-csrf-token: <csrf-token>

{"email":"[email protected]","password":"<password>"}

200 and the sign-in envelope (is_new_player: false).

  • A wrong password and an unknown address get the same response (plain-text 403); which one it was does not leak.
  • An account with the correct password but banned gets 403 and {"code":"account_banned","message":"account banned"}; the cookie is not deleted.
  • 10 failed sign-ins in 15 minutes for the same address + IP lock the account; while locked, even the correct password gets 429. A successful sign-in refunds the allowance. There is also a separate 5 / 60 s limit per IP.
  • If the request carries a usable session of another player (for example an old guest in the same browser), that session family is revoked after the sign-in succeeds. The same player’s session is not touched.

Guest upgrade

Turns a guest player into a permanent account with the same player.id; progress is kept. A guest session is required (a cookie or Authorization: Bearer <session-token>).

POST /v1/auth/guest/upgrade/email
content-type: application/json
x-crx-env: <environment-id>
x-csrf-token: <csrf-token>

{"email":"[email protected]","password":"<password>","display_name":"Alex"}

200; the same player.id, is_guest: false, only a new session pair. The upgrade is a single database transaction: the identity is added, the guest secret is retired, old sessions are revoked, and a new session is opened.

StatusMeaning
400Email, password, or name validation
401Environment header or session missing
403The session is not active, belongs to another environment, or a CSRF rejection
409The address is registered to another account in this environment; the operation is rolled back and the guest session is not damaged
422The player is no longer a guest (a duplicate or concurrent request)
429IP 10 / 60 s, guest player 30 / 60 s, or address 5 / 15 min exceeded

If the response is lost (the connection dropped), the old guest sessions may have been revoked; the recovery path is a normal email sign-in.

Password change

A session is required. The old password is verified; on success all sessions (including this one) are revoked and the cookies are deleted; the player signs in again.

POST /v1/auth/change-password
content-type: application/json
x-crx-env: <environment-id>
x-csrf-token: <csrf-token>

{"old_password":"<current>","new_password":"<new>"}

204. Errors: 400 new password policy violation, 403 session or old password rejected, 429 IP 10 / 60 s or account 6 / 60 s. A wrong current password also counts toward the sign-in lock counter.

Password reset

  1. A reset email is requested with POST /v1/auth/forgot-password. Whether or not the account exists, the response is the same 200 (account existence does not leak).
  2. POST /v1/auth/reset-password is called with the token from the email. The token is single-use and valid for 1 hour. On success all sessions are revoked.
POST /v1/auth/forgot-password
content-type: application/json
x-crx-env: <environment-id>
x-csrf-token: <csrf-token>

{"email":"[email protected]"}
POST /v1/auth/reset-password
content-type: application/json
x-crx-env: <environment-id>
x-csrf-token: <csrf-token>

{"token":"<64 hex>","new_password":"<new>"}

reset-password errors: 400 new password policy violation (the token is not burned, you can retry with the same link), 403 token invalid/used/expired or CSRF (the distinction does not leak), 429 limit exceeded (5 / 60 s, the IP bucket shared with forgot and magic link).

Warning

Known limitation: The link in the reset email goes not to your game’s page but to /v1/auth/reset-password?token=… at the server’s API root. There is no GET endpoint on this path; that is, clicking the link does not show a page. To offer reset in your game, the player must carry the token value from the email to your own reset screen (for example by routing this path to a game page on your own reverse proxy; this routing is not verified in this repository). Email delivery depends on the operator’s SMTP setting.

SDK

import { CrxError, ErrorKind, type PlayerClient } from "@ferdiunal/open-play-ts";

/** Yeni hesap açar ve oturumu başlatır. Adres kayıtlıysa sunucu 409 döner (`ErrorKind.Http`, status 409). */
export async function register(client: PlayerClient, email: string, password: string, displayName: string): Promise<void> {
  await client.session.signUpEmail({ email, password, displayName });
}

/** Mevcut hesapla girer. Yanlış parola ile bilinmeyen adres aynı yanıtı alır; hangisi olduğu sızmaz. */
export async function login(client: PlayerClient, email: string, password: string): Promise<void> {
  await client.session.signInEmail({ email, password });
}

/**
 * Misafir ilerlemesini koruyarak kalıcı hesaba geçer: aynı `playerId`, yeni oturum.
 * Önce misafir olarak girilmiş olmalıdır; adres başka bir hesapta kayıtlıysa 409 döner ve misafir oturumu bozulmaz.
 */
export async function upgradeGuestToEmail(client: PlayerClient, email: string, password: string, displayName: string): Promise<void> {
  if (client.session.state.status !== "authenticated") await client.session.signInGuest();
  try {
    await client.session.upgradeGuest({ email, password, displayName });
  } catch (error) {
    if (error instanceof CrxError && error.kind === ErrorKind.Http && error.status === 409) {
      throw new Error("Bu e-posta adresi başka bir hesapta kayıtlı.");
    }
    throw error;
  }
}

/** Parola sıfırlama isteği: hesap olsa da olmasa da yanıt aynıdır. */
export async function requestReset(client: PlayerClient, email: string): Promise<void> {
  await client.session.requestPasswordReset({ email });
}

/** E-postadaki token ile yeni parola belirler; başarıda tüm oturumlar kapanır, oyuncu yeniden giriş yapar. */
export async function finishReset(client: PlayerClient, token: string, newPassword: string): Promise<void> {
  await client.session.resetPassword({ token, newPassword });
}

/** Oturum açıkken parola değiştirir; başarıda tüm oturumlar (bu dahil) kapanır. */
export async function changePassword(client: PlayerClient, currentPassword: string, newPassword: string): Promise<void> {
  await client.session.changePassword({ currentPassword, newPassword });
}
TypeScriptUnityEndpoint
signUpEmailSignUpEmailAsyncsign-up/email
signInEmailSignInEmailAsyncsign-in/email
upgradeGuestUpgradeGuestAsyncguest/upgrade/email
changePassword({ currentPassword, newPassword })ChangePasswordAsyncchange-password (old_password in the body)
requestPasswordResetRequestPasswordResetAsyncforgot-password
resetPasswordResetPasswordAsyncreset-password

Shared SDK behavior:

  • The password and token are written only to the request body; they do not enter session state, event listeners, error objects, or logs.
  • On the auth endpoints, only if the CSRF layer rejected a 403 (there is no new x-csrf-token in the response) does the SDK retry once with a fresh token.
  • If the connection is lost after the request was sent, the sign-in endpoints make at most one session probe; if the outcome is uncertain, you get an UnknownOutcome error. Calls that return no result, such as password change and reset, are also not blindly retried.
  • upgradeGuest sends no network request when there is no session; the server’s definitive rejections (4xx) do not drop the guest session.
  • On success, changePassword applies the same transition as logout (the state becomes signedOut).

Unity example:

using System.Threading.Tasks;
using OpenPlay;
using OpenPlay.Session;
using OpenPlay.Support;

/// <summary>E-posta ve parola akışları. Parola SecretText ile taşınır; günlüğe ve ToString()'e girmez.</summary>
public static class EmailAuth
{
    public static Task<SessionState> Register(OpenPlayClient client, string email, string password, string displayName) =>
        client.Session.SignUpEmailAsync(new SignUpEmailInput { Email = email, Password = new SecretText(password), DisplayName = displayName });

    public static Task<SessionState> Login(OpenPlayClient client, string email, string password) =>
        client.Session.SignInEmailAsync(new SignInEmailInput { Email = email, Password = new SecretText(password) });

    /// <summary>Misafir ilerlemesini korur: aynı oyuncu, yeni oturum. Adres kayıtlıysa HTTP 409 gelir.</summary>
    public static async Task<SessionState> UpgradeGuest(OpenPlayClient client, string email, string password, string displayName)
    {
        try
        {
            return await client.Session.UpgradeGuestAsync(new UpgradeGuestInput { Email = email, Password = new SecretText(password), DisplayName = displayName });
        }
        catch (OpenPlayException ex) when (ex.Kind == ErrorKind.Http && ex.Status == 409)
        {
            throw new System.InvalidOperationException("Bu e-posta adresi başka bir hesapta kayıtlı.");
        }
    }

    public static Task RequestReset(OpenPlayClient client, string email) =>
        client.Session.RequestPasswordResetAsync(new PasswordResetRequestInput { Email = email });

    /// <summary>E-postadaki token ile yeni parola; başarıda tüm oturumlar kapanır.</summary>
    public static Task FinishReset(OpenPlayClient client, string token, string newPassword) =>
        client.Session.ResetPasswordAsync(new ResetPasswordInput { Token = new SecretText(token), NewPassword = new SecretText(newPassword) });

    /// <summary>Görünen ad kuralı: normalize sonrası 2..24 karakter, saatte 10 değişiklik.</summary>
    public static Task<SessionState> Rename(OpenPlayClient client, string displayName) =>
        client.Session.RenameAsync(displayName);
}

A magic link sends a single-use link to the email. The server endpoints exist, there is no SDK helper yet, and it is not documented as an in-game flow.

POST /v1/auth/sign-in/magic-link
content-type: application/json
x-crx-env: <environment-id>
x-csrf-token: <csrf-token>

{"email":"[email protected]","name":"Alex"}

200 (email existence does not leak); the token is only in the email, is valid for 5 minutes, and is single-use. Limit: IP 5 / 60 s, address 5 / 15 min. The token is consumed with GET /v1/auth/magic-link/verify?token=<token>; on success 200, the sign-in envelope, and session cookies are returned, and an invalid token gives 403.

Warning

The verify endpoint also requires the x-crx-env header. In a browser, clicking the link in the email directly does not send this header and gets 401. If you want to build a magic link flow, your game page must take the token and verify it with a request that carries the header; this flow is not covered by the SDKs or by this document.