Player Identity
If your game stores data on your own backend — cloud saves, leaderboards, inventories — carver.getIdentity() tells that backend which marketplace player it is talking to, without your game ever handling a marketplace credential.
The shell mints a short-lived, signed token from the player's signed-in session and hands it to your game. You forward it to your server, which verifies it against the marketplace's public keys and trusts the player id inside. No passwords, no OAuth dance, no marketplace API keys in your game.
You only need identity if you persist per-player data yourself. If your game is stateless, or stores everything in the browser's own per-game storage, you can ignore this page.
The flow #
game shell (carverjs.dev) your backend
│ getIdentity() │ │
│ ──────────────► │ mint token from session │
│ │ (signed, short-lived) │
│ ◄────────────── │ │
│ { token, … } │
│ fetch(/save, Authorization: Bearer token) ───►│
│ verify token vs JWKS, trust `sub`
│ ◄──────────────────────────────────────────── │Your game calls
carver.getIdentity().The shell mints an RS256 JWT scoped to your game from the signed-in session and posts it back to your iframe.
Your game sends the token to your backend.
Your backend verifies the token against the marketplace JWKS and reads the player id from it.
In the game #
import { carver } from "@carverjs/embed-sdk";
async function syncSave(progress: unknown) {
const id = await carver.getIdentity();
if (!id.ok) {
if (id.reason === "signin-required") promptSignIn();
return; // play continues; just don't sync
}
await fetch("https://my-game-backend.example/save", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${id.token}`,
},
body: JSON.stringify({ progress }),
});
}getIdentity() always resolves — it never throws and never rejects. On success you get the token and a player id; otherwise you get a reason you can branch on.
| Field (on success) | Type | Meaning |
|---|---|---|
ok | true | Token was issued. |
token | string | A short-lived RS256 JWT. Send to your backend; verify there. |
userId | string | Stable, opaque id for this player in this game (the token's sub). |
expiresAt | number | Expiry as a Unix epoch in milliseconds. |
reason (on failure) | Meaning |
|---|---|
signin-required | No real marketplace account is signed in (the visitor is signed out or anonymous). Prompt them to sign in. |
not-embedded | Not running inside the marketplace shell (e.g. your local dev build). |
timeout | The shell did not answer within 10 seconds. |
rate-limited | Too many identity requests in a short window. Back off and retry. |
error | Any other failure (network, shell error). |
Verifying the token on your backend #
The token is a standard RS256 JWT. Verify it with any JWT library, in any language, against the marketplace's public key set (JWKS):
https://www.carverjs.dev/.well-known/jwks.jsonHere is a Node example using jose:
import { jwtVerify, createRemoteJWKSet } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://www.carverjs.dev/.well-known/jwks.json"),
);
export async function verifyPlayer(token: string, myGameId: string) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: "https://www.carverjs.dev",
audience: myGameId, // also present as payload.gameId
});
return payload.sub as string; // === the game's `userId` — your stable key
}jose fetches and caches the JWKS for you, so you are not hitting the endpoint on every request. In other ecosystems, point your JWT library's JWKS/“well-known keys” option at the same URL.
Claims #
| Claim | Value |
|---|---|
iss | https://www.carverjs.dev |
sub | Stable, opaque per-(player, game) id — the same value as userId. |
aud | Your gameId. |
gameId | Your gameId (same as aud, for convenience). |
iat / exp | Issued-at / expiry, in seconds. Tokens are short-lived (minutes). |
Always check iss, aud (or gameId), and exp. Use sub as the primary key for the player's data in your store.
Never trust the token in the browser. A value the client can read, it can fake. Only your backend — which checks the signature against the JWKS — should decide who the player is. Treat getIdentity()'s userId in the game purely as a display/cache hint, not as an authorization decision.
Privacy: ids are per-game #
userId / sub is scoped to your game. The same person gets a different id in a different game, so ids cannot be used to track a player across the marketplace. Within your game, the id is stable across sessions and devices — it is the right key for that player's saves.
Requires a signed-in account #
Identity requires a real marketplace account. The play page signs anonymous visitors in transparently so they can play, but those sessions return signin-required from getIdentity() — there is no durable person to attest to. Handle it gracefully: let the game run, and prompt for sign-in only when the player does something that needs to persist.
const id = await carver.getIdentity();
if (!id.ok && id.reason === "signin-required") {
showToast("Sign in to save your progress across devices.");
}Token lifetime #
Tokens are intentionally short-lived. If you make long-lived backend calls, request a fresh token close to when you need it rather than holding one — getIdentity() is cheap to call again. expiresAt tells you when the current token stops being valid.