Docs / Errors

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 owns world_max_per_player worlds (default 5).
  • 503 + world_global_cap — global world_max hit (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_permread_perm/write_perm must be "public" or "owner".

Chat / DM

  • 400 + channel_id_invalid — channel id must start with one of dm:, 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.connect frame 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?