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 has not 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 does not 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 does not match the stored hash for :externalId.err.isPlayerSecretInvalid

402 Payment Required

CodeWhenSDK helper
insufficient_entry_costevents.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.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 cannot see yet (visibility gate).err.isUnlockConditionFailed
entry_requirement_failedPlayer attempted an event they can see but does not meet the joinability requirement (e.g. "must own item X").err.isEntryRequirementFailed
tenant_mismatchYou tried to access a resource that does not 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) does not 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 is 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 could not 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 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.err.isLobbyForming

Portal / member-management codes

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.

CodeHTTPWhen
no_account404OAuth claims did not 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 is not a member of the studio they are trying to act on.

Handling patterns

Typed handling with helper getters

try {
  await kraty.events.start('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 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;
  }
}

Network failures

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}');
}

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, 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.

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, so pin a specific SDK version in your pubspec.yaml / package.json to stay in control.