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.

The flow #

text
 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`
  │ ◄──────────────────────────────────────────── │
  1. Your game calls carver.getIdentity().

  2. The shell mints an RS256 JWT scoped to your game from the signed-in session and posts it back to your iframe.

  3. Your game sends the token to your backend.

  4. Your backend verifies the token against the marketplace JWKS and reads the player id from it.

In the game #

ts
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)TypeMeaning
oktrueToken was issued.
tokenstringA short-lived RS256 JWT. Send to your backend; verify there.
userIdstringStable, opaque id for this player in this game (the token's sub).
expiresAtnumberExpiry as a Unix epoch in milliseconds.
reason (on failure)Meaning
signin-requiredNo real marketplace account is signed in (the visitor is signed out or anonymous). Prompt them to sign in.
not-embeddedNot running inside the marketplace shell (e.g. your local dev build).
timeoutThe shell did not answer within 10 seconds.
rate-limitedToo many identity requests in a short window. Back off and retry.
errorAny 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):

text
https://www.carverjs.dev/.well-known/jwks.json

Here is a Node example using jose:

ts
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 #

ClaimValue
isshttps://www.carverjs.dev
subStable, opaque per-(player, game) id — the same value as userId.
audYour gameId.
gameIdYour gameId (same as aud, for convenience).
iat / expIssued-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.

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.

ts
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.