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, built 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 keep the
common patterns to 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,
which 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, so install directly from the public GitHub
repo against a tagged release. Each release ships compiled dist/
artefacts so no build step is needed on your side.
npm install github:PedroTrincheiras/kraty-sdk-typescript#v0.18.1
# or with pnpm:
pnpm add github:PedroTrincheiras/kraty-sdk-typescript#v0.18.1Browse releases at
github.com/PedroTrincheiras/kraty-sdk-typescript/releases.
Once we publish to npm (planned for v1.0) you'll be able to swap to
pnpm add @kraty/sdk.
Integration in three steps
Install with the command above.
Get a client_sdk API 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 is 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 is 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();Bootstrap with a known player id
When you already know the player's id at launch (your own auth
resolved it), Kraty.connectAsPlayer(...) gives you a fully
wired, player-authenticated SDK in one call: it reads any stored
secret, registers if absent, and persists the result back to the
secretStore:
import { Kraty, LocalStorageSecretStore } from '@kraty/sdk';
const secretStore = new LocalStorageSecretStore();
const kraty = await Kraty.connectAsPlayer({
options: { apiKey: '<your-client-sdk-key>' },
externalPlayerId: 'player_42',
secretStore,
});
// Ready to use: the secret is already attached to every request.
await kraty.events.listForPlayer();At app boot you can check whether this device has connected before, and skip straight into the session instead of showing an onboarding screen:
const stored = await Kraty.readStoredIdentity(secretStore);
if (stored) {
// Returning player: resume immediately.
const kraty = new Kraty({
apiKey: '<your-client-sdk-key>',
playerSecret: stored.secret,
activeExternalPlayerId: stored.externalPlayerId,
});
startGame(stored.externalPlayerId, kraty);
} else {
// Fresh install / post-logout: show "tap to play".
showOnboarding();
}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 does not 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 ten resource clients, all sharing one
KratyClient:
kraty.events // event list / start / progress
kraty.leaderboards // cross-event boards by key: read / listPeriods
kraty.eventLeaderboards // per-event attempt board by UUID: read + live/subscribe SSE
kraty.grants // pending / claim / open / collectAll
kraty.lobbies // read (with botSlots projection)
kraty.inventory // list / consume
kraty.wallet // list / debit
kraty.players // register / rotate / setIdentity / getIdentity / getAnonymizedIdentity / setMetadata / mergeMetadata
kraty.friends // friend codes / search / requests / presence / blocks
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 do not 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.
Real vs anonymized identity
Every player carries two orthogonal identities:
- Anonymized (synthetic) — a stable
{ name, avatar }pulled from the game's identity pool at register-time, seeded off the player id so it never changes. Safe to surface on public boards or cross-game aggregations without exposing the real profile. - Display (real) — what
players.setIdentity()writes. Optional; falls back to the anonymized value when the player never renamed themselves. This is what leaderboards render as the primary name.
Both accessors return the same envelope shape ({ name, avatar?, country? })
so a game can render either view with one branch:
const kraty = await Kraty.connectAsPlayer({ options, externalPlayerId, secretStore });
const real = await kraty.players.getIdentity(); // display name if set, else pool
const anon = await kraty.players.getAnonymizedIdentity(); // always the immutable pool value
greet(real?.name ?? 'Player');
publicScoreboard(anon?.name, anon?.country); // render a flag next to the aliasThe same shape is embedded on the register response so game code can read both identities without a follow-up call:
const reg = await kraty.players.register('player_42');
reg.displayIdentity?.name; // real name (or the pool value, if never renamed)
reg.anonymizedIdentity?.name; // e.g. "Crazy Panda" — always the pool value
reg.anonymizedIdentity?.country; // ISO-3166 alpha-2, for a flag iconEvery LeaderboardEntry mirrors this: name / avatar hold the real
display view for that participant, and anonymizedName /
anonymizedAvatar carry the immutable pool value so a game can flip
between the two views without a second lookup.
To rename the active player from a "choose your handle" screen:
await kraty.players.setIdentity({ name: 'CaptainAlice', avatar: 'https://…/alice.png' });The rewrite is retroactive: past-period standings and the live board both surface the new name immediately. The anonymized identity stays intact.
Custom player metadata
Kraty carries a free-form metadata bag on every player — studios
attach any keys they want (VIP flag, gender, preferred language,
cohort, whatever the platform's model doesn't already cover). The bag
surfaces on the register response and on every leaderboard entry so
game code can render metadata-aware UI (VIP crowns, gender-aware
avatars) without a follow-up player-lookup call.
Two write shapes:
players.setMetadata(bag)— replace the whole bag with the supplied object.players.mergeMetadata(patch)— shallow-merge the patch into the existing bag. Keys not in the patch stay untouched.
// Replace: player's metadata becomes exactly { vip: true, tier: 'gold' }.
await kraty.players.setMetadata({ vip: true, tier: 'gold' });
// Merge: only the `preferredLanguage` key is touched.
await kraty.players.mergeMetadata({ preferredLanguage: 'pt' });Authorized by the player's own secret — a client can only ever edit
its own metadata. Studios that need moderation (e.g. reject profanity
inside a handle key) should set metadata from their backend via the
server SDK players.setMetadata instead.
Run an event end to end
// 0) Connect. The first player-scoped call auto-registers + persists
// the active player.
const kraty = new Kraty({ apiKey: '<your-client-sdk-key>' });
// Show a result screen exactly once when a board the player is on ends:
// live over SSE while subscribed, and on catch-up after a cold start.
kraty.onFinalized((result) => {
const placed = result.self ? `#${result.self.rank}` : '—';
showResultScreen(result.ref.leaderboardId, result.reason, placed);
void kraty.dismiss(result.ref);
});
// 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 cannot afford it.
const start = await kraty.events.start(
available[0].eventKey,
{ country: 'PT', level: 7 },
);
// 3) Subscribe to the attempt's live board. `subscribe(...)` is the
// high-level helper; it composes the SSE stream with a background
// poll so bots tick and deltas are deduped for you. (The raw
// `live(...)` primitive is available if you want to drive the poll
// yourself.)
const sub = kraty.eventLeaderboards.subscribe(start.leaderboardId, (ev) => {
if (ev.kind === 'score_update') repaint(ev.data);
});
// 4) Push progress. `set` writes; `increment` adds. The board repaints
// via the subscription above.
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`);
}
// 5) Attempt completed? Tear down the stream and collect rewards.
if (update.attempt.status === 'completed') {
await sub.close();
await kraty.grants.collectAll();
}For an untimed score-attack event (no completion target), step 5
never trips on its own; the player decides when the run is over.
Call events.finish to end it and settle the current score instead
of relying on a target (see below).
End the run (score-attack)
events.finish finalizes the in-progress attempt at its current
score and returns an outcome. Untimed score-attack events (no
completion target) require it; otherwise the attempt just sits
until the window closes. Wire it to your "End run" button:
async function onEndRun() {
const { attempt, outcome } = await kraty.events.finish(
'score_attack',
activeAttemptId,
);
await sub.close(); // tear down the live board
if (outcome === 'completed') {
// No target (or target already met) → completion rewards rolled.
await kraty.grants.collectAll();
}
// 'expired' → event had a target the player did not meet;
// participation rewards only, same as a timeout.
showResultScreen(attempt);
}finish throws KratyApiError on a session / lobby event (those
end via their session rules) and on an already-finished attempt.
Leaderboards
Two resource clients sit on the facade:
kraty.leaderboards: the dashboard-configured cross-event boards your studio defines (weekly / monthly / all-time, optionally segmented). Addressed by stable key. Use this for most game UI.kraty.eventLeaderboards: the auto-generated per-event-window leaderboard tied to an attempt. Addressed by UUID, the oneevents.start(...)returned inattempt.leaderboardId. Includes SSE live streaming.
Snapshot read: by key
// Top 50 of an unsegmented all-time board. `includeSelf` resolves
// to the active player by default, so there's no need to repeat the id.
const allTime = await kraty.leaderboards.read('all_time_global', {
limit: 50,
includeSelf: true,
});
for (const e of allTime.entries) {
console.log(`#${e.rank} ${e.name} ${e.score} (${e.kind})`); // kind: player | bot
}
if (allTime.self) {
console.log(`You: #${allTime.self.rank} score ${allTime.self.score}`);
}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.read('season_league', {
segment: 'diamond',
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.listPeriods('weekly_global');
const lastWeek = periods[0]; // newest snapshot
const snap = await kraty.leaderboards.read('weekly_global', {
period: lastWeek.periodStartedAt,
limit: 10,
});The response carries key, period, segment, entries, and
self so your UI can label the section correctly.
Snapshot read: by event-window UUID
const eventLeaderboard = await kraty.eventLeaderboards.read(start.leaderboardId, {
limit: 50,
includeSelf: true,
});Use this only when you want the attempt's leaderboard view, the
short-lived board for one event window. For "the leaderboard
players see in the lobby UI", kraty.leaderboards.read(key) is
what you want.
Submit a score directly
Push a score straight to a configurable board for the active player
with no event attempt in the loop. Only valid for score-ranked boards;
a progression-ranked board returns 400 score_not_supported.
// Unsegmented / progression-segmented board: no segment.
const result = await kraty.leaderboards.submitScore('daily_steps', 8421);
console.log(result.leaderboardId, result.score, result.rank);
// context-segmented board: pass the bucket the client owns.
await kraty.leaderboards.submitScore('weekly_region', 8421, {
segment: 'EU',
idempotencyKey: 'steps_2026_06_29', // optional; dedupes retries
});rank is null when the board can't place the player yet (e.g. a
best-aggregation board the score didn't beat). If the board has
Accept client scores turned off, this throws
KratyApiError with code: 'client_scoring_disabled' (403): that
board is server-authoritative; score it from your backend with
@kraty/server-sdk. See
Client vs server segmentation
for when segment is required.
Join without a score
// Appear on the board at 0 without scoring; returns current standings.
const board = await kraty.leaderboards.join('weekly_region', { segment: 'EU' });
const evBoard = await kraty.eventLeaderboards.join(start.leaderboardId);Flexible standings
read returns one segment; standings returns one block per segment
(scope: self_segment | mine | segment | all), each flagging
the caller, live or for a past period.
const mine = await kraty.leaderboards.standings('season_league', {
scope: 'self_segment', // or 'mine' | 'all' | 'segment'
externalId: 'player_alice',
});
mine.segments[0].selfRank; // caller's rank in their division
mine.segments[0].participated; // true
const { periods } = await kraty.leaderboards.listPeriods('season_league');
const past = await kraty.leaderboards.standings('season_league', {
scope: 'all',
period: periods[0].periodStartedAt,
});Live SSE stream
const stream = await kraty.eventLeaderboards.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).
Finalization catch-up
Boards end. A session (lobby) inside an event can terminate early (first to N points, roster full, idle timeout), and the whole event window closes at its end time. When a board the player is on ends, you usually want to show a result screen: "your session ended, you placed 2nd."
The finalized SSE event tells you this live, but only while the player
is connected. A player who closed the app misses it and comes back to a
fresh board. So the SDK keeps a small persisted registry of the boards
the player is in (every events.start auto-tracks its board) and gives you
one callback that fires exactly once per board across both paths:
- Live: the
finalizedSSE event (while subscribed). - Catch-up:
checkFinalizations(), which you call on app foreground / reconnect to detect boards that ended while away.
// Register once (e.g. at app boot). Returns an unsubscribe fn.
const off = kraty.onFinalized((result) => {
const placed = result.self ? `#${result.self.rank}` : '—';
showResultScreen(result.ref.leaderboardId, result.reason, placed);
// Acknowledge so it never resurfaces and leaves storage.
void kraty.dismiss(result.ref);
});
// On app foreground / reconnect: cheap when nothing ended.
const ended = await kraty.checkFinalizations();
console.log(`${ended.length} board(s) finalized while away`);result.reason distinguishes why the board ended, even on the catch-up
path, because the board persists it. Use the FinalizationReason constants
rather than magic strings:
import { FinalizationReason } from '@kraty/sdk';
kraty.onFinalized((result) => {
switch (result.reason) {
case FinalizationReason.SessionTerminated: // your lobby ended early
case FinalizationReason.WindowClosed: // the whole event ended
case FinalizationReason.Finalized: // ended, exact cause unknown
}
});Delivery is at-most-once even across a crash: the registry entry is marked
reported before your callback fires. dismiss(ref) drops one handled
entry; clearReported() bulk-drops every delivered entry (returns the count).
Rewards are separate and durable: any prize from a finalized session
or window lands as a pending grant regardless of connectivity. Pull them
with grants.collectAll(). onFinalized is for the result screen;
grants are for what the player earned.
Persistence uses localStorage in the browser (via
LocalStorageMembershipStore) and an in-memory store elsewhere. Pass your
own membershipStore in KratyClientOptions to override.
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
cannot 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.
Friends
kraty.friends is the client-side social graph: friend codes,
username search, requests, live presence, and blocking. Every call is
authorized by the active player's own secret, so a client can only act
on its own graph; friendships are scoped to one game + environment. The
full how-to (with the model and raw REST) lives in the
Friends guide.
// Your shareable 6-char code (stable for the player's lifetime).
const { friendCode } = await kraty.friends.getCode();
// Find players by display name; each hit carries your relationship.
const hits = await kraty.friends.search('shadow', { limit: 10 });
// Add by code or by externalPlayerId (exactly one).
const sent = await kraty.friends.add({ friendCode: 'K7F2Q9' });
await kraty.friends.add({ externalPlayerId: 'player_88' });
// sent.status is 'pending' (they must accept) or 'accepted'
// (reciprocal auto-accept when they'd already requested you).
// Pending requests, both directions.
const { incoming, outgoing } = await kraty.friends.listRequests();
await kraty.friends.accept(incoming[0].requestId); // → Friend
await kraty.friends.decline(incoming[1]?.requestId);
await kraty.friends.cancelRequest(outgoing[0]?.requestId); // your own outgoing one
// Your friends, enriched with online / lastActiveAt / status.
const friends = await kraty.friends.list();
// Broadcast presence on a ~30s timer; it expires when beats stop.
await kraty.friends.heartbeat({ status: 'in_match' });
// Remove a friend by their externalPlayerId.
await kraty.friends.remove('player_88');
// Blocking tears down any friendship/request and hides you both.
await kraty.friends.block({ externalPlayerId: 'player_99' });
const blocked = await kraty.friends.listBlocks();
await kraty.friends.unblock('player_99');The friend-specific error codes (cannot_friend_self,
already_friends, friend_code_invalid, player_blocked) surface as
KratyApiError.code; switch on them the same way as any other error.
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 cannot 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 cannot 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 does not 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 }), finish(eventKey, attemptId, { as }) |
kraty.leaderboards | read(key, options), submitScore(key, value, opts?), listPeriods(key, opts?) (dashboard-configured cross-event boards) |
kraty.eventLeaderboards | read(id, options), live(id), subscribe(id, options) (per-event-window boards (UUID), includes SSE stream) |
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.friends | getCode({ as }), search(query, { limit, as }), add(target, { as }), listRequests({ as }), accept(requestId, { as }), decline(requestId, { as }), cancelRequest(requestId, { as }), list({ as }), remove(friendExternalId, { as }), heartbeat({ status, as }), block(target, { as }), listBlocks({ as }), unblock(blockedExternalId, { as }) |
kraty.catalog | read() (items + currencies for display) |
kraty.players | register(externalId, { force }) (power-user / dev only; the SDK calls this automatically inside ensureIdentity); setIdentity({ name, avatar? }) — set the active player's own display name (see player tools) |
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.