Skip to main content
Every failure returns the same JSON envelope, whatever produced it. A single exception filter builds it, so a Prisma error, a guard rejection and a validation failure all look alike to a client.

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 answer 429 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.
A self hosted instance that sets 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 400 with the message property <name> should not exist, so a typo in a field name is refused rather than silently ignored.
  • A failing field produces a 400 whose message names the field and the rule it broke.
Because unknown properties are rejected rather than stripped, a misspelled field fails the request instead of letting it succeed while changing nothing.

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 404 with the message Resource not found, because no id can contain one.
  • A NUL in a query value or a body field answers 400, and the message names the value, such as note must not contain a NUL character or keywords[1] must not contain a NUL character. When a property name itself holds the character, the message names body or query instead, so the character never travels back in a response.
A JSON body can also carry half of an emoji, written as a lone escape such as \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, answers 400 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

The path 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.

API conventions

What the successful responses look like.

Troubleshooting

The same codes seen from the operator side.
Last modified on October 20, 2026