Player
User identification
On the Jest platform, each user has a unique player ID per app. This identifier remains the same if a player starts as a guest and later registers, allowing you to track user progress and store user data.
Guest users can be prompted to register or sign in on the Jest platform. See Platform login for more information.
Registered users can be re-engaged via notifications. See Notifications for more information.
Get the player
Read the current player's ID and whether they are registered.
- HTML5
- Unity
JestSDK.getPlayer()
JestSDK.getPlayer(): {
playerId: string;
registered: boolean;
username: string | null;
avatarUrl: string | null;
};
Example:
const player = JestSDK.getPlayer();
if (!player.registered) {
// Guest player: notifications cannot be scheduled
// Consider prompting the player to register
} else {
JestSDK.notifications.scheduleNotification({
/*...*/
});
}
Player.id
var player = JestSDK.Instance.Player;
string playerId = player.id;
Player.isRegistered
var player = JestSDK.Instance.Player;
if (!player.isRegistered)
{
// Guest player: notifications cannot be scheduled
// Consider prompting the player to register
}
else
{
// Registered player: full functionality available
await JestSDK.Instance.RichNotifications.ScheduleNotification(new RichNotifications.Options
{
// ...
});
}
Get a signed player payload
If your app has a backend, use this method to obtain a signed payload that can be sent to your server to authenticate the user and attach to authenticated requests.
This works for both registered users and guests. For guests, registered is false.
- HTML5
- Unity
JestSDK.getPlayerSigned()
JestSDK.getPlayerSigned(): Promise<{
player: {
playerId: string;
registered: boolean;
username: string | null;
avatarUrl: string | null;
};
playerSigned: string; // JWS with the same `player` object plus iat, aud, sub
}>;
Player.GetSigned()
var player = JestSDK.Instance.Player;
var signedTask = player.GetSigned();
await signedTask;
if (signedTask.IsCompleted)
{
var response = signedTask.Result;
string playerId = response.player.playerId;
bool isRegistered = response.player.registered;
string signedPayload = response.playerSigned; // JWS format for server verification
}
The returned playerSigned value is a JSON Web Signature (JWS) signed using your app’s shared secret with the HS-256 algorithm. You can use any standard JWS/JWT library on your backend to verify the signature and extract the user information.
The shared secret shown in the Developer Console is base64-encoded. Most JWT libraries expect the decoded key when verifying the signature.
The signed payload includes an issued-at (iat) timestamp that can be used to verify token freshness. Jest does not set an explicit expiration time; you may reject tokens older than a chosen threshold and have the client request a new token.
The verified payload has this shape:
type PlayerSignedPayload = {
player: {
playerId: string;
registered: boolean;
username: string | null;
avatarUrl: string | null;
};
iat: number;
aud: string; // game id
sub: string; // player id
};
Examples for popular backend languages, including signature validation and freshness checks (for example, rejecting tokens older than 24 hours), are provided below.
Example server code
- Node.js
- PHP
- Ruby
- Python
- Java
- C#
// npm i jsonwebtoken
import jwt from "jsonwebtoken";
export function verifyPlayerSigned(token: string) {
const secretBase64 = process.env.JWS_SECRET!;
const secret = Buffer.from(secretBase64, "base64");
try {
// IMPORTANT: lock the allowed algs
const payload = jwt.verify(token, secret, {
algorithms: ["HS256"],
}) as jwt.JwtPayload;
const iat = payload.iat as number;
const maxAgeSeconds = 24 * 60 * 60;
const nowSeconds = Math.floor(Date.now() / 1000);
if (nowSeconds - iat > maxAgeSeconds) {
throw new Error("Token too old");
}
const player = payload.player as
| {
playerId: string;
registered: boolean;
}
| undefined;
if (!player) {
throw new Error("Missing player payload");
}
console.log("playerId:", player.playerId);
console.log("registered:", player.registered);
return payload;
} catch (err) {
console.error("Invalid token:", err);
throw err;
}
}
<?php
// composer require firebase/php-jwt
require "vendor/autoload.php";
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
function verifyPlayerSigned(string $token): object
{
$secretBase64 = getenv("JWS_SECRET");
if ($secretBase64 === false) {
throw new RuntimeException("Missing JWS_SECRET");
}
$key = base64_decode($secretBase64, true);
if ($key === false) {
throw new RuntimeException("Invalid base64 in JWS_SECRET");
}
$payload = JWT::decode($token, new Key($key, "HS256"));
$iat = $payload->iat;
$maxAgeSeconds = 24 * 60 * 60;
if (time() - (int)$iat > $maxAgeSeconds) {
throw new RuntimeException("Token too old");
}
if (!isset($payload->player) || !is_object($payload->player)) {
throw new RuntimeException("Missing player payload");
}
echo "playerId: {$payload->player->playerId}\n";
echo "registered: " . ($payload->player->registered ? "true" : "false") . "\n";
return $payload;
}
# gem install jwt
require "jwt"
require "base64"
def verify_player_signed(token)
secret_base64 = ENV.fetch("JWS_SECRET")
secret = Base64.decode64(secret_base64)
begin
payload, _header = JWT.decode(token, secret, true, { algorithm: "HS256" })
iat = payload["iat"]
max_age_seconds = 24 * 60 * 60
if (Time.now.to_i - iat) > max_age_seconds
raise "Token too old"
end
player = payload["player"]
raise "Missing player payload" unless player.is_a?(Hash)
puts "playerId: #{player['playerId']}"
puts "registered: #{player['registered']}"
payload
rescue JWT::DecodeError => e
warn "Invalid token: #{e.message}"
raise
end
end
# pip install PyJWT
import os
import base64
import jwt
import time
def verify_player_signed(token: str) -> dict:
secret_b64 = os.environ["JWS_SECRET"]
secret = base64.b64decode(secret_b64)
try:
payload = jwt.decode(token, secret, algorithms=["HS256"])
iat = payload["iat"]
max_age_seconds = 24 * 60 * 60
now_seconds = int(time.time())
if now_seconds - int(iat) > max_age_seconds:
raise ValueError("Token too old")
player = payload.get("player")
if not isinstance(player, dict):
raise ValueError("Missing player payload")
print("playerId:", player.get("playerId"))
print("registered:", player.get("registered"))
return payload
except jwt.InvalidTokenError as e:
print("Invalid token:", str(e))
raise
// JJWT (io.jsonwebtoken). Add jjwt-api + a runtime impl (jjwt-impl + jjwt-jackson).
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.util.Base64;
import java.util.Date;
public class VerifyJwsHs256 {
public static Claims verifyPlayerSigned(String token) {
String secretBase64 = System.getenv("JWS_SECRET");
if (secretBase64 == null || secretBase64.isEmpty()) {
throw new IllegalStateException("Missing JWS_SECRET");
}
byte[] keyBytes = Base64.getDecoder().decode(secretBase64);
SecretKey key = Keys.hmacShaKeyFor(keyBytes);
try {
Jws<Claims> jws = Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token);
Date issuedAt = jws.getPayload().getIssuedAt();
long maxAgeSeconds = 24 * 60 * 60;
long ageSeconds = (System.currentTimeMillis() - issuedAt.getTime()) / 1000;
if (ageSeconds > maxAgeSeconds) {
throw new IllegalArgumentException("Token too old");
}
Claims player = jws.getPayload().get("player", Claims.class);
if (player == null) {
throw new IllegalArgumentException("Missing player payload");
}
String playerId = player.get("playerId", String.class);
Boolean registered = player.get("registered", Boolean.class);
System.out.println("playerId: " + playerId);
System.out.println("registered: " + registered);
return jws.getPayload();
} catch (Exception e) {
System.out.println("Invalid token: " + e.getMessage());
throw e;
}
}
}
// NuGet: System.IdentityModel.Tokens.Jwt
using System;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
class VerifyJwsHs256
{
static JwtPayload VerifyPlayerSigned(string token)
{
var secret = Environment.GetEnvironmentVariable("JWS_SECRET");
if (string.IsNullOrWhiteSpace(secret))
{
throw new InvalidOperationException("Missing JWS_SECRET");
}
var key = new SymmetricSecurityKey(Convert.FromBase64String(secret));
var handler = new JwtSecurityTokenHandler();
var parameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
// set these to true + configure ValidIssuer/ValidAudience in real apps
ValidateIssuer = false,
ValidateAudience = false,
RequireExpirationTime = false,
ValidateLifetime = false,
ClockSkew = TimeSpan.FromMinutes(1),
// IMPORTANT: lock the allowed algs
ValidAlgorithms = new[] { SecurityAlgorithms.HmacSha256 }
};
try
{
var principal = handler.ValidateToken(token, parameters, out var validatedToken);
var jwt = (JwtSecurityToken)validatedToken;
var iat = Convert.ToInt64(jwt.Payload["iat"]);
var maxAgeSeconds = 24 * 60 * 60;
var ageSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - iat;
if (ageSeconds > maxAgeSeconds)
{
throw new InvalidOperationException("Token too old");
}
var player = jwt.Payload["player"] as IDictionary<string, object>;
if (player is null)
{
throw new InvalidOperationException("Missing player payload");
}
Console.WriteLine($"playerId: {player["playerId"]}");
Console.WriteLine($"registered: {player["registered"]}");
return jwt.Payload;
}
catch (Exception e)
{
Console.WriteLine("Invalid token: " + e.Message);
throw;
}
}
}
User data
If your app does not have a backend, Jest provides a simple key-value store for per-user data.
User data is stored alongside the user record on the Jest platform and is persistent across sessions and devices. All values must be serializable to JSON.
User data is written directly from the app client and must not be used to store sensitive information or data that requires strong security guarantees.
User data is limited to 1 MB per app; if this limit is exceeded, further writes will fail until the stored data size is reduced.
Get all data
Returns a snapshot of all key-value data stored for the current user.
- HTML5
- Unity
JestSDK.data.getAll()
JestSDK.data.getAll(): { [key: string]: unknown };
Example:
const playerData = JestSDK.data.getAll();
console.log("Player data:", playerData);
// This returns a snapshot; modifying it does not update stored data.
playerData.coins = 1000; // does not affect stored player data
Not available in the Unity SDK yet. Read individual keys with Player.Get<T>(key) or Player.TryGet<T>(key, out value) instead.
Get a value
Returns the value stored for the given key in the user data.
- HTML5
- Unity
JestSDK.data.get(key)
Returns undefined if the key does not exist.
JestSDK.data.get(key: string): unknown;
Example:
const coins = JestSDK.data.get("coins");
console.log("Player coins:", coins);
Player.Get<T>(key)
Retrieves and converts a value of the specified type associated with the given key.
var player = JestSDK.Instance.Player;
// Get primitive values
int level = player.Get<int>("level");
string nickname = player.Get<string>("nickname");
// Get custom struct
public struct PlayerStats
{
public int wins;
public int losses;
public float winRate;
}
PlayerStats stats = player.Get<PlayerStats>("stats");
Player.TryGet<T>(key, out value)
Attempts to retrieve and convert a value. Returns true if the key exists, false otherwise.
var player = JestSDK.Instance.Player;
if (player.TryGet<int>("coins", out int coins))
{
Debug.Log($"Player has {coins} coins");
}
else
{
Debug.Log("No coins data found");
}
Set a value
Sets the value for the given key in the user data.
- HTML5
- Unity
JestSDK.data.set(key, value)
JestSDK.data.set(key: string, value: unknown): void;
Example:
JestSDK.data.set("coins", 750);
JestSDK.data.set(data)
Batch form: merges the provided key-value pairs into the user data, setting multiple keys at once.
This method performs a shallow merge and does not replace existing data. To remove a key, use JestSDK.data.delete(key) or set its value to undefined.
JestSDK.data.set(data: { [key: string]: unknown }): void;
Example:
JestSDK.data.set({ coins: 500, level: 2 });
Player.Set<T>(key, value)
Sets a value of the specified type for the given key.
var player = JestSDK.Instance.Player;
// Set primitive values
player.Set("level", 5);
player.Set("lastLogin", DateTime.Now);
player.Set("inventory", "sword");
// Set custom struct
player.Set("stats", new PlayerStats
{
wins = 10,
losses = 2,
winRate = 0.83f
});
Delete a value
Deletes the given key from the user data.
- HTML5
- Unity
JestSDK.data.delete(key)
JestSDK.data.delete(key: string): void;
Example:
JestSDK.data.delete("temporaryBoost");
Player.Delete(key)
var player = JestSDK.Instance.Player;
player.Delete("temporaryBoost");
Flush pending writes
Waits for the platform to acknowledge the user data written so far. Set and delete send their update as soon as they are called, unless an earlier update is still unacknowledged — in that case the SDK coalesces the queued changes into the next message. Flushing completes immediately when nothing is outstanding. The acknowledgement reports that the platform handled the update, not that it stored it: an update the platform rejects because another session advanced the user's state is acknowledged as well.
- HTML5
- Unity
JestSDK.data.flush()
JestSDK.data.flush(): Promise<void>;
Example:
// Update some data.
JestSDK.data.set("score", 1500);
await JestSDK.data.flush();
console.log("Player data update acknowledged by the platform.");
Player.Flush()
var player = JestSDK.Instance.Player;
// Update some data
player.Set("score", 1500);
await player.Flush();
Debug.Log("Player data update acknowledged by the platform.");