Every error code Kraty returns, when it fires, what HTTP status it maps to, and how to handle it in the SDK.
Kraty errors are sealed: every non-2xx response carries a
code from a known set, plus a human-readable message and an
optional structured details. Codes are stable: a new code is a
breaking change, never silently introduced.
Grouped by HTTP status. Every code has a typed is… getter on
the SDK's error class. Match on the getter, not on a string
comparison. The "SDK helper" column lists the canonical getter
name; each SDK adapts to its language convention:
Language
Convention
Example
TypeScript (client + server)
isXxx getter
err.isPlayerSecretInvalid
Dart (Flutter)
isXxx getter
err.isPlayerSecretInvalid
C# (Unity)
IsXxx property (PascalCase)
err.IsPlayerSecretInvalid
Python (server)
is_xxx property (snake_case)
err.is_player_secret_invalid
For codes the SDK has not been bumped to know about yet, use the
generic escape hatch:
TS / Dart: err.is('code_string') (or err.isCode(...) in
Dart)
events.start on a paid event the player cannot afford. message names the resource shortfall ("not enough cash to enter, need 50"). Tx is rolled back, so partial debits never persist.
Member / API key has the right authentication but lacks the permission for this route.
err.isForbidden
unlock_condition_failed
Player attempted an event they cannot see yet (visibility gate).
err.isUnlockConditionFailed
entry_requirement_failed
Player attempted an event they can see but does not meet the joinability requirement (e.g. "must own item X").
err.isEntryRequirementFailed
tenant_mismatch
You tried to access a resource that does not belong to the studio / game your API key is scoped to.
err.isTenantMismatch
player_banned
The player has been soft-banned by the studio (auto-detection or operator action). All player-scoped SDK writes are gated. Existing scores stay intact; lift via the server SDK or portal.
A server-side anti-cheat validator on the event rejected this progress write. The write was rolled back; the player's score is unchanged. The body's message carries the validator's reason.
Returned by events.start on lobby-matched events when the lobby is not yet at capacity. details.lobbyId carries the lobby to poll. Despite the 2xx status, the SDK throws this as KratyApiError so the caller can try/catch uniformly.
These fire on the portal session surface (/admin/v1 and the
member-OAuth login flow). They are never returned to game clients
through /sdk/v1, so SDK consumers can ignore them; they exist
for the portal UI and for any internal tooling you build against
the admin API.
Code
HTTP
When
no_account
404
OAuth claims did not match any existing member. Trigger the sign-up / invitation acceptance flow.
no_active_studio
403
Session exists but has no active studio context (user removed from their last studio mid-session).
invitation_invalid
404 / 409
Invitation token not found, already accepted, or revoked.
invitation_expired
410
Invitation outside its TTL; operator must re-send.
invitation_email_mismatch
403
The invitation was issued to a different email than the authenticated user's.
member_not_found
404
Referenced member record was deleted between request issue and execution.
not_a_member
403
Caller is not a member of the studio they are trying to act on.
try { await kraty.events.progress(...);} on KratyApiError catch (err) { switch (err.code) { case KratyErrorCode.noActiveWindow: showToast('Event has not started yet.'); break; case KratyErrorCode.maxAttemptsReached: showToast('Out of tries. Come back next round.'); break; case KratyErrorCode.attemptFinished: // server-side completion races client; just refresh state await kraty.grants.collectAll(); break; default: rethrow; }}
KratyNetworkError covers everything that did not produce an
HTTP response (DNS, socket reset, timeout). The SDK auto-retries
network errors with backoff before surfacing this.
try { await kraty.events.listForPlayer();} on KratyNetworkError catch (err) { // Backend unreachable. Show offline UI, queue actions for later. print('Network: ${err.message}'); print('Original: ${err.originalCause}');}
Replaying a write with the same idempotencyKey and the same
body returns the original response: no error, no double-effect.
Replay with a different body returns 409
idempotency_conflict.
The SDK auto-generates an idempotency key per POST / PUT / PATCH
and preserves it across retries, so a network failure between
request-send and response-receive is replay-safe by default.
429 rate_limited carries a Retry-After header. The SDK
honours it automatically, so you only see the 429 surface as a
thrown error if all retry attempts (default 4) exhausted the
budget.
If you are a studio backend hitting /server/v1 hard for batch
fulfilment, prefer fewer larger requests over many small ones.
Per-key rate limits today: client_sdk keys get a read/write
split (~600 reads/min, ~120 writes/min); server_integration
keys get ~1000 req/min. See the full table in
REST API → Rate limits.
The full list mirrors the table above. Adding a new code is a
breaking SDK change and bumps the minor version, so pin a specific
SDK version in your pubspec.yaml / package.json to stay in
control.