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: use its player/SDK guide and game-host guide. It never gives a model the raw GraphQL access described below.
Gameplay authority: Best practices.
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. Do not hardcode an SDK version —
npm view @crowdedkingdoms/crowdyjs dist-tags. latest targets production;
pass httpUrl / wsUrl explicitly if you are not on production.
| 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 | Binary UDP | Lowest-latency native spatial replication. Most clients use the Game API UDP-proxy instead and never touch raw UDP. Buddy (the replication server) authenticates only app-scoped tokens. |
| ck-exec gateway | WebSocket (binary frames, MessagePack payloads) | Calls to your app's server code (hubs and spokes) and pushes from its topics. execConnect, with the app-scoped token, returns the gateway and a 60-second connect token. See ck-exec. |
| 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 and ck-exec (client.exec). Use one identity client plus a per-game client. Prefer it for web. |
| CrowdyCPP, CrowdyPy | C++ and Python SDKs | Native games and tools: the same surface as CrowdyJS, with native UDP replication. See CrowdyCPP and CrowdyPy. |
Get the schema
- Management API SDL:
/schema/management-api.graphql - Game API SDL:
/schema/game-api.graphql - CrowdyJS SDL:
/schema/crowdyjs.graphql - Production serves no GraphiQL and does not answer introspection: read the SDL above.
- Unreal SDK: For AI agents (Unreal SDK) and the drop-in
/helpers/unreal-sdk/AGENTS.md.
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. -
Players of an organization that runs its own accounts (org-managed players): do not ask them for a Crowded Kingdoms password inside the game. The organization signs them in itself, by one of two routes:
- its server mints each player's app token with an org token
(
mintManagedPlayerToken, scopeplayers:mint). The secret starts withcko_, stays on the server, and is refused when a request carries anOriginheader; - its sign-in service signs a short assertion that the player's device trades with
exchangeManagedPlayerAssertion.
Either way the device then plays with an ordinary app-scoped token. Crowded Kingdoms switches the feature on per organization, and an admin of the organization first accepts the Org-Managed Players Addendum in Studio. Until then those calls answer
ORG_LEGAL_ACCEPTANCE_REQUIRED, which neither the player nor a retry can fix. See Org-managed players. - its server mints each player's app token with an org token
(
-
Who may play (Game API v2.60.0): every sign-up, mint and refresh checks the player's country, then their age, then their documents:
TERRITORY_NOT_SUPPORTED,AGE_REQUIRED,AGE_REQUIREMENT_NOT_MET, andLEGAL_ACCEPTANCE_REQUIREDwithextensions.missingDocuments; for a player under 18, alsoSAFETY_LOG_REQUIREDandSAFETY_MODERATOR_REQUIRED(v2.61.0). None is fixed by retrying: an age refusal names no age and locks the same client or player for 24 hours. Crowded Kingdoms admits players from 13 (since 2026-11-04). See Players under 18 and player safety. -
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. Wallet balances, holds and ledger amounts are micro-USD — fields named
*Microusd, where 1 USD =1000000— asBigIntdecimal strings. Nothing is rounded: a model turn that cost $0.000117 is a-117on the ledger, andholdsMicrousdis what in-flight requests have reserved (spendable =balanceMicrousd - holdsMicrousd). The older*Centswallet fields are deprecated and derived (micro-USD ÷ 10,000, truncated). Processor-facing amounts (amountCentson checkouts,priceCentson access tiers) stay in cents because Stripe and PayPal settle in cents. -
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 key
Use production, as any developer does: sign in, create an org and an app (the free
tier covers it), and obtain your identity session token (then mintAppToken per app for
gameplay). register(registerUserInput:{ email, password }) gives an agent a session in
one call and needs no inbox — see Sign in.
Crowded Kingdoms' dev and test environments are internal and refuse outside accounts
with TIER_ACCESS_REQUIRED.
There is no unauthenticated shortcut. Start at Connecting game clients 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 client 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:
# Pick a datacenter (a `code` whose `placeable` is true), create an app there,
# then an access tier, then give the tier a price.
query Datacenters { placeableDatacenters { datacenters { code placeable } } }
mutation NewApp { createApp(input: { orgId: "10", name: "My Game", slug: "my-game", datacenter: "or" }) { appId } }
mutation NewTier {
createAccessTier(input: { appId: "42", name: "Premium", permissionKeys: ["access"] }) { tierId }
}
mutation PriceTier {
updateAccessTier(tierId: "7", input: { priceCents: "999", currency: "usd", billingPeriod: "month" }) { tierId priceCents }
}
# Check an org wallet (micro-USD), then open a top-up checkout (cents: the processor's unit).
query Wallet { walletBalance(orgId: "10") { balanceMicrousd holdsMicrousd 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 (the identity hub, 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.)