Errors & status codes
Every Asobi REST endpoint returns a JSON body of the shape {"error": "<reason>", ...} on failure. The reason atom is stable; the HTTP status indicates the class.
Status codes the runtime emits
Status | Class | Reason atoms (examples) | Where it comes from
--------|-------------------------|------------------------------------------------------------|------------------------------------------------
400 | Bad request | content_empty, invalid_perm, invalid_quantity, | Controllers reject malformed payloads or
| | bad_data, channel_id_invalid, body_too_large | invalid query params before any DB work.
401 | Unauthenticated | invalid_token, expired_token | asobi_auth_plugin / IAP receipt validation.
403 | Forbidden | not_member, not_owner, not_match_participant, | Caller is authenticated but lacks the right
| | last_auth_method, group_full, friendship_self | (channel membership, ticket ownership, etc).
404 | Not found | match_not_found, world_not_found, ticket_not_found, | Resource lookup miss.
| | save_not_found, group_not_found
409 | Conflict | already_friends, username_taken, world_already_owned | Idempotent-ish endpoints flag duplicate state.
413 | Payload too large | content_too_large, save_too_large | Body exceeded the per-endpoint cap (DM 2000B,
| | | save 256KB, etc).
429 | Too many requests | rate_limited, world_cap_exceeded | Seki limiter or per-player world cap hit.
500 | Internal error | internal_error | Unexpected crash in a controller; logged with
| | | a correlation id, never leaks internals.
503 | Service unavailable | world_global_cap | Global world cap hit (operator should raise
| | | world_max in sys config).Per-endpoint specifics
Auth (/api/v1/auth/*)
401+invalid_credentials— login failed. Rate-limited at 5 req/sec/IP.409+username_taken— register against an existing username.403+last_auth_method— unlinking the only remaining auth method (would lock the player out).
IAP (/api/v1/iap/*)
400+invalid_jws— Apple receipt failed any of header alg, x5c chain, or signature checks. Reason atom is sanitised; full detail stays in server logs.400+invalid_ticket_format— Steam ticket failed hex/length validation.
World (/api/v1/worlds)
429+world_cap_exceeded— player already ownsworld_max_per_playerworlds (default 5).503+world_global_cap— globalworld_maxhit (default 1000). Operators should raise this and possibly add nodes.
Storage / saves
413+save_too_large— save body > 256 KB.400+slot_cap— player already has 10 slots.400+invalid_perm—read_perm/write_permmust be"public"or"owner".
Chat / DM
400+channel_id_invalid— channel id must start with one ofdm:,world:,zone:,prox:,room:.403+not_member— fetching history for a channel you don't belong to.413+content_too_large— DM content > 2000 bytes.400+too_many_channels— more than 32 channels joined on one WS connection.
Matchmaker
403+not_owner— fetching or cancelling a ticket the caller didn't create. Ticket reads / cancellations require ownership.
Voting
403+not_match_participant— the caller is not in the match they are trying to vote in.
WebSocket frame errors
Errors on a WebSocket are returned as a frame of type error, not as an HTTP status. Common reasons:
{"type": "error",
"payload": {"reason": "rate_limited", "context": "chat.send"}}unauthenticated— sent a non-session.connectframe as the first message.channel_id_invalid,too_many_channels,not_member— chat-related, mirror the REST shapes above.unknown_type— message type the runtime does not recognise; safe to ignore client-side.
Where next?
- Auth & rate limiting — the rationale and tunables for the codes above.
- REST API reference
- WebSocket protocol