Kraty

Error reference

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.

Response envelope

{
  "error": {
    "code": "insufficient_entry_cost",
    "message": "not enough cash to enter — need 50",
    "details": { "resource": "cash", "needed": 50, "have": 30 }
  }
}
FieldTypeNotes
codestringOne of the codes below. Stable.
messagestringPlayer-facing where appropriate. Localise if you need other languages.
detailsobject?Code-specific extra fields. Optional, may be absent.

Code reference

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:

LanguageConventionExample
TypeScript (client + server)isXxx gettererr.isPlayerSecretInvalid
Dart (Flutter)isXxx gettererr.isPlayerSecretInvalid
C# (Unity)IsXxx property (PascalCase)err.IsPlayerSecretInvalid
Python (server)is_xxx property (snake_case)err.is_player_secret_invalid

For codes the SDK hasn't been bumped to know about yet, use the generic escape hatch:

  • TS / Dart: err.is('code_string') (or err.isCode(...) in Dart)
  • C#: err.Is("code_string")
  • Python: err.is_code("code_string")

400 Bad Request

CodeWhenSDK helper
validation_failedRequest body / query failed schema validation. details carries field-level errors.err.isValidationFailed
invalid_metricprogress referenced a metric key the event doesn't declare.err.isInvalidMetric

401 Unauthenticated

CodeWhenSDK helper
unauthenticatedAuthorization header missing on a protected route.err.isUnauthenticated
session_invalidBearer token is malformed, revoked, or rejected.err.isSessionInvalid
player_secret_invalidX-Player-Secret header missing, malformed, or doesn't match the stored hash for :externalId.err.isPlayerSecretInvalid

402 Payment Required

CodeWhenSDK helper
insufficient_entry_costevents.start on a paid event the player can't afford. message names the resource shortfall ("not enough cash to enter — need 50"). Tx is rolled back — partial debits never persist.err.isInsufficientEntryCost

403 Forbidden

CodeWhenSDK helper
forbiddenMember / API key has the right authentication but lacks the permission for this route.err.isForbidden
unlock_condition_failedPlayer attempted an event they can't see yet (visibility gate).err.isUnlockConditionFailed
entry_requirement_failedPlayer attempted an event they can see but doesn't meet the joinability requirement (e.g. "must own item X").err.isEntryRequirementFailed
tenant_mismatchYou tried to access a resource that doesn't belong to the studio/game your API key is scoped to.err.isTenantMismatch
player_bannedThe 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.err.isPlayerBanned

422 Unprocessable Entity

CodeWhenSDK helper
anti_cheat_rejectedA 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.err.isAntiCheatRejected

404 Not Found

CodeWhenSDK helper
not_foundThe referenced resource (event, attempt, grant, lobby, leaderboard, player) doesn't exist or is archived.err.isNotFound

409 Conflict

CodeWhenSDK helper
conflictGeneric mutation conflict (e.g. wallet debit on a 0 balance, idempotency-replay with a different body).err.isConflict
event_disabledEvent is configured but disabled.err.isEventDisabled
no_active_windowEvent has no currently-active window — player is between windows.err.isNoActiveWindow
attempt_finishedprogress on an attempt that's already completed / expired.err.isAttemptFinished
idempotency_conflictSame idempotencyKey used with a different request body within the cache TTL.err.isIdempotencyConflict
player_already_registeredregister for a player who already has a secret. In dev/test, retry with ?force=true. In production, route to your recovery flow.err.isPlayerAlreadyRegistered

429 Too Many Requests

CodeWhenSDK helper
rate_limitedPer-key rate limit exceeded. Retry-After header carries the wait. The SDK auto-retries with backoff.err.isRateLimited
max_attempts_reachedPlayer burned all attempts allowed for the current event window.err.isMaxAttemptsReached
max_daily_attempts_reachedPer-day cap reached — wait until midnight in the event's timezone.err.isMaxDailyAttemptsReached

500 Internal Error

CodeWhenSDK helper
internal_errorUnhandled exception. Logged + alerted server-side; surface a generic "something went wrong" to the player.err.isInternalError
invalid_unlock_conditionEvent config has a malformed unlock condition tree — operator should fix it in the portal.err.isInvalidUnlockCondition
invalid_entry_requirementSame as above for the entry requirement field.err.isInvalidEntryRequirement
no_leaderboardServer couldn't allocate / find the leaderboard for this attempt. Usually transient.err.isNoLeaderboard

202 (Special) Lobby forming

CodeWhenSDK helper
lobby_formingReturned by events.start on lobby-matched events when the lobby isn't 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.err.isLobbyForming

Portal / member-management codes

These fire on the portal session surface (/admin/v1 and the member-OAuth login flow). They're 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.

CodeHTTPWhen
no_account404OAuth claims didn't match any existing member. Trigger the sign-up / invitation acceptance flow.
no_active_studio403Session exists but has no active studio context (user removed from their last studio mid-session).
invitation_invalid404 / 409Invitation token not found, already accepted, or revoked.
invitation_expired410Invitation outside its TTL — operator must re-send.
invitation_email_mismatch403The invitation was issued to a different email than the authenticated user's.
member_not_found404Referenced member record was deleted between request issue and execution.
not_a_member403Caller isn't a member of the studio they're trying to act on.

Handling patterns

Typed handling with helper getters

try {
  await kraty.events.start('player_42', 'bounty_hunt');
} on KratyApiError catch (err) {
  if (err.isLobbyForming) {
    final lobbyId = (err.details as Map)['lobbyId'] as String;
    await pollLobbyUntilActive(kraty.lobbies, lobbyId);
  } else if (err.isInsufficientEntryCost) {
    showInsufficientResourceDialog(err.message);
  } else if (err.isPlayerSecretInvalid) {
    await reregister();
  } else if (err.isEntryRequirementFailed) {
    showLockedEventDialog(err.message);
  } else {
    rethrow;
  }
}

Switch on code for the rest

try {
  await kraty.events.progress(...);
} on KratyApiError catch (err) {
  switch (err.code) {
    case KratyErrorCode.noActiveWindow:
      showToast('Event hasn\'t 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(externalId);
      break;
    default:
      rethrow;
  }
}

Network failures

KratyNetworkError covers everything that didn't produce an HTTP response (DNS, socket reset, timeout). The SDK auto-retries network errors with backoff before surfacing this.

try {
  await kraty.events.listForPlayer('player_42');
} on KratyNetworkError catch (err) {
  // Backend unreachable. Show offline UI, queue actions for later.
  print('Network: ${err.message}');
  print('Original: ${err.originalCause}');
}

Idempotency replays

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.

Rate limiting

429 rate_limited carries a Retry-After header. The SDK honours it automatically — you only see the 429 surface as a thrown error if all retry attempts (default 3) exhausted the budget.

If you're a studio backend hitting /server/v1 hard for batch fulfilment, prefer fewer larger requests over many small ones. Per- key rate limits sit at ~600 req/min for client_sdk and ~1000/min for server_integration today.

Constants

All codes are exported as KratyErrorCode constants in the Flutter SDK so you can compare without typos:

import 'package:kraty/kraty.dart';

if (err.code == KratyErrorCode.insufficientEntryCost) { ... }

The full list mirrors the table above. Adding a new code is a breaking SDK change and bumps the minor version — track the changelog.