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_verifiedasfalse. - 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.
| Status | Meaning |
|---|---|
400 | Email format or password policy violation |
409 | The address is registered in this environment (the body does not repeat the address) |
429 | 10 / 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
403and{"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.
| Status | Meaning |
|---|---|
400 | Email, password, or name validation |
401 | Environment header or session missing |
403 | The session is not active, belongs to another environment, or a CSRF rejection |
409 | The address is registered to another account in this environment; the operation is rolled back and the guest session is not damaged |
422 | The player is no longer a guest (a duplicate or concurrent request) |
429 | IP 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
- A reset email is requested with
POST /v1/auth/forgot-password. Whether or not the account exists, the response is the same200(account existence does not leak). POST /v1/auth/reset-passwordis 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 noGETendpoint on this path; that is, clicking the link does not show a page. To offer reset in your game, the player must carry thetokenvalue 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 });
}
| TypeScript | Unity | Endpoint |
|---|---|---|
signUpEmail | SignUpEmailAsync | sign-up/email |
signInEmail | SignInEmailAsync | sign-in/email |
upgradeGuest | UpgradeGuestAsync | guest/upgrade/email |
changePassword({ currentPassword, newPassword }) | ChangePasswordAsync | change-password (old_password in the body) |
requestPasswordReset | RequestPasswordResetAsync | forgot-password |
resetPassword | ResetPasswordAsync | reset-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 newx-csrf-tokenin 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
UnknownOutcomeerror. Calls that return no result, such as password change and reset, are also not blindly retried. upgradeGuestsends no network request when there is no session; the server’s definitive rejections (4xx) do not drop the guest session.- On success,
changePasswordapplies the same transition aslogout(the state becomessignedOut).
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);
}
Magic link
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
verifyendpoint also requires thex-crx-envheader. In a browser, clicking the link in the email directly does not send this header and gets401. 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.