API Docs

OAuth for apps

Crateyard is an OAuth 2.1 authorization server for MCP clients such as ChatGPT. Clients use the authorization code flow with PKCE (S256) and get short-lived bearer tokens for the MCP endpoint. Users approve each app on a consent page and can disconnect it at any time in Settings > Connected Apps.

These endpoints are off unless the server sets OAUTH_SERVER_ENABLED=true. When off, every endpoint here returns 404.

Placeholders below: https://crateyard.com is the site origin, for example https://crateyard.example.

Scopes

  • assets:read: list and view assets, tasks, pixel sprites, pixel animations, and the account name.
  • assets:write: create and rename assets. No tokens are spent.
  • generate: start image, 3D, and pixel animation generations. These spend the user's app tokens.

If a client asks for no scope, it gets assets:read assets:write. generate is only granted when requested and approved.

GET /.well-known/oauth-protected-resource

Protected resource metadata (RFC 9728). Also served at /.well-known/oauth-protected-resource/mcp. No authentication.

{
  "resource": "https://crateyard.com/mcp",
  "authorization_servers": ["https://crateyard.com"],
  "scopes_supported": ["assets:read", "assets:write", "generate"],
  "bearer_methods_supported": ["header"],
  "resource_documentation": "https://crateyard.com/docs/api"
}

GET /.well-known/oauth-authorization-server

Authorization server metadata (RFC 8414). No authentication.

{
  "issuer": "https://crateyard.com",
  "authorization_response_iss_parameter_supported": true,
  "authorization_endpoint": "https://crateyard.com/oauth/authorize",
  "token_endpoint": "https://crateyard.com/oauth/token",
  "registration_endpoint": "https://crateyard.com/oauth/register",
  "revocation_endpoint": "https://crateyard.com/oauth/revoke",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_methods_supported": ["none"],
  "code_challenge_methods_supported": ["S256"],
  "scopes_supported": ["assets:read", "assets:write", "generate"]
}

POST /oauth/register

Dynamic client registration (RFC 7591). Public clients only. No authentication. Rate limit: 300 registrations per IP per hour.

Request body (JSON):

  • redirect_uris (required): 1 to 10 https URLs without fragments. http://localhost is accepted outside production.
  • client_name (optional): shown on the consent page.
  • token_endpoint_auth_method (optional): must be none if present.
curl -X POST "https://crateyard.com/oauth/register" \
  -H "Content-Type: application/json" \
  -d '{"client_name":"My MCP client","redirect_uris":["https://client.example/callback"]}'

Response 201:

{
  "client_id": "cy_client_...",
  "client_id_issued_at": 1760000000,
  "client_name": "My MCP client",
  "redirect_uris": ["https://client.example/callback"],
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"]
}

Errors: 400 invalid_redirect_uri, 400 invalid_client_metadata, 429 temporarily_unavailable.

GET /oauth/authorize

Opens the consent page in the user's browser. Users who are not signed in go to sign in (or sign up) first and then come back.

Query parameters: client_id, redirect_uri (must exactly match a registered URI), response_type=code, code_challenge, code_challenge_method=S256, state (recommended), scope (optional), resource (optional, must be https://crateyard.com/mcp).

After the user decides, the browser is redirected to redirect_uri with:

  • Approved: code, state, iss.
  • Declined: error=access_denied, state, iss.

Every redirect carries iss (RFC 9207). Codes are single use and expire after 10 minutes. Problems with client_id or redirect_uri show an error page and never redirect. Other problems redirect with error set to invalid_request, invalid_scope, invalid_target, or unsupported_response_type.

POST /oauth/token

Form-encoded or JSON. No client secret. Rate limit: 600 requests per minute per client ID and IP pair. Exceeding the limit returns 429 with Retry-After.

Authorization code exchange:

curl -X POST "https://crateyard.com/oauth/token" \
  -d grant_type=authorization_code \
  -d client_id=cy_client_example \
  -d code=cy_ac_example \
  -d redirect_uri=https://client.example/callback \
  -d code_verifier=YOUR_PKCE_VERIFIER \
  -d resource=https://crateyard.com/mcp

Refresh:

curl -X POST "https://crateyard.com/oauth/token" \
  -d grant_type=refresh_token \
  -d client_id=cy_client_example \
  -d refresh_token=cy_rt_example

Response 200:

{
  "access_token": "cy_at_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "cy_rt_...",
  "scope": "assets:read assets:write"
}

Access tokens last 1 hour. Refresh tokens last 30 days and rotate: each refresh returns a new pair and the old refresh token stops working. Presenting an already used code or refresh token revokes the whole connection, because it means the token leaked.

Errors (400 unless noted): invalid_request, invalid_grant, invalid_target, invalid_scope, unsupported_grant_type, 401 invalid_client, 429 temporarily_unavailable (120 requests per IP per minute).

POST /oauth/revoke

RFC 7009. Body: token (access or refresh token). Revoking any token disconnects the app for that user. Always returns 200, even for unknown tokens.

Token usage

OAuth endpoints never spend tokens. Only MCP tools with the generate scope do. See ChatGPT app.