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.code | Meaning | Remediation |
|---|---|---|
UNAUTHENTICATED | No bearer token, or it is invalid/expired. | For Management API calls, sign in again (passwordless: magic link, social, or devLogin) for the session token — see Sign in (passwordless). 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. |
FORBIDDEN | Authenticated, 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_MISSING | The 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_INPUT | An 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_REQUEST | The request was rejected by the resolver (e.g. a disabled feature, a precondition not met). | Read message; it states the precondition. |
GRAPHQL_VALIDATION_FAILED | The query/mutation document is invalid against the schema. | Validate against the published SDL or introspection. |
NOT_FOUND | The referenced entity does not exist or is not visible to you. | Verify the ID and that your token can see it. |
CONFLICT | The request conflicts with current state (incl. an idempotency key still processing). | Refetch and retry if appropriate. |
IDEMPOTENCY_CONFLICT | An 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. |
RATE_LIMITED | A rate/usage limit was exceeded. | Back off and retry with exponential backoff. |
INTERNAL_SERVER_ERROR | Unexpected server error. | Safe to retry idempotent reads; do not blind-retry non-idempotent mutations (send an idempotencyKey instead). |
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.
Consent
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.
| Code | Meaning | Remediation |
|---|---|---|
NO_ERROR | Success. | — |
UNKNOWN_ERROR | Unspecified server error. | Retry; report if persistent. |
INVALID_TOKEN | The 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_FOUND | No app matches the supplied appId. | Verify appId. |
UNAUTHORIZED | Missing 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_SIZE | The token is not the expected length. | Send the exact 64-character app-scoped token from mintAppToken / exchangePortalCode (no trimming/re-encoding). |
INVALID_REQUEST | The message was malformed or failed validation. | Check the message shape / arguments. |
INVALID_APP_ID | appId 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_AUTHENTICATED | No session on the server for this client. | Open the UDP proxy (connectUdpProxy) or complete the native token handshake first. |
TOKEN_EXPIRED | The 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):
| Code | Meaning | Remediation |
|---|---|---|
AUTH_REQUIRED | The subscription opened without a valid bearer token. | Send the token in the connection_init payload. |
APP_ID_REQUIRED | The subscription was app-agnostic. | Scope the subscription to one appId (run one client per app). |
APP_TOKEN_REQUIRED | The 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_MISMATCH | The 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_FAILED | The 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.