The envelope
The shape is
ApiErrorEnvelope in @asobeast/shared, so a TypeScript client can import it rather than redeclare it.
Status codes
Three of these are worth reading twice.
A
429 and a 403 with a quota object are different problems. A 429 clears by waiting; a capacity limit does not. See Rate limits and quotas.
A 504 is produced by the Next.js proxy rather than by the API, so it appears when the API is slow rather than when a request is invalid. The same proxy returns 502 when it cannot reach the API at all. See Web app configuration.
A 404 on /docs or /admin/queues is a deliberate answer rather than a missing route. See The queue dashboard and the OpenAPI surface.
A spent AI allowance
When a workspace has used every AI call its plan includes this month, the four AI endpoints answer429 with an aiAllowance object, and Retry-After counts the seconds until 00:00 UTC on the 1st. What counts as a call is described in AI features.
AI_CALLS_PER_MONTH=0 has turned AI calls off, so the same 429 carries the message This workspace includes no AI calls. and an aiAllowance whose limit is 0. Nothing renews in that case, so waiting does not help until the operator raises the limit. Retry-After is still sent, pointing at the next period, so a client that reads it keeps working.
Validation errors
Every controller input is a DTO validated by a global pipe that refuses any property the DTO does not declare. Two consequences follow.- An unknown property answers
400with the messageproperty <name> should not exist, so a typo in a field name is refused rather than silently ignored. - A failing field produces a
400whosemessagenames the field and the rule it broke.
Text the database cannot store
PostgreSQL text cannot hold the NUL character (U+0000), so the API refuses it before any query runs, in the path, the query string and the body alike.
- A NUL in a path id answers
404with the messageResource not found, because no id can contain one. - A NUL in a query value or a body field answers
400, and themessagenames the value, such asnote must not contain a NUL characterorkeywords[1] must not contain a NUL character. When a property name itself holds the character, themessagenamesbodyorqueryinstead, so the character never travels back in a response.
\ud83d with no partner. That is not valid Unicode text, so it answers 400 with must be well formed Unicode text.
Every other character is accepted, including emoji and the other control characters. Passwords and the one time tokens in confirmation, reset and invitation links are hashed rather than stored, so they are never inspected and may hold any character. A refused request stores nothing.
Nesting depth
No request this API accepts nests more than a few levels, so a body or query string nested more than 64 levels deep, counting every object and array, answers400 with the message body must not be nested more than 64 levels deep (or query). The limit applies to every field, passwords included, and is checked before validation, so a deeply nested body is refused cheaply instead of exhausting the validator.
What an error never repeats
Thepath and the message of an unknown route name the path only. The query string is left out on purpose: a client that puts a token in a URL by mistake would otherwise get it back in an error body that its logs, its error reporter or a proxy may keep. The API never reads a credential from a query string. Send a personal token in the Authorization: Bearer header, as described in Authentication. Validation messages name the field that failed and the rule it broke, not the value you sent.
Related
API conventions
What the successful responses look like.
Troubleshooting
The same codes seen from the operator side.