net_backend

API reference

Conventions & errors

2. Conventions#

JSON everywhere#

  • Request bodies are JSON with Content-Type: application/json (otherwise 415 unsupported_media_type). Routes with a JSON body need one even when every field is optional: send {}.
  • Every success is HTTP 200 with a JSON body. Routes that return nothing return {} (called Ack below).
  • Every 4xx / 5xx carries the error body.
  • Field names are snake_case. Key order means nothing.
  • Forward compatibility: ignore fields you do not know (a newer server may add fields). Optional fields may be absent or null; treat both the same. Unknown enum values (a room kind, a storage write rule), unknown error codes and unknown push kinds must not break a client.

Headers#

Header Direction Meaning
Authorization: Bearer <access token> request the caller (see Accounts and authentication)
Content-Type: application/json request on every request with a body
x-net-backend-protocol: 1 both the protocol version (optional on requests)
x-request-id answer the request's id; quote it when reporting a problem
ETag: "3" answer a storage object's version (single-object GET / PUT)
If-Match: "3" / If-None-Match: * request conditional storage writes (alternative to if_version)
Retry-After: <seconds> answer on some 429 / 503 answers
Allow answer on 405, the allowed methods

Ids#

User, room, message and audit ids are integers (64-bit signed, plain JSON numbers). They are assigned by the database, start at 1 and grow, so they fit JavaScript numbers in practice. Ids from other systems travel as strings (a Steam id: "76561197960287930"). WebSocket request ids are chosen by the client (unsigned integers; keep them below 2^53 in JavaScript).

Timestamps#

Unix time in milliseconds (UTC), a plain JSON number: 1790000000000. In JavaScript: new Date(ms); Date.now() gives the same unit.

Pagination#

Lists use cursors:

  • Request (HTTP query string, or the same fields in a WebSocket request's data): cursor (optional; the previous page's next_cursor) and limit (optional; default 50, clamped to 1–100).
  • Answer: {"items":[…],"next_cursor":"…"}. next_cursor is absent (or null) on the last page.
  • Cursors are opaque strings of at most 512 bytes: pass them back unchanged (URL-encoded in a query string), never build or parse one.
text
GET /v1/chat/rooms?limit=20
GET /v1/chat/rooms?limit=20&cursor=<next_cursor of the previous page, URL-encoded>

Text rules#

The server refuses text that could impersonate or break layouts. Requests that break a rule get 422 validation_failed with the field named in details.fields.

Field Rule
email a plain local@domain (no display name, no angle brackets, comments, quoted local parts or address literals), at most 254 bytes; unique per server, case-insensitive
password 10 characters to 128 bytes (UTF-8), no control characters, not only white space
display name at most 32 characters, trimmed; no control characters and no invisible or direction-changing characters (bidi controls, zero-width characters, BOM, …)
storage collection / key 1–128 bytes of A-Z a-z 0-9 _ - ., starting with a letter or digit
chat room key 1–64 bytes of a-z 0-9 _ - ., starting with a letter or digit
chat text at most 500 characters (server setting); line breaks (\n) and tabs allowed, \r and other control characters refused; invisible / direction-changing characters refused except the zero-width joiner and non-joiner (emoji need them); must show something visible; at most 8 combining marks in a row
chat nonce 1–64 visible ASCII characters
role [a-z][a-z0-9_.-]*, at most 64 bytes

CORS (browser pages on another origin)#

CORS is off unless the server operator sets cors.allowed_origins (for example ["https://your-site.example"], or ["*"]). With it on, the server allows the methods GET, POST, PUT, PATCH, DELETE, the request headers Authorization, Content-Type, If-Match, If-None-Match, x-net-backend-protocol, x-request-id, and lets the page read the answer headers x-net-backend-protocol, x-request-id, ETag and Retry-After, so a page on another origin uses the API exactly like any other client.

A page served from the same origin as the API (for example through the same reverse proxy) needs no CORS at all. WebSocket connections are not subject to CORS.

Tokens travel only in the Authorization header or the WebSocket auth message, never in cookies, so cross-site request forgery does not apply.

3. Errors#

Every error over HTTP is:

json
{"error":{"code":"validation_failed","message":"the request is invalid","details":{"fields":{"password":["is shorter than 10 characters"]}}}}
  • code: stable, snake_case. Branch on it.
  • message: human-readable English; may change at any time; never contains internal details.
  • details: optional, code-specific JSON.

Over the WebSocket the same object is the error of a refused request or of auth.failed.

Every error code#

Code HTTP Meaning Client action
bad_request 400 malformed request: not JSON, wrong types, missing fields, invalid path parameter fix the request
validation_failed 422 well-formed but breaks a rule; details.fields maps each field to its problems show the field messages
unauthorized 401 no valid credentials: missing, unknown, malformed or revoked token log in again (after one refresh attempt for a revoked access token)
token_expired 401 the access token expired refresh, then retry once
forbidden 403 authenticated but not allowed (missing role, server-locked object, registration closed, a server rule) do not retry
not_found 404 no such route or object (or the caller may not know it exists)
method_not_allowed 405 wrong HTTP method; Allow lists the allowed ones fix the request
unsupported_media_type 415 a JSON route without Content-Type: application/json send the header
conflict 409 the request conflicts with the current state
version_conflict 409 storage: the stored version is not the expected one; details: {"current_version":N} (absent when the object does not exist), plus "index" in a batch reload, merge, write again
payload_too_large 413 body (or WebSocket answer, or stored value) too large send less
rate_limited 429 too many requests; details: {"retry_after_ms":N} wait that long, then retry
quota_exceeded 403 a per-user quota is used up (stored objects / bytes, rooms per WebSocket) free something up
unknown_type – WebSocket only: the request type is not known to this server
unsupported_protocol 400 the client's protocol version is not supported; details: {"supported_min":N,"supported_max":N} update the client
invalid_credentials 401 email + password do not match (never says which part)
refresh_token_reused 401 a refresh token was used a second time after the grace window: the whole session is revoked log in again
email_taken 409 registration: the address already has an account offer login / password reset
email_not_verified 403 the action needs a verified email address verify first
invalid_token 400 a one-time mail token (verification, reset) is unknown, used or expired ask for a new mail
banned 403 the account is banned; details: {"until":<unix ms>} for a timed ban (absent: until lifted) show the ban; do not retry
reauthentication_required 403 the action needs a recent login (linking / unlinking a login provider) log in again, then retry
steam_auth_failed 401 Steam refused the ticket (or could not be asked)
room_full 409 the chat room is at its member cap try later
not_a_member 403 not a member of the chat room (join it first; group / DM rooms: members only)
hook_timeout 503 a server rule did not answer in time retry later
unavailable 503 overloaded, shutting down, or the request took longer than the server's limit retry later with backoff
internal 500 unexpected server error (never with details) retry later; report with x-request-id

Servers and their games may add their own codes: treat an unknown code like its HTTP status.

Notes:

  • An unknown route answers 404 {"error":{"code":"not_found","message":"no such route or object"}}.
  • A request that takes longer than the server's request timeout (default 30 s) answers 503 unavailable.
  • email_taken reveals that an address has an account: a deliberate choice for a clear sign-up answer, bounded by the registration rate limit. Login, password reset and the failed-login limits never reveal it.

4. Rate limits and sizes#

All numbers are the server's defaults; an operator may change them. Every 429 answer carries details.retry_after_ms; answers from the request-level limiters also carry Retry-After (whole seconds).

HTTP rate limits#

What Limit
logins (password and Steam), per client address and route 10 per minute
registrations, per client address 10 per hour
refreshes, per client address 60 per minute
forgot / reset / verify / resend / password change, per client address and route 10 per minute
mails per account (verification, reset) 3 per hour (a further reset request answers the same and sends nothing; a further resend answers 429)
failed logins per email address and client network 5, then one more try every 3 minutes
failed logins per email address, from everywhere 50 per hour; above it only networks that logged in to the account before may try
storage writes (PUT, DELETE, a batch counts once), per user 60 per 60 s (a burst of 60, then one per second)
opening a direct-message room, per user 20 per 600 s (a burst of 20, then one every 30 s)

Client networks are single IPv4 addresses and IPv6 /64 blocks. The failed-login limits count whether or not the address has an account.

WebSocket limits#

What Limit
frames per connection 20 per second, burst 40 (text, binary and ping frames count); over it requests are answered rate_limited; a client that keeps flooding is closed with 1008
chat messages per user a burst of 5, then one every 2 s (rate_limited with retry_after_ms)
message size 1 MiB (1048576 bytes) in both directions; bigger: close 1009
connections per user 5; a 6th closes the oldest with 4009 (the same session's first)
connections per client address 100 (429 at the handshake)
handshakes per client address 60 per minute (429 at the handshake)
rooms per connection 16 (quota_exceeded)
connections per public room 200 (room_full)
time to authenticate 5 s after the upgrade (else close 1008)

Body and value sizes#

What Limit
JSON request bodies 64 KiB (413 payload_too_large above)
storage PUT body 272 KiB (a value of up to 256 KiB plus JSON)
storage batch PUT body 4 MiB + 64 KiB
one stored value 256 KiB of JSON (422 validation_failed above)
stored objects per user 1000 (403 quota_exceeded)
stored bytes per user 4 MiB of values (403 quota_exceeded; a write that does not grow an object always passes)
batch 1–16 distinct objects, at most 4 MiB of values together; a batch read over 4 MiB answers 413
Steam ticket 8192 hex characters
ban reason 255 characters

Generated from API.md (repository commit e25edb9, file sha256 3db87f7bba70).