Errors
Status codes and JSON shapes from the REST API, the MCP server, the content host and the OAuth endpoints.
Fermium has a small number of error shapes. Which one you get depends on which part of the server produced it.
REST API
Service errors (validation, not found, quota) come as:
{ "error": "document not found" }
Framework errors (authentication, request validation, rate limits raised by routes) come as:
{ "detail": "authentication required" }
Request-body validation failures return 422 with detail as a list of field errors.
| Status | Meaning | Examples |
|---|---|---|
| 400 | invalid request | visibility must be one of ('unlisted', 'password', 'workspace'), password visibility requires a password, retention_days must be >= 0 (0 = keep forever), invalid tag 'X': ..., bad path '...', file type .exe not allowed; allowed: ..., doc is a snippet; use update_snippet, use a subdomain (docs.yourcompany.com), not your apex domain, gmail.com is a public email provider and cannot be a viewer domain, alias must be a lowercase slug: ..., old_string matches 3 times in 'source.md'; ..., hunk 2 in 'source.md' failed at line 41: expected '...', found '...', workspace policy does not allow visibility 'unlisted'; allowed: workspace, workspace policy caps retention at 365 days; 'keep forever' (0) is not allowed, this content looks like it contains credentials, and your workspace policy blocks publishing secrets: ..., webhook url must be publicly reachable; ..., type your email address to confirm account deletion |
| 401 | no valid session or token | authentication required |
| 402 | plan does not allow it | visibility 'workspace' (sign-in gate) needs a Team plan or above; upgrade at https://fermium.app/pricing ..., custom domains need a Team plan or above ..., your plan allows 1 custom domain(s); remove one or upgrade, workspace policy requires the Business plan or above, the audit log requires the Business plan or above, webhooks requires the Business plan or above, legal hold requires the Enterprise plan, the IP allowlist requires the Enterprise plan, the team plan allows 25 members and 24 members plus 1 pending invites already fill it; ..., SCIM provisioning is an Enterprise feature; ... |
| 403 | forbidden | cross-origin request rejected, invalid or expired upload ticket, admin only, only the document owner can change its visibility (an editor trying an owner-only change), only the document owner or a workspace admin can share it, workspace owner or admin role required, this action requires the owner role, groups and group grants need a Team plan or above; ..., comments need a paid plan (Team or above); ..., you can read this document but not comment on it, your workspace only allows access from approved networks; <ip> is not on the allowlist, this invite was sent to bo@acme.com; sign in with that account to accept it |
| 404 | not found or not yours | document not found, token not found, domain not found, no billing account yet; start a plan first, grant not found, group not found, 'bo@acme.com' is not a member of this workspace, no file at 'notes.md', doc has no version 9 |
| 409 | conflict | that hostname is already registered, acme.com is already claimed by another workspace, alias 'q3-roadmap' already points at '...' (01J7…); pass force=true to move it, bo@acme.com is already a member of this workspace, bo@acme.com already has a pending invite, a group with slug 'design' already exists, you own a workspace with other members; transfer ownership before deleting your account, acme.com is already used for single sign-on by another workspace |
| 410 | gone | this invite has expired or was already used |
| 413 | too large or over quota | request body too large, storage quota exceeded: ..., doc limit reached (5000 docs per user), file 'x' is N bytes; limit is 20971520, body exceeds the 2147483648-byte upload limit |
| 423 | locked | this document is under legal hold and cannot be deleted; an admin must release the hold first |
| 429 | rate limited | publish rate limit exceeded, too many requests, too many attempts |
| 503 | dependency not configured or unavailable | billing is not configured, readiness check degraded |
Documents that exist but belong to another account return 404, not 403, so the API never confirms that a document exists. Someone who holds an access grant on a document and tries an owner-only change gets the 403 instead, since they already know the document exists.
Endpoints under /scim/v2 return the SCIM error schema (application/scim+json with schemas, status, scimType, detail) instead; see SCIM.
MCP tools
A failed tool call returns an MCP tool error whose message is the same text the REST API would put in error. There is no separate status code; the agent reads the message and acts on it. The messages are written for that: they name the limit and, where an upgrade would help, include the pricing URL.
publish rate limit exceeded (120/hour); slow down and retry later
storage quota exceeded: 1 GiB on the free plan; free space by deleting docs or upgrade at https://fermium.app/pricing
upload visibility must be 'unlisted' or 'workspace'; for a password doc, upload unlisted then call set_visibility
provide exactly one of 'content' or 'content_base64'
content_base64 is not valid base64
decoded content would exceed the 20971520-byte file cap
authentication lost; retry the request
The last one means the token was valid when the request started but the account was disabled or the token revoked in between; a retry gets a clean 401.
Unauthenticated MCP requests
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://fermium.app/.well-known/oauth-protected-resource/mcp", error="invalid_token"
Content-Type: application/json
{"error":"unauthorized","detail":"authenticate via the MCP OAuth flow (your client should open a browser), or supply 'Authorization: Bearer dsk_...'"}
error="invalid_token" is present only when a token was sent. Clients use the resource_metadata URL to start OAuth; see Authentication.
OAuth endpoints
RFC 6749 shape, with CORS headers and Cache-Control: no-store:
{ "error": "invalid_grant", "error_description": "authorization code is invalid or expired" }
| Status | error |
|---|---|
| 400 | invalid_client_metadata, invalid_redirect_uri, invalid_request, invalid_grant, unsupported_grant_type |
| 401 | invalid_client |
| 429 | temporarily_unavailable |
At /oauth/authorize, an unknown client_id or unregistered redirect_uri is a plain 400 with detail; the server never redirects to an unverified URL. Other authorize errors (unsupported_response_type, invalid_request, access_denied) are delivered as query parameters on the registered redirect_uri.
The content host
Readers see as little as possible.
| Status | When | Body |
|---|---|---|
| 404 | missing, expired or deleted document; a path not in the document; another workspace's document on a custom domain | a neutral HTML page, "This link isn't available", for browsers; {"error":"not found"} otherwise |
| 401 | password document without or with wrong credentials | empty, with WWW-Authenticate: Basic realm="fermium-<slug>" |
| 302 | workspace document without a viewer cookie | redirect to https://fermium.app/viewer/authorize |
| 403 | signed-in reader outside the publisher's workspace | {"detail":"this document is limited to members of the publisher's workspace"} from the app host |
| 403 | workspace document read from outside an Enterprise IP allowlist | {"error":"this document is restricted to approved networks"} |
| 302 | /x/<ref> that resolves |
redirect to /d/<slug>/<path> on the same host; an unresolvable reference is the neutral 404 page |
| 416 | unsatisfiable Range |
empty, with Content-Range: bytes */<size> |
| 429 | too many 404s or too many password failures from one IP | {"error":"too many requests"} or {"error":"too many attempts"} |
| 304 | If-None-Match matches an unlisted file's ETag |
empty |
Anything other than /d/*, /x/*, health probes and two shared assets on the content host is a 404 with {"error":"not found"}.
The upload endpoints
PUT /u/<ticket> and PUT /api/upload use detail:
| Status | Detail |
|---|---|
| 400 | request body is empty, bad content-length, client disconnected, query parameter 'filename' required |
| 401 | authentication required (/api/upload only) |
| 403 | invalid or expired upload ticket, uploader account unavailable |
| 413 | body is N bytes; upload limit is 2147483648, or the storage quota message |
| 429 | publish rate limit exceeded, too many requests |
If a quota check fails after the bytes have landed, the orphaned object is deleted before the error is returned.