Reference
API Reference
Everything the CLI, SDK, and portal UI do goes through these HTTP APIs. Authenticated endpoints accept Authorization: Bearer mtl_api_… (scoped) or mtl_cli_… tokens — see API Tokens. Three origins are involved: the portal (https://www.metaloot.app), the Studio (https://studio.metaloot.app), and your game's own domain (<slug>.metaloot.app).
OAuth (Portal)#
The identity provider behind Sign in with Metaloot — full protocol walkthrough in Authentication.
| GET /oauth/authorize | Authorization endpoint. Params: response_type=code, client_id, redirect_uri, scope, state. |
| POST /api/oauth/token | Exchange an authorization code (form-encoded: grant_type, client_id, client_secret, code, redirect_uri). Server-side only. |
| GET /api/oauth/userinfo | The player profile for a bearer access token; sub is the stable player id. |
Portal API#
| GET /api/cli/whoami | Identify the bearer token → { user: { id, name, email? }, tokenLabel }. The cheapest way to validate a token. |
| DELETE /api/cli/tokens | Revoke the calling token (what metaloot logout uses). |
| GET | POST /api/tokens · DELETE /api/tokens/{id} | Manage scoped API tokens (browser session required) — see API Tokens. |
| POST /api/games | Create a game: { title, description, url?, tags[] }. Scope: games:write. |
| PATCH /api/games/{id} | Update title, description, url (re-syncs the auth redirect URI), or playable. Scope: games:write. |
| POST /api/oauth/apps | Issue OAuth credentials for a game: { name, homepageUrl, gameId, redirectUris } → client ID plus one-time secret. Scope: oauth:write. |
| POST /api/deploy | Start a deploy: returns { deployId, uploads: [{ path, uploadUrl }] } presigned URLs. Scope: deployments:write. |
| POST /api/deploy/{deployId}/finalize | Atomically flip the site live: returns { url, playUrl, manageUrl, auth, multiplayer }. Scope: deployments:write. |
curl https://www.metaloot.app/api/cli/whoami -H "Authorization: Bearer mtl_api_…"Studio Assets API#
Public asset endpoints send Access-Control-Allow-Origin: * (GET/HEAD/OPTIONS), so games can hot-link them from any origin. Private assets return the same headers only to their owner (bearer token with assets:read) and 404 to everyone else.
| GET /api/assets/{id-or-slug}/file | The hosted file. Query: variant=auto|source|lod (default auto; response carries X-Metaloot-Variant), and path=<file> for one file out of a pack (omit for the pack ZIP). |
| GET /api/assets/{id}/animation/{preset} | A rigged animation preset as its own GLB (rigged model + that clip). |
| Asset metadata & listing | Use the SDK's getAsset(), listAssets(), and getAssetManifest(), or the CLI with --json — the underlying routes are versioned with the SDK. |
Cache behavior:
| public assets | Cache-Control: public, max-age=86400 |
| private assets | Cache-Control: private, no-store |
| curated pack files | Cache-Control: public, max-age=31536000, immutable |
Game Domain Routes#
Provisioned on every deployed game at https://<slug>.metaloot.app, alongside your static files:
| GET /auth/metaloot/start | Begin sign-in (redirects to the portal). |
| GET /auth/metaloot/callback | OAuth callback — sets the signed session cookie. |
| GET /auth/metaloot/session | Current session: { signedIn, user, scope, expiresAt } or { signedIn: false }. |
| GET | POST /auth/metaloot/logout | Clear the game session. |
| GET /mp | Multiplayer availability: { enabled, version, rooms, client, docs }. |
| wss://…/mp/rooms/{roomId} | Room WebSocket. Authenticated by the same-origin session cookie; unauthenticated upgrades get 401 { error: "auth_required", signIn: "/auth/metaloot/start" }. Room ids match [A-Za-z0-9_.~-]{1,64}. |
| GET /__metaloot/multiplayer.js | Zero-install multiplayer client (ES module). |
| GET /__metaloot/auth-widget.js | The injected sign-in widget. |
| GET /__metaloot/assets/{id-or-slug}.glb | Same-origin, edge-cached proxy to the Studio file endpoint. Public assets only; Access-Control-Allow-Origin: *, max-age=86400. |
/auth/metaloot/, /mp/, or /__metaloot/. Everything else falls through to your static files with SPA fallback to index.html.