For AI agents and integrators
This page is the fastest path for an autonomous agent (or any backend integrator) to call Crowded Kingdoms correctly. It links the machine-readable schema, states the global conventions, and walks the three most common end-to-end workflows.
This page is for external API clients and coding agents. The in-product
Agentic Crowdy Studio model runtime is a separate, allowlisted development
contract on the current CK API + CrowdyJS 15.x line (not the retired
v0.1.94 / CrowdyJS 12.0.0 train): use its
player/SDK guide,
game-host guide, and
operator runbook. It never gives a model
the raw GraphQL access described below.
The surface at a glance
There is one GraphQL endpoint. The two rows below are surfaces of that API,
not two hosts. Do not set managementUrl. Gameplay is PostgreSQL + Citus, not
galaxy. CrowdyJS is on the 15.x line: 15.0.0 removed devLogin and added
auth.login / auth.register; later 15.x releases added password-management
wrappers and rebuilt baked default origins after every tier moved onto the
brand root — an older build dials a host that no longer exists. Do not
hardcode the npm version here — npm view @crowdedkingdoms/crowdyjs dist-tags.
The dist-tag decides the default origin. latest is built for production;
@dev / @test are separate prereleases whose builds bake their own tier's
host. Installing latest and pointing it at a non-production tier works only
if you pass the origin explicitly.
| API | Protocol | Use it for |
|---|---|---|
| Management surface | GraphQL (HTTP) | Identity, organizations, the apps marketplace, access tiers, billing, payments, quotas. Studio-backend operations. Dedicated customer environments were retired. |
| Game surface | GraphQL (HTTP + WebSocket) | Runtime world data (chunks, voxels, actors, avatars) and the realtime UDP-proxy subscription/spatial-send surface. Authenticated with an app-scoped token (not the identity session token). |
| Replication API (Buddy) | Binary UDP | Lowest-latency native spatial replication. Most clients use the Game API UDP-proxy instead and never touch raw UDP. Buddy authenticates only app-scoped tokens. |
| CrowdyJS | TypeScript SDK | Browser clients — wraps sign-in (auth.login / auth.register, magic link, social), client.portal (app-scoped tokens / PKCE / consent), and the unified GraphQL API including the UDP proxy. Use one identity client plus a per-game client. Prefer it for web. |
Get the schema
- Management API SDL:
/schema/management-api.graphql - Game API SDL:
/schema/game-api.graphql - CrowdyJS SDL:
/schema/crowdyjs.graphql - Introspection is enabled on the dev tier; each GraphQL API serves a Playground at its
/graphqlendpoint.
Every machine-readable index is at /llms.txt.
Global conventions
- Auth (two tokens): obtain an identity session token with email +
password (
registerfor a new account,loginfor an existing one), a magic link (requestLoginLink→completeLoginLink), or a social provider (socialLoginStart→socialLoginComplete; enabled providers inavailableLoginProviders). Password is the one that needs no inbox and no browser, so it is usually the right choice for an agent. That session token is a management-plane credential and is rejected for gameplay; the Game API + realtime/UDP surface require an app-scoped token confined to one app. Mint one from the session token withmintAppToken(input:{ appId })(or the PKCE portal flow across origins, which adds a consent gate for untrusted apps), then sendAuthorization: Bearer <token>— the session token on Management API requests, the app-scoped token on Game API HTTP requests and in thegraphql-transport-wsconnection_initpayload. App tokens expire (~30 min) —refreshAppTokenbefore then. See Sign in, Portals & app-scoped tokens, and Game API → Authentication. BigIntis transmitted as a decimal string in both directions (e.g."123"), never a JSON number — this avoids precision loss. IDs (appId,orgId,userId) areBigInt.DateTimeis an ISO-8601 UTC string, e.g.2026-06-12T18:00:00Z.- Money fields named
*Centsare in minor currency units (cents); pair them with the adjacentcurrencyfield. - Errors are structured —
extensions.code(+remediation, andrequiredPermissionfor auth failures). See Error codes. - Pagination: prefer the Relay
*Connectionqueries (first/after); offset args are deprecated. See Pagination. - Rate limits / cost — see Rate limits.
- Idempotency: economy-sensitive and destructive mutations accept an optional
idempotencyKey(on theinputobject or as a top-level arg). Send one when retrying so a retry replays the first result instead of double-applying; a different payload under the same key returnsIDEMPOTENCY_CONFLICT. The realtimesequenceNumberis correlation only, not deduplication. - Permissions are machine-readable: each guarded field carries a
@requiresPermission(scope:, permission:, scopeArg:)directive in the SDL/introspection.
Get a sandbox key
Use the dev tier to sign in, create an org and an app, and obtain your identity
session token without touching production data (then mintAppToken per app for
gameplay). register(registerUserInput:{ email, password }) gives an agent a session
in one call on any environment, and needs no inbox — see
Sign in.
There is no dev bypass to shortcut this. devLogin and the devToken field were
deleted on 2026-08-20; every environment authenticates the same way.
Start at Dev tier and
Create your first app.
Workflow 1 — realtime gameplay (recommended path)
# 1) Authenticate (Management API). Returns the identity SESSION token.
# `register` for an address that has never been seen; `login` to return to an
# existing account. Magic link and social are the other two paths and need an
# inbox or a browser. See /management-api/authentication.
mutation Register {
register(registerUserInput: { email: "player@example.com", password: "..." }) {
token # session token — management-plane only; mint an app token next
}
}
# 2) Mint an app-scoped token (Management API, session token as the Bearer). Use THIS
# token for every Game API HTTP request and the realtime connection_init. (Browser
# games at a different origin use the PKCE portal flow instead — see CrowdyJS.)
mutation Enter {
mintAppToken(input: { appId: "1" }) {
token expiresAt gameApiUrl gameApiWsUrl discoveryUrl
}
}
# 3) Per-app bootstrap (Game API, app-token Bearer): version requirements, UDP state, spatial limits.
query Bootstrap {
gameClientBootstrap(appId: "1") {
sequenceNumberModulo
maxReplicationDistance
maxDecayRate
udpProxyConnectionStatus { connected }
versionInfo { minimumClientVersion }
}
}
# 4) Open the UDP proxy (idempotent) and subscribe (appId-scoped).
mutation Connect { connectUdpProxy }
subscription Realtime {
udpNotifications {
__typename
... on ActorUpdateNotification { uuid chunkX chunkY chunkZ state sequenceNumber epochMillis }
... on GenericErrorResponse { sequenceNumber errorCode }
... on RealtimeConnectionEvent { status code message retryable }
}
}
# 5) Send a spatial update. The Boolean result means "accepted for sending" — the
# applied result (or an error) arrives asynchronously on the subscription above,
# correlated by sequenceNumber.
mutation Move {
sendActorUpdate(input: {
appId: "1",
chunk: { x: "0", y: "0", z: "0" },
uuid: "0123456789abcdef0123456789abcdef",
state: "AA==", # base64 app-defined actor state
distance: 8, decayRate: 1, sequenceNumber: 1
})
}
Key behavior: sendActorUpdate returning true only means the datagram was accepted —
the server applies it asynchronously and reports failures as a GenericErrorResponse
carrying the same sequenceNumber. Unsubscribing stops delivery but does not close the
session; call disconnectUdpProxy to close it. Browser clients should use
CrowdyJS instead of hand-writing this.
Workflow 2 — native UDP (advanced, lowest latency)
For native clients that bypass the GraphQL proxy: sign in for the identity
session token, mint an app-scoped token for the app (mintAppToken), discover a server with the
Game API serverWithLeastClients (app-token Bearer), then send the binary long-spatial
message to the server's client port 9091 with an HMAC computed from your 64-octet
app-scoped token. Refresh before expiresAt (refreshAppToken) or Buddy drops the
session with TOKEN_EXPIRED. Full protocol: Replication API,
Authenticate and assign,
Wire formats, HMAC. Native
clients never send the internal peer handshake.
Workflow 3 — studio backend (economy and access)
Server-side automation (with a studio/org token) typically:
# Create an app, then a paid access tier for it.
mutation NewApp { createApp(input: { orgId: "10", name: "My Game", slug: "my-game" }) { appId } }
mutation NewTier {
createAccessTier(input: { appId: "42", name: "Premium", priceCents: "999", permissionKeys: ["access"] }) { id }
}
# Check an org wallet (minor units), then open a top-up checkout.
query Wallet { walletBalance(orgId: "10") { balanceCents currency } }
mutation TopUp {
createCheckout(input: { purpose: ORG_WALLET_TOPUP, orgId: "10", amountCents: "5000", provider: STRIPE }) {
externalUrl status
}
}
# Grant a user access to an app (entitlement change that propagates to the game runtime).
mutation Grant { grantAppAccess(input: { appId: "42", userId: "777", tierId: "5" }) { status } }
The exact input fields are authoritative in the Management API SDL
and reference — every field, argument, and enum value carries a description, including the
required org/app permission and any side effects. Redirect the user to externalUrl to
complete a checkout; wallet credit reconciles via webhook, not the redirect.
Agent gotchas
- Retry safely with an idempotency key. Economy-sensitive and destructive mutations
accept an optional
idempotencyKey; pass one (a UUID per logical operation) so a retriedcreateCheckout/grant/etc. replays the first result instead of double-applying. Without a key, a retried non-idempotent mutation can double-create. - Permissions are per field and machine-readable. Each guarded field has a
@requiresPermissiondirective (and names the permission in its description). A read-only or wrong-scope token getsFORBIDDEN(withextensions.requiredPermission) orSCOPE_MISSING. - Game vs management surface. Identity/billing operations only work on the Management
API; calling them on the Game API returns
FORBIDDEN/not-found. Route accordingly. - Consent gate on the portal handoff. Minting a browser portal code for an untrusted
app fails with a
FORBIDDENerror whose message is prefixedCONSENT_REQUIREDuntil the user authorizes it. CheckportalConsent(appId)and callauthorizeAppfirst; trusted apps (e.g. the Overworld, app 1) skip consent. The requestedredirectUrimust also be in the app'sredirectUrisallow-list. See Error codes → Consent. - Silent drops on native UDP. A failed HMAC or bad token can be dropped with no NAK — do not treat silence as packet loss. (Not applicable to the GraphQL proxy path.)