Skip to main content

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.

APIProtocolUse it for
Management surfaceGraphQL (HTTP)Identity, organizations, the apps marketplace, access tiers, billing, payments, quotas. Studio-backend operations. Dedicated customer environments were retired.
Game surfaceGraphQL (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 APIBinary UDPLowest-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 gatewayWebSocket (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.
CrowdyJSTypeScript SDKBrowser 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, CrowdyPyC++ and Python SDKsNative games and tools: the same surface as CrowdyJS, with native UDP replication. See CrowdyCPP and CrowdyPy.

Get the schema​

Every machine-readable index is at /llms.txt.

Global conventions​

  • Auth (two tokens): obtain an identity session token with email + password (register for a new account, login for an existing one), a magic link (requestLoginLink → completeLoginLink), or a social provider (socialLoginStart → socialLoginComplete; enabled providers in availableLoginProviders). 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 with mintAppToken(input:{ appId }) (or the PKCE portal flow across origins, which adds a consent gate for untrusted apps), then send Authorization: Bearer <token> — the session token on Management API requests, the app-scoped token on Game API HTTP requests and in the graphql-transport-ws connection_init payload. App tokens expire (~30 min) — refreshAppToken before 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, scope players:mint). The secret starts with cko_, stays on the server, and is refused when a request carries an Origin header;
    • 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.

  • 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, and LEGAL_ACCEPTANCE_REQUIRED with extensions.missingDocuments; for a player under 18, also SAFETY_LOG_REQUIRED and SAFETY_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.

  • BigInt is transmitted as a decimal string in both directions (e.g. "123"), never a JSON number — this avoids precision loss. IDs (appId, orgId, userId) are BigInt.

  • DateTime is 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 — as BigInt decimal strings. Nothing is rounded: a model turn that cost $0.000117 is a -117 on the ledger, and holdsMicrousd is what in-flight requests have reserved (spendable = balanceMicrousd - holdsMicrousd). The older *Cents wallet fields are deprecated and derived (micro-USD ÷ 10,000, truncated). Processor-facing amounts (amountCents on checkouts, priceCents on access tiers) stay in cents because Stripe and PayPal settle in cents.

  • Errors are structured — extensions.code (+ remediation, and requiredPermission for auth failures). See Error codes.

  • Pagination: prefer the Relay *Connection queries (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 the input object 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 returns IDEMPOTENCY_CONFLICT. The realtime sequenceNumber is 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.

# 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 retried createCheckout/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 @requiresPermission directive (and names the permission in its description). A read-only or wrong-scope token gets FORBIDDEN (with extensions.requiredPermission) or SCOPE_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 FORBIDDEN error whose message is prefixed CONSENT_REQUIRED until the user authorizes it. Check portalConsent(appId) and call authorizeApp first; trusted apps (the identity hub, app 1) skip consent. The requested redirectUri must also be in the app's redirectUris allow-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.)