TypeScript SDK
Pure-fetch web/Node client for the Kraty platform — events, leaderboards, lobbies, grants, inventory, wallet, and per-player auth.
@kraty/sdk is the TypeScript / JavaScript client SDK — for
web games, JS-based mobile shells (React Native via the JS bridge),
and any TS/JS runtime that needs to consume the /sdk/v1 surface.
Auto-stamped idempotency keys on every write (preserved across
retries), exponential backoff with jitter, sealed error codes you
can switch on, Server-Sent-Events leaderboard streaming, and
adaptive polling helpers — so the common patterns are one line of
code.
Targets Node 18+ and any modern browser / runtime that ships
crypto.randomUUID() and fetch. Zero runtime dependencies.
This SDK is for game CLIENTS only. It does not expose the
/server/v1 (server_integration) or /admin/v1 surfaces — those
can mint currency, grant items, and rotate player secrets.
Server-side IAP fulfilment, manual grants, and admin tooling belong
on your own backend — use @kraty/server-sdk
(Node/TS) or the Python server SDK.
Embedding a server_integration key in a web bundle is a security
incident.
Install
The SDK isn't on npm yet — install directly from GitHub against a
tagged release. Each release ships compiled dist/ artefacts so
no build step is needed on your side.
# Pin to a specific version (recommended):
npm install github:PedroTrincheiras/kraty-sdk-typescript#v0.1.0
# or with pnpm:
pnpm add github:PedroTrincheiras/kraty-sdk-typescript#v0.1.0Browse releases at
github.com/PedroTrincheiras/kraty-sdk-typescript/releases.
Migrate to pnpm add @kraty/sdk once we publish to npm (planned
for v1.0).
Integration in three steps
-
Install with the command above.
-
Get a
client_sdkAPI key from the Kraty portal under your game → Settings → API Keys. -
Drop the SDK into your game code:
import { Kraty } from '@kraty/sdk'; const kraty = new Kraty({ apiKey: 'YOUR_CLIENT_SDK_KEY' }); const available = await kraty.events.listForPlayer();That's the whole bootstrap. The SDK auto-registers the player on the first call and reuses the identity on every subsequent one. See Authentication for the bring-your-own-id and device-link flows.
Authentication
Kraty uses two credentials in lock-step:
| Credential | Identifies | Where it lives |
|---|---|---|
SDK key (Authorization: Bearer …) | The game (studio + game + permission set) | Embedded in the web bundle, one per game/environment |
Player secret (X-Player-Secret: …) | The player | Generated server-side, persisted by the SDK across launches, attached to every player-scoped call |
Player-scoped routes (events.start, events.progress,
inventory.*, wallet.*, grants.*) require both — the SDK
key alone gets you public catalog data (events list, leaderboards,
lobby state). See the Authentication guide for the
full security model.
One-line setup
import { Kraty } from '@kraty/sdk';
const kraty = new Kraty({ apiKey: '<your-client-sdk-key>' });
// Every call below auto-resolves the player identity. The SDK
// generates one, persists it, and reuses it across launches.
const events = await kraty.events.listForPlayer();
await kraty.events.start(events[0].eventKey);That's the whole bootstrap. On the very first player-scoped call the SDK:
- Generates a
kp_<uuid>external player id. - Calls
POST /sdk/v1/players/:id/registerto mint the player secret. - Persists both to a platform-appropriate store (
localStoragein browsers / React Native, in-memory elsewhere — see Persistence backends). - Wires them onto the client.
Subsequent calls — in the same session and after a page reload — restore the persisted identity without a round-trip.
Bring your own player id
When your own auth system already minted an id (e.g. your account service maps Apple/Google IDs to internal user IDs), pass it in. The SDK still handles register + persistence:
const kraty = new Kraty({
apiKey: '<your-client-sdk-key>',
activeExternalPlayerId: 'player_42',
});
// First call registers `player_42` and persists the secret.
// All subsequent boots restore the same identity transparently.
await kraty.events.listForPlayer();Sign in with a server-issued secret
Device-link flow: a player taps "transfer my save", your backend
mints them a fresh secret server-side (via /server/v1/players/:p/secret/rotate),
and you hand the pair to the SDK to install + persist:
const secret = await myBackend.linkDevice(playerId);
await kraty.signIn({ externalPlayerId: playerId, secret });The next call uses the new identity. Use this instead of mutating storage manually.
Log out / switch player
await kraty.logout();
// Next player-scoped call lazily registers a fresh player.
// Or sign in as someone else immediately:
await kraty.signIn({
externalPlayerId: 'player_99',
secret: '<from your auth backend>',
});logout() wipes the persisted active id + secret. The next call
falls back through the same three-tier resolver (constructor id →
persisted id → fresh register).
Inspect the active identity
// Returns null until the first player-scoped call resolves
// (or you await ensureIdentity directly).
const id = kraty.activeExternalPlayerId;
// Force a resolve up-front (rare — usually unnecessary).
const { externalPlayerId } = await kraty.ensureIdentity();Persistence backends
The SDK picks a default SecretStore based on the runtime — game
code doesn't construct or pass one:
| Runtime | Default backend |
|---|---|
Browser / React Native (globalThis.localStorage) | LocalStorageSecretStore |
| Node / workers / SSR | InMemorySecretStore (volatile across process restarts) |
The Node case is rare for the client SDK — server-side fulfilment
belongs in @kraty/server-sdk, which has
its own auth model. If you do need a custom backend (e.g. an
encrypted file store on Node), the SecretStore interface lives in
src/secret-store.ts and accepts an instance via
KratyClientOptions.secretStore.
Configure
import { Kraty } from '@kraty/sdk';
const kraty = new Kraty({
apiKey: process.env.KRATY_API_KEY!,
timeoutMs: 10_000,
retry: {
attempts: 5,
initialDelayMs: 200,
maxDelayMs: 10_000,
jitter: 0.25,
},
onRequest: (info) => { // optional telemetry
console.log(`${info.method} ${info.url} → ${info.status}`);
},
});The API key alone identifies your studio + game; pass nothing else.
Resource clients
Kraty exposes eight resource clients, all sharing one KratyClient:
kraty.events // event list / start / progress
kraty.leaderboards // read + live SSE stream
kraty.grants // pending / claim / open / collectAll
kraty.lobbies // read (with botSlots projection)
kraty.inventory // list / consume
kraty.wallet // list / debit
kraty.players // register / rotate
kraty.catalog // items + currencies for displayThe active player
After the first player-scoped call (or an explicit
await kraty.ensureIdentity()), the SDK holds the player's
externalPlayerId and secret internally. Every player-scoped
method then resolves to that id on its own — you don't pass it
on each call.
const kraty = new Kraty({ apiKey: '<your-client-sdk-key>' });
// All of these target the active player implicitly:
await kraty.events.listForPlayer();
await kraty.grants.listPending();
await kraty.inventory.list();
await kraty.wallet.list();If you need to address a different player from the same
client (typically server-side admin tooling, not a game client),
pass as:
await kraty.grants.listPending({ as: 'other_player' });as skips the active-player resolution entirely — no identity
gets registered or persisted for the override id.
Run an event end to end
// 1) What can the active player play right now?
const available = await kraty.events.listForPlayer();
for (const e of available) {
console.log(`${e.eventKey} (${e.type})`);
if (e.entryCost?.currencies?.length) {
for (const c of e.entryCost.currencies) {
console.log(` cost: ${c.amount} ${c.key}`);
}
}
}
// 2) Start an attempt. Pays entryCost atomically; throws on
// insufficient_entry_cost if the player can't afford it.
const start = await kraty.events.start(
available[0].eventKey,
{ country: 'PT', level: 7 },
);
// 3) Push progress. `set` writes; `increment` adds.
const update = await kraty.events.progress(
available[0].eventKey,
start.attempt.id,
{ mode: 'increment', metricValue: 1 },
);
for (const fired of update.milestonesFired) {
showToast(`Milestone ${fired.key} → ${fired.grants.length} grants`);
}
// 4) Attempt completed?
if (update.attempt.status === 'completed') {
await kraty.grants.collectAll();
}Leaderboards
Snapshot read
// `includeSelf` resolves to the active player by default — no
// need to repeat the id.
const board = await kraty.leaderboards.read(start.leaderboardId, {
limit: 50,
includeSelf: true,
});
for (const e of board.entries) {
console.log(`#${e.rank} ${e.name} ${e.score} (${e.kind})`); // kind: player | bot
}
if (board.self) {
console.log(`You: #${board.self.rank} score ${board.self.score}`);
}Shared leaderboards
Shared leaderboards are cross-event boards configured by the studio (weekly / monthly / all-time, optionally segmented). They're addressed by their game-scoped key — pick a stable string at design time and the client can hardcode it.
// Top 50 of an unsegmented all-time board. The active player's
// rank is included automatically.
const allTime = await kraty.leaderboards.readShared('all_time_global', {
limit: 50,
includeSelf: true,
});For segmented boards you MUST pass the bucket value — the same
field your client supplied in playerContext[segmentation.key]
on attempt start:
const leagueBoard = await kraty.leaderboards.readShared('season_league', {
segment: 'diamond', // the player's bucket
limit: 20,
includeSelf: true,
});To show "last week's top 10" alongside the current board, list
the snapshotted periods and re-read by periodStartedAt:
const { periods } = await kraty.leaderboards.listSharedPeriods('weekly_global');
const lastWeek = periods[0]; // newest snapshot
const snap = await kraty.leaderboards.readShared('weekly_global', {
period: lastWeek.periodStartedAt,
limit: 10,
});readShared returns the same row shape as read plus the
segment and period fields so your UI can label the section
correctly.
Live SSE stream
const stream = await kraty.leaderboards.live(leaderboardId);
try {
for await (const ev of stream.events) {
switch (ev.kind) {
case 'ready': break; // initial handshake
case 'score_update': repaint(ev.data); break;
case 'closed': break; // server finalized
}
}
} catch (err) {
// Transport drop — call live() again after a backoff.
} finally {
await stream.close();
}The SDK does not auto-reconnect — that policy belongs to your app (page-visibility, network-quality detection, etc. differ by use case).
Grants and crates
// Manual loop.
const pending = await kraty.grants.listPending();
for (const g of pending) {
if (g.kind === 'crate') {
await kraty.grants.open(g.id);
} else {
await kraty.grants.claim(g.id);
}
}
// Or in one call:
const result = await kraty.grants.collectAll();
console.log(`Opened ${result.opened.length}, claimed ${result.claimed.length}`);
if (result.hasFailures) {
for (const f of result.failures) {
console.warn(`${f.grant.id} failed:`, f.error);
}
}collectAll opens crates first; the rolled-contents grants the
crates produce land in the next listPending — recall it after
a moment if you want to drain those too.
Inventory and wallet
Only meaningful when the game has
settings.inventoryManagement === 'platform'. For studio-managed
games these endpoints return empty lists.
const items = await kraty.inventory.list();
const wallet = await kraty.wallet.list();
// Spend.
await kraty.inventory.consume('health_potion', { quantity: 1 });
await kraty.wallet.debit('gold', { amount: 100 });Credit / grant flows are server-API-only by design — clients can't
mint resources. Server-side IAP fulfilment goes through
@kraty/server-sdk.
Catalog
Use the catalog client to fetch the public display data for every item and currency in the game — names, icons, descriptions — without needing a player secret. Useful for rendering shop tiles, inventory slots, and reward previews.
const catalog = await kraty.catalog.read();
catalog.items; // [{ key, name, description, iconUrl, kind, rarity, tags }, ...]
catalog.currencies; // [{ key, name, description, iconUrl, kind }, ...]Cache the result client-side and refresh on a long interval — catalog rarely changes during a session, and a single read is enough to render the whole UI.
Lobbies (matchmaking)
When you call events.start on a lobby-matched event, it may throw
KratyApiError with isLobbyForming === true. The SDK exposes a
ready-made polling helper:
import { pollLobbyUntilActive, KratyApiError } from '@kraty/sdk';
try {
const start = await kraty.events.start('quick_brawl');
} catch (err) {
if (err instanceof KratyApiError && err.isLobbyForming) {
const lobbyId = (err.details as { lobbyId: string }).lobbyId;
const lobby = await pollLobbyUntilActive(kraty.lobbies, lobbyId);
// Now safe to retry events.start
const start = await kraty.events.start('quick_brawl');
}
}Lobby reads carry a botSlots projection — the number of bot slots
the server will materialise on promote, derived from lobby age.
Use it (or the lobbyFilledSlots helper) to render a smooth
"filling up" UI:
import { lobbyFilledSlots } from '@kraty/sdk';
const lobby = await kraty.lobbies.read(lobbyId);
console.log(`${lobby.participantCount} humans + ${lobby.botSlots ?? 0} bots / ${lobby.capacity}`);
console.log(`filled (clamped): ${lobbyFilledSlots(lobby)}`);Adaptive polling
import { pollPendingGrants } from '@kraty/sdk';
const ctrl = new AbortController();
void pollPendingGrants(kraty.grants, {
startMs: 2_000,
growMs: 1.5,
maxMs: 30_000,
signal: ctrl.signal,
onBatch: (batch) => { /* claim / open / queue for UI */ },
});
// later: ctrl.abort();Errors
Every non-2xx response throws KratyApiError with a code,
message, and HTTP status. Network failures (DNS, socket reset,
abort) throw KratyNetworkError.
import { KratyApiError, KratyNetworkError } from '@kraty/sdk';
try {
await kraty.events.start('bounty_hunt');
} catch (err) {
if (err instanceof KratyApiError) {
if (err.isLobbyForming) {
// matchmaking lobby still filling — poll and retry
} else if (err.isInsufficientEntryCost) {
// player can't afford — err.message has the resource detail
} else if (err.isPlayerSecretInvalid) {
// re-register or surface to the user
} else {
switch (err.code) {
case 'no_active_window':
// event is between windows
break;
case 'max_attempts_reached':
// player burned all attempts for this window
break;
}
}
} else if (err instanceof KratyNetworkError) {
// backend unreachable
}
}Typed getters on KratyApiError:
isLobbyForming— 202 lobby_forming, poll the lobbyisInsufficientEntryCost— 402, paid event the player can't affordisPlayerSecretInvalid— 401, secret missing or wrongisPlayerAlreadyRegistered— 409, retry withforce: truein devisEntryRequirementFailed— 403, ownership gate failed
Full code reference: Error codes.
Retries and idempotency
Every POST / PUT / PATCH is auto-stamped with an
idempotencyKey (crypto.randomUUID() by default), preserved
across retries — so a network reset between request-sent and
response-received doesn't double-charge or double-grant.
Default retry policy: 408 / 425 / 429 / 5xx and network
failures; exponential backoff with jitter; honours Retry-After.
Configure via the retry option (see Configure).
Telemetry
new Kraty({
apiKey: '...',
onRequest: (info) => {
metrics.timing(`kraty.${info.url}`, info.durationMs);
if (!info.ok) metrics.increment(`kraty.error.${info.status}`);
},
});Fires once per HTTP attempt, including retries. Use the attempt
field to dedupe.
Resource reference
Every player-scoped method defaults to the active player and
accepts an { as } option to address a different one.
| Client | Methods |
|---|---|
kraty.events | listForPlayer({ as }), start(eventKey, playerContext, { as }), progress(eventKey, attemptId, input, { as }) |
kraty.leaderboards | read(id, options), readShared(key, options), listSharedPeriods(key), live(id) |
kraty.grants | listPending({ as, limit }), claim(grantId, { as }), open(grantId, { as }), collectAll({ as }) |
kraty.inventory | list({ as }), consume(itemKey, input, { as }) |
kraty.wallet | list({ as }), debit(economyKey, input, { as }) |
kraty.lobbies | read(lobbyId) |
kraty.catalog | read() — items + currencies for display |
kraty.players | register(externalId, { force }) — power-user / dev only; the SDK calls this automatically inside ensureIdentity |
Identity surface on Kraty:
activeExternalPlayerId— getter,nulluntil the first resolve.ensureIdentity()— resolve up-front (rare).signIn({ externalPlayerId, secret })— install + persist a server-issued identity.logout()— wipe the persisted identity.
Free functions / classes:
pollPendingGrants(grantsClient, options)pollLobbyUntilActive(lobbiesClient, lobbyId, options)InMemorySecretStore,LocalStorageSecretStore— for custom-store consumers; not needed by default.lobbyFilledSlots(lobby)— helper for the UI fill projection
Client SDKs
Game-client SDKs for the Kraty platform — TypeScript, Unity, and Flutter. Each speaks the /sdk/v1 surface with a client_sdk API key + per-player auth.
Unity SDK
Drop-in C# client for Unity 2022 LTS+ game clients — events, leaderboards, lobbies, grants, inventory, wallet, and per-player auth.