Skip to main content

Error codes

A single reference for every error an integrator can receive, with remediation. There are two channels: GraphQL errors (Management API and Game API) and realtime/UDP errors (the Game API UDP-proxy and the native Replication API).

GraphQL errors

GraphQL responses carry errors in the top-level errors array. Each entry has a message, a path, and an extensions object you can branch on programmatically. Beyond code, errors carry an extensions.remediation hint and — for permission failures — an extensions.requiredPermission:

{
"errors": [
{
"message": "Missing org permission 'manage_billing'",
"path": ["setAppBudget"],
"extensions": {
"code": "FORBIDDEN",
"httpStatus": 403,
"requiredPermission": "manage_billing",
"remediation": "Your token lacks the permission this operation requires. The field description (and its @requiresPermission directive) names the required permission; use a token/role that holds it."
}
}
],
"data": null
}
extensions.codeMeaningRemediation
UNAUTHENTICATEDNo bearer token, or it is invalid/expired.For Management API calls, sign in again (password, magic link, or social) for the session token — see Sign in. For the Game API + realtime subscriptions, send an app-scoped token (mintAppToken / portal flow) as the Bearer (and in the ws connection_init payload) — the session token is rejected there.
FORBIDDENAuthenticated, but the token lacks the required permission for this field.Each operation's description and its @requiresPermission directive name the permission; extensions.requiredPermission carries the key. Use a token/role that holds it.
SCOPE_MISSINGThe token is scoped to a different org/app than the request targets.Use a token minted for the requested org/app, or a full-scope org token.
BAD_USER_INPUTAn argument failed validation (wrong type, out of range, missing required field).Check the argument descriptions in the SDL; BigInt must be a string, enums must be exact names.
BAD_REQUESTThe request was rejected by the resolver (e.g. a disabled feature, a precondition not met).Read message; it states the precondition.
GRAPHQL_VALIDATION_FAILEDThe query/mutation document is invalid against the schema.Validate against the published SDL or introspection.
NOT_FOUNDThe referenced entity does not exist or is not visible to you.Verify the ID and that your token can see it.
CONFLICTThe request conflicts with current state (incl. an idempotency key still processing).Refetch and retry if appropriate.
IDEMPOTENCY_CONFLICTAn idempotencyKey was reused with different request parameters.Use a new key, or resend the byte-identical request to replay the first result. See pagination/idempotency notes.
EMAIL_ALREADY_REGISTEREDregister was called for an address that already has an account. The password was attached pending email confirmation and no session was issued.Routine, not a fault. Have the user follow the emailed link, or sign in with their existing method. Do not retry the registration.
PASSWORD_ALREADY_SETsetInitialPassword on an account that already has a password.Use changePassword (which verifies the current one), or requestPasswordReset. The session is valid — do not sign the user out.
PASSWORD_NOT_SETchangePassword on an account with no password to change.Use setInitialPassword, which needs only the session. The session is valid — do not sign the user out.
PASSWORD_PWNEDregister or setInitialPassword used a password that appears in a public breach list (Have I Been Pwned). 403.Choose a different password. The server sends only a SHA-1 prefix (k-anonymity); the password never leaves the API. A HIBP outage fails open so registration is not blocked.
HOSTED_SIGN_IN_REQUIREDA direct sign-in mutation (login, register, magic link, social, reset…) was called from a browser origin that is not first-party (Studio, crowdy.games). 403.Use hosted sign-in: client.portal.signIn({ appId, redirectUri }) then client.portal.handleSignInCallback(); register your origin under the app's redirect URIs in Studio. Non-browser clients (no Origin header) are unaffected.
CSRF_REQUIREDThe request authenticated with the Studio ck_session cookie but did not send X-CSRF-Token. 403. Bearer-only clients never see this.Send the readable ck_csrf cookie value as X-CSRF-Token, or send Authorization: Bearer instead.
CSRF_MISMATCHX-CSRF-Token does not match any presented ck_csrf. 403. Duplicate cookies (parent-zone leftover + tier cookie) are accepted when the header matches either one (ck-api v1.98.0).Refresh to pick up a new CSRF cookie, or send Authorization: Bearer.
COOKIE_AUTH_ORIGIN_REFUSEDA cookie session was offered from a browser origin that is not first-party. 403.From a customer origin, use a Bearer app token from hosted /authorize. Do not send ck_session.
INVALID_CURRENT_PASSWORDchangePassword was given the wrong current password.Ask again, or offer requestPasswordReset. The session is valid — do not sign the user out.
RATE_LIMITEDA rate/usage limit was exceeded.Back off and retry with exponential backoff.
CONTAINER_TYPE_UNDEFINEDA container was created against a game-model container type the app has not defined.extensions.definedTypes lists what the app does declare, so a typo is visible without a second call. An empty list means the app has no game model at all — the shape an app takes when it is recreated or moved between orgs — so re-run your gameModelSeed. gameModelLint returns every such problem in one query.
OBJECT_QUARANTINEDOne game-model function or automation is refusing to run because an enforced gameModelLint error stands against it. Scoped to that object — the rest of the app is unaffected. On gameModelInvoke you get USER_CODE_ERROR instead (see the player boundary below), with blame: AUTHOR and the quarantine fields intact.extensions.quarantineReason names the finding, and quarantinedKind / quarantinedName name the object — on both codes. Fix the definition and write it again: quarantine never blocks a write and clears itself once the finding is gone, so there is nothing to ask us to reset. gameModelLint lists everything currently wrong with the app.
INTERNAL_SERVER_ERRORUnexpected server error.Safe to retry idempotent reads; do not blind-retry non-idempotent mutations (send an idempotencyKey instead).
Codes changed in ck-api v1.60.0 — check the tier before you branch

Until v1.60.0, only four HTTP statuses reached you as a distinct code (400, 401, 403, 422). Everything else — every NOT_FOUND, every CONFLICT — arrived as INTERNAL_SERVER_ERROR with the real status in extensions.httpStatus. That is fixed, and the four password/registration codes above are new in the same release.

Two consequences if you are writing a client today:

  • Against an older tier, branch on extensions.httpStatus, which was always correct, or on the message. extensions.code was not usable for these.
  • The three changePassword/setInitialPassword refusals used to arrive as UNAUTHENTICATED, which is also what an expired session looks like. A client that signs the user out on UNAUTHENTICATED was signing them out for mistyping a password. None of the new codes means the session is bad.

CROWDY_STUDIO_REVISION_CONFLICT is a related trap that was never a mapping bug: it is its own code, not CONFLICT with a detail message. Branch on the exact string.

requiredPermission and the directive. Permission-gated fields carry a machine-readable @requiresPermission(scope:, permission:, scopeArg:) directive in the SDL/introspection, so an agent can plan calls without parsing prose. On a FORBIDDEN/SCOPE_MISSING error, extensions.requiredPermission echoes the missing key.

Idempotency. Economy-sensitive and destructive mutations accept an optional idempotencyKey (on the input object or as a top-level argument). Replaying with the same key and identical parameters returns the first result instead of re-applying; a different payload under the same key returns IDEMPOTENCY_CONFLICT. Keys expire after 24h.

Partial failures: GraphQL can return both data and errors in one response — a nullable field may resolve to null with a corresponding errors entry while the rest of data is populated. Always inspect errors even when data is present.

When code you wrote fails: blame, retryable and the fault codes

Three entry points run code the platform did not write — gameModelInvoke, computeInvoke and playerComputeInvoke. A failure on one of them is answered with a fault: a stable code, a blame, and a retryable flag. Nothing else comes back. The engine's own error text, the sandbox's fault kind, the failing expression and any internal identifiers stay on the server, where the app's developer reads them in gameModelEvents and computeModuleRuns.

That is deliberate, and the reason is worth stating: the caller of these operations is usually a player, not the developer. A player shown Evaluation timed out learns nothing they can act on, and the game that displayed it has put the platform's words on its own screen. Blame attribution is the platform's job; presentation is yours.

blame answers the one question a client cannot answer for itself:

extensions.blameMeaningWhat a game should usually do
PLATFORMOurs. The app's code may not have run at all.Retry when retryable; otherwise say something went wrong on our side.
AUTHORThe app's own code or configuration. Repeating the call gets the same answer.Do not retry. Show your own wording for "that did not work".
BUDGETA metered allowance for the app or the caller is spent. Nothing is broken.Back off. retryable says whether the allowance returns on its own.

retryable is about the caller's options, not about how long a fix takes: an open breaker is retryable because it closes itself after a cooldown, while a spent plan allowance is not, even though neither is a bug.

extensions.codeblameMeaning
USER_CODE_ERRORAUTHORThe app's own code failed while running.
USER_CODE_TOO_SLOWAUTHORIt ran past the time it is allowed.
USER_CODE_LIMIT_EXCEEDEDAUTHORIt exceeded a per-call ceiling (gas, fuel, memory, depth, database operations, response size).
INVALID_REQUESTAUTHORThe arguments did not satisfy the function's declared contract.
NOT_ALLOWEDAUTHORAn invoke policy or permission refused this caller. Applies to app admins too: since 2026-09-08 a manage_apps holder is judged like a player unless the input sets bypassPolicy: true, and that flag itself answers NOT_ALLOWED for anyone without manage_apps. On gameModelInvoke a policy refusal arrives in band (success: false, fault.code: NOT_ALLOWED); a refused bypassPolicy is a GraphQL error.
NOT_FOUNDAUTHORThe named function, module or export does not exist for this app.
PLATFORM_BUSYPLATFORMWe could not start the work in time. The app's code never ran. Retry.
PLATFORM_ERRORPLATFORMA platform failure. Retrying is reasonable.
TEMPORARILY_DISABLEDeitherA breaker is open, or an operator switch is off. blame distinguishes them.
BUDGET_EXCEEDEDBUDGETA per-minute allowance is spent; it returns on the next window.
RATE_LIMITEDBUDGETThis caller is asking too often. extensions.retryAfterMs when known.
QUOTA_EXHAUSTEDBUDGETA metered allowance is spent and does not return on its own.
WALLET_EMPTYBUDGETThe calling player's own wallet is at or below zero, so their grid code is paused. Top up (createCheckout with PLAYER_WALLET_TOPUP) or enable setPlayerAutoBilling; not retryable until funded.
SPEND_CAP_REACHEDBUDGETA spend cap the player set on themselves (setPlayerSpendCap) is reached for the current period. Raise or clear it, or wait for the period boundary.
WRONG_DATACENTERPLATFORMThis app is served elsewhere. extensions.gameApiUrl names where; move and retry.
APP_UNAVAILABLEPLATFORMThe app's datacenter has no instance able to serve. No endpoint is named, on purpose — do not fall back to a cached one, it is in the datacenter that is down.
NO_LOCAL_BUDDYPLATFORMYou are on the app's own datacenter and it has no healthy UDP server. No endpoint is named, because there is nowhere else to go — a Buddy elsewhere would make every gameplay write cross a WAN, invisibly, because each write still succeeds. Retry, and report it: this one needs an operator. If you called serverWithLeastClients on the wrong datacenter you get WRONG_DATACENTER instead, with an endpoint to move to.

gameModelInvoke reports a gameplay verdict in band, not as an error. An authority denial or an evaluation failure is a verdict, so the mutation succeeds and the result carries success: false with a fault { code blame retryable } object. It also carries the event id and any writes that did apply, which is why it is not thrown. computeInvoke and playerComputeInvoke have no result to return on failure and therefore throw, with the same three values in extensions.

A PLATFORM-blamed refusal is the exception, and gameModelInvoke throws it. When the platform declines to start the work — no connection available, or a contended property whose lock could not be taken in time — there is no result to report in band: the whole transaction rolled back and no event row was written, deliberately, so that a refusal we issued cannot trip the app's own circuit breaker. So handle both carriers on this field: success: false with a fault, and a thrown error whose extensions carry the same blame and retryable. Branching on those two is what stays correct; they are the contract, and a refusal that is ours is always blame: PLATFORM with retryable: true.

In CrowdyJS, playerFaultOf(errorOrResult) reads both carriers and returns one { code, blame, retryable }, and a thrown fault arrives as CrowdyUserCodeFaultError (a subclass of CrowdyGraphQLError, so existing handlers keep working).

{
"errors": [
{
"message": "The service is busy. Please try again in a moment.",
"path": ["computeInvoke"],
"extensions": {
"code": "PLATFORM_BUSY",
"blame": "PLATFORM",
"retryable": true,
"remediation": "Ours, not the app's: the work could not be STARTED in time."
}
}
],
"data": null
}

GmInvokeResult.errorMessage still exists and is deprecated. It now carries a platform-authored sentence matching fault rather than the engine's text, so it is safe to show a player as-is — but prefer fault and your own wording.

Agentic Crowdy Studio stable errors

Agent failures use stable AGENT_* codes both at the GraphQL boundary and inside typed run/tool events. message is safe explanatory text; branch on code plus retryable, and use remediation / requiredScope when present.

CodesMeaning and action
AGENT_DISABLED, AGENT_OPERATOR_KILLED, AGENT_PERMISSION_DENIED, AGENT_SCOPE_DENIED, AGENT_MODEL_NOT_ALLOWEDPolicy or authority does not allow the operation. Do not retry until an authorized human/operator changes the relevant state.
AGENT_DISCONNECTED, AGENT_CLIENT_REATTACHED, AGENT_CLIENT_EPOCH_STALE, AGENT_EVENT_GAPLocal control is already cleared. Attach a fresh epoch, replay/fill durable history, then require explicit human resume; Play needs a new lease.
AGENT_CONTEXT_CHANGED, AGENT_CONTEXT_STALE, AGENT_HOST_CAPABILITY_CHANGED, AGENT_OBSERVATION_STALE, AGENT_CONTROL_TARGET_CHANGED, CROWDY_STUDIO_REVISION_CONFLICTRefetch the project/game/host context. Never apply an old approval, lease, observation, or revision.
AGENT_APPROVAL_REQUIRED, AGENT_APPROVAL_MISMATCH, AGENT_APPROVAL_EXPIRED, AGENT_APPROVAL_DENIED, AGENT_APPROVAL_REVOKEDShow the exact current safe summary/hash or return control to the human. Never approve automatically.
AGENT_LEASE_REQUIRED, AGENT_LEASE_EXPIRED, AGENT_LEASE_REVOKED, AGENT_LEASE_SCOPE_MISSINGNo valid control scope exists. Stop intent; only a human can grant a new Play lease.
AGENT_BUDGET_EXHAUSTED, AGENT_QUOTA_EXHAUSTED, AGENT_RATE_LIMITED, AGENT_PROVIDER_UNAVAILABLEStop the current run. Retry only when retryable and after current budget/quota/policy revalidation.
AGENT_TOOL_UNKNOWN, AGENT_TOOL_VERSION_UNSUPPORTED, AGENT_TOOL_INPUT_INVALID, AGENT_TOOL_OUTPUT_INVALID, AGENT_TOOL_FAILED, AGENT_TOOL_TIMEOUTTreat the exact descriptor/schema as authoritative. Do not invent fallback tools or raw API calls.
AGENT_TOOL_OUTCOME_UNKNOWNThe effect may have happened. Inspect authoritative state and do not blind-retry.

See Agentic Crowdy Studio for reconnect, approval, checkpoint, budget, and human-takeover semantics.

The portal browser handoff has a consent gate for untrusted apps. Minting a portal authorization code (createPortalAuthorizationCode) for an untrusted app the user has not authorized fails with a FORBIDDEN error whose message is prefixed CONSENT_REQUIRED:

{
"errors": [
{
"message": "CONSENT_REQUIRED: the user has not authorized this app. Call authorizeApp first.",
"path": ["createPortalAuthorizationCode"],
"extensions": { "code": "FORBIDDEN", "httpStatus": 403 }
}
],
"data": null
}

CONSENT_REQUIRED is not a distinct extensions.code — it is a FORBIDDEN whose message carries the marker. Resolve it on the Overworld by checking portalConsent(appId) { consentRequired } and recording approval with authorizeApp(input:{ appId }) before retrying. Trusted apps (the Overworld is app 1) skip consent entirely. CrowdyJS detects this proactively: client.portal.handleAuthorizeRequest throws PortalConsentRequiredError (pass grantConsent: true to approve). The requested redirectUri must also be in the app's redirectUris allow-list, or the call is rejected (BAD_REQUEST). See Portals & app-scoped tokens.

Realtime / UDP errors

The Game API UDP-proxy surfaces server-side spatial errors asynchronously on the udpNotifications subscription, not as GraphQL errors. A spatial-send mutation returning true only means the datagram was accepted for sending.

GenericErrorResponse.errorCode (UdpErrorCode)

Correlate to the request that caused it by sequenceNumber.

CodeMeaningRemediation
NO_ERRORSuccess.
UNKNOWN_ERRORUnspecified server error.Retry; report if persistent.
INVALID_TOKENThe token is malformed, revoked, or not a valid app-scoped gameplay token.Mint a fresh app-scoped token (mintAppToken / exchangePortalCode, or refreshAppToken for the same app); the identity session token is not valid here.
APP_NOT_FOUNDNo app matches the supplied appId.Verify appId.
UNAUTHORIZEDMissing the runtime/grid permission for this action.May be transient on first entry to a new region while grid permissions load — retry shortly; otherwise obtain the permission.
GAME_TOKEN_WRONG_SIZEThe token is not the expected length.Send the exact 64-character app-scoped token from mintAppToken / exchangePortalCode (no trimming/re-encoding).
INVALID_REQUESTThe message was malformed or failed validation.Check the message shape / arguments.
INVALID_APP_IDappId was missing, zero, or invalid — or the token is not scoped to the packet's app (app-scoped token confinement).Supply a valid appId, and use the token minted for that app.
USER_NOT_AUTHENTICATEDNo session on the server for this client.Open the UDP proxy (connectUdpProxy) or complete the native token handshake first.
TOKEN_EXPIREDThe app-scoped gameplay token's TTL elapsed mid-session.Refresh the app token (same app: refreshAppToken) before it lapses, or re-portal through the Overworld for a fresh one, then re-authorize the session.

The full enum (including login-validation codes that never appear on the UDP wire) is in the Game API SDL as UdpErrorCode, each value documented. The native-UDP view of these codes is in Operations and Wire formats.

RealtimeConnectionEvent.code

Emitted when the realtime session itself cannot be established (distinct from a per-message error):

CodeMeaningRemediation
AUTH_REQUIREDThe subscription opened without a valid bearer token.Send the token in the connection_init payload.
APP_ID_REQUIREDThe subscription was app-agnostic.Scope the subscription to one appId (run one client per app).
APP_TOKEN_REQUIREDThe subscription presented an identity session token, not an app-scoped gameplay token.Obtain a token scoped to this app (portal in via the Overworld, or mintAppToken) and use it for gameplay.
APP_SCOPE_MISMATCHThe token is scoped to a different app than the subscription's appId.Use the token minted for the app you are subscribing to.
UDP_PROXY_CONNECTION_FAILEDThe server could not open the upstream UDP proxy session.Inspect retryable; back off and retry if true.

Native UDP: silent drops

On the native Replication API, some failures produce no reply at all — a missing or invalid HMAC, an unknown token, or an unparseable packet is dropped without a NAK. Do not treat silence as a network black hole. If you sent an authenticated message and receive neither a notification nor a GENERIC_ERROR_MESSAGE, re-check the HMAC and token before assuming packet loss. (This does not apply to the GraphQL UDP-proxy path, which authenticates at connect time.) See Troubleshooting.