API Reference
Every method on the carver object, with its signature and when to call it. Import the singleton once and use it anywhere:
import { carver } from "@carverjs/embed-sdk";
// or: import carver from "@carverjs/embed-sdk"; // default export, same objectAll methods are safe no-ops outside the marketplace iframe and never throw.
Overview #
| Method | Signature | When to call it |
|---|---|---|
ready | () => void | First frame rendered. Required — the shell loader waits for it. |
progress | (percent: number) => void | Loading progress, clamped to 0–100. |
score | (value: number, label?: string) => void | Report a score (finite numbers only). |
event | (name: string, payload?: unknown) => void | Gameplay events for stat aggregation. |
error | (code: string, message: string) => void | Fatal error — the shell shows an error card. |
requestFullscreen | () => void | Ask the shell to go fullscreen (needs a user gesture). |
exit | () => void | Game finished; hand control back to the shell. |
requestInit | () => void | v2 — ask the shell for your runtime configuration. Call it after onInit. |
onInit | (handler) => () => void | v2 — receive carver:init and every later carver:ice. Returns an unsubscribe function. |
telemetry | (tuple: CarverTelemetryTuple) => void | v2 — report one learning interaction on the closed tuple. |
iceExpired | () => void | v2 — current ICE credentials stopped working; ask for fresh ones. |
getIdentity | () => Promise<CarverIdentity> | Signed token proving which player this is. See Player Identity. |
subscribe | (handler) => () => void | Listen for shell → game messages. Returns an unsubscribe function. |
configure | (config) => void | Optional origin pinning. |
isEmbedded | () => boolean | true when running inside the marketplace shell. |
ready #
carver.ready(): voidSignals that the game has booted and rendered its first frame. The shell hides its loader and begins counting the play. Call this exactly once per boot, as early as the first frame is actually visible.
startRenderLoop();
carver.ready();progress(percent) #
carver.progress(percent: number): voidReports load or level progress. percent is clamped to [0, 100]; non-finite values become 0. Drives the numeric readout inside the shell loader.
await loadAssets((pct) => carver.progress(pct));score(value, label) #
carver.score(value: number, label?: string): voidReports a score for the player-profile stats. value must be a finite number — NaN and ±Infinity are dropped by the shell. label is an optional display unit (e.g. "coins", "points"); the shell truncates it at 64 characters.
carver.score(1280, "points");
carver.score(coins, "coins");event(name, payload) #
carver.event(name: string, payload?: unknown): voidEmits an arbitrary gameplay event for play-stat aggregation. name is rejected by the shell if it exceeds 128 characters. payload is any JSON-cloneable value (passed by reference into postMessage).
carver.event("level-complete", { level: 3, timeMs: 48210 });
carver.event("boss-defeated");Treat events as fire-and-forget telemetry. A non-cloneable payload (functions, DOM nodes, class instances) is silently dropped — pass plain data.
error(code, message) #
carver.error(code: string, message: string): voidReports a fatal error. The shell replaces the game iframe with an error card showing the code and message. Use a short machine-readable code (truncated at 64 chars) and a human-readable message (truncated at 500 chars).
carver.error("asset-load-failed", "texture atlas returned 404");requestFullscreen() #
carver.requestFullscreen(): voidAsks the shell to take the game fullscreen on its behalf. Call it from inside a user-gesture handler (click, key, tap) — browsers refuse fullscreen requests without a recent user activation. The game can also call the native element.requestFullscreen() itself; both paths are supported.
Since 1.1.0 the message carries an optional v: 2. Nothing else changed, and an old shell's parser ignores the extra field.
button.addEventListener("click", () => carver.requestFullscreen());exit() #
carver.exit(): voidSignals that the game is finished. The shell may navigate back or show an end card. Use it on game-over or when the player chooses to quit.
Since 1.1.0 the message carries an optional v: 2, exactly as carver:request-fullscreen does. An old shell's parser ignores the extra field.
requestInit() #
carver.requestInit(): voidAsks the shell to send (or re-send) carver:init — the runtime configuration you would otherwise bake into your bundle: a session token, a locale, a consent flag, TURN credentials, a multiplayer room. Subscribe with onInit first: a shell may answer synchronously, and a listener installed afterwards misses its own reply.
Calling it again is safe. Shells treat a repeat as a re-send, never an error, so a retry after a silence is fine.
const off = carver.onInit(applyConfig);
carver.requestInit(); // subscribe first, then askSome shells push carver:init unprompted at load, so subscribing early covers both. Everything the message can carry: Protocol v2.
onInit(handler) #
carver.onInit(
handler: (message: CarverInitMessage | CarverIceMessage) => void,
): () => voidSubscribes to runtime configuration and returns an unsubscribe function. The handler receives carver:init, and then every later carver:ice refresh — TURN credentials expire, so keep the handler installed for the whole session rather than dropping it after the first message.
onInit sends carver:init-ack for you on each carver:init, so the shell can prove your game actually took its configuration. It does not send carver:init-request; that is requestInit(), which you call after subscribing.
const off = carver.onInit((msg) => {
if (msg.type === "carver:init") boot(msg.payload);
else refreshIce(msg.payload.iceServers); // carver:ice
});
carver.requestInit();carver:init chooses your signaling backend, your TURN servers and your API base URL, and by default anyone who iframes your game can send one. See configure(config).
Every field of the payload, and how a shell is expected to behave: Protocol v2.
telemetry(tuple) #
carver.telemetry(tuple: CarverTelemetryTuple): voidReports one learning interaction. The tuple is the whole type:
type CarverTelemetryTuple = {
objectId: string;
kcCode: string | null;
success: boolean;
attempts: number;
hintsUsed: number;
latencyBucket: "lt5s" | "lt15s" | "lt60s" | "gte60s";
misconceptions?: string[];
probeItemId?: number;
};It is closed on purpose, because the closed type is the privacy whitelist. There is no free-form field, so a game cannot attach a name, a device id, a raw timestamp or a session token to a learning event — there is nowhere to put one.
The eight keys are also picked out by hand at runtime, not forwarded. TypeScript's excess-property check fires only on a fresh object literal at the call site, so carver.telemetry(someVariable) would otherwise ship every extra property that variable happens to carry. A compile-time whitelist is not a whitelist when a variable defeats it.
carver.telemetry({
objectId: "task-3",
kcCode: "NCERT.G6.FRAC.EQUIV",
success: true,
attempts: 2,
hintsUsed: 1,
latencyBucket: "lt15s",
});carver:telemetry is the only message a shell may relay to a learning-telemetry endpoint. event() stays free-form for play stats and must never leave the shell.
Why latency is bucketed and what each field maps to: Protocol v2.
iceExpired() #
carver.iceExpired(): voidTells the shell your current ICE credentials stopped working — a TURN allocation refused, or ice.expiresAt already passed — and asks for a fresh carver:ice, which arrives on your onInit handler. It carries no credential material, only the fact.
peerConnection.addEventListener("icecandidateerror", () => {
carver.iceExpired();
});The full refresh cycle, including the unprompted push before expiresAt: Protocol v2.
getIdentity() #
carver.getIdentity(): Promise<CarverIdentity>Requests a short-lived signed token identifying the signed-in marketplace player, so your own backend can tie its data to that player. Always resolves — never rejects — with a discriminated union:
type CarverIdentity =
| { ok: true; token: string; userId: string; expiresAt: number }
| { ok: false; reason:
| "signin-required" | "not-embedded" | "timeout" | "rate-limited" | "error" };Full walkthrough, including how to verify the token on your server: Player Identity.
const id = await carver.getIdentity();
if (id.ok) {
await saveToMyBackend(id.token, id.userId);
} else if (id.reason === "signin-required") {
showSignInPrompt();
}subscribe(handler) #
carver.subscribe(handler: (message: CarverInboundMessage) => void): () => voidListens for shell → game messages and returns an unsubscribe function. Only messages whose source is the direct parent window are delivered — plus, in a native WebView, whatever the shell hands to window.__carverShellDeliver.
Since 1.1.0 the shell does emit inbound messages: carver:init and carver:ice carry runtime configuration, carver:pause and carver:resume tell you the shell backgrounded or foregrounded the game. Configuration and ICE are easier to take through onInit, which types them and ACKs for you; pause and resume arrive here. Unknown type values are still no-ops, so a message kind added later never breaks an existing handler. The full list: Protocol v2.
const unsubscribe = carver.subscribe((msg) => {
if (msg.type === "carver:pause") pauseLoop();
});
// later, on teardown
unsubscribe();configure(config) #
carver.configure(config: { targetOrigin?: string; parentOrigin?: string }): voidOrigin pinning, in both directions. Call once at boot, before subscribe.
Outbound, targetOrigin defaults to "*": those messages carry no secrets, and a sandboxed game cannot know which shell origin embeds it. Pinning it is optional tidiness for a game that only ever ships to one shell.
parentOrigin is not tidiness. The default inbound check is only source === window.parent — sound, since nothing but the real embedder can be that object, but it does not tell you who the embedder is. A published game is loadable by anyone, so anyone can iframe it, and by framing it their page is window.parent. They can then forge a carver:init and choose your signaling backend, your TURN servers and your API base URL. event.origin is set by the browser and cannot be forged, which is what makes parentOrigin a fence rather than a hint.
carver.configure({
targetOrigin: "https://carverjs.dev",
parentOrigin: "https://carverjs.dev",
});| Option | Default | Description |
|---|---|---|
targetOrigin | "*" | Target origin for outbound postMessage. |
parentOrigin | (unset) | When set, inbound messages must also come from this exact origin, on top of the always-on parent-window check. Any game that consumes carver:init must set it. |
The inbound channel now carries secrets — a session token, TURN credentials, a room signaling token — and without parentOrigin it trusts whoever framed you. Set it before you subscribe. See Protocol v2 and Security & Origins.
isEmbedded() #
carver.isEmbedded(): booleanReturns true when the game is running inside another frame (the marketplace shell) in a browser, false otherwise (top-level window, SSR, tests). Useful for branching dev-only UI:
if (!carver.isEmbedded()) {
showLocalDevBanner(); // running your build directly, not on the marketplace
}PROTOCOL_VERSION #
import { PROTOCOL_VERSION } from "@carverjs/embed-sdk"; // 2A named export, not a method on carver. It is the protocol revision this SDK speaks, and the SDK stamps it as v on every v2 message it sends, so a shell can tell a new game from an old one. Since 1.1.0 it also rides on carver:exit and carver:request-fullscreen, where it is optional and an older shell ignores it. The rest of v1 — carver:ready, progress, score, event, error — carries no v and never will.
You rarely need the number yourself — reach for it when your game reports its own build metadata, or logs which protocol it negotiated. Which messages carry it: Protocol v2.