Integration
Error handling
One shape of refusal, and what to do with each family of it.
The shape of a refusal
Every refusal, from any route and under any code, returns the same object:
{
"error": "Card não encontrado.",
"details": null
}- `error` is a sentence in Portuguese, written for whoever is on screen. Its wording can change without notice: do not build logic on the text.
- `details` is only filled on schema refusals (422), and carries zod's
fieldErrorsandformErrors. - The HTTP code is the contract. That is what your integration should branch on.
The schema refusal, from the inside
{
"error": "Dados inválidos.",
"details": {
"formErrors": ["Nada para atualizar."],
"fieldErrors": {
"name": ["Campo obrigatório."],
"confirm": ["Envie { \"confirm\": \"APAGAR TENANT\" } para confirmar."]
}
}
}fieldErrors is per body field; formErrors holds what belongs to no field, such as “nothing to update”, which is about the whole object.
The families, and what to do with each
422 has two origins, and details tells them apart
With details, it was the schema: the body is malformed. Without details, it was a business rule checked inside the transaction: an epic from another project, a locked epic, a column outside the set.
What is safe to retry
- 429 is the only refusal that asks for a retry, and
Retry-Aftersays when. - Every write is transactional: a refused request left no half effect behind. Retrying after fixing the body is safe.
- Creation is not idempotent. Repeating a
POST /api/cardsthat already worked creates a second card, with a new number.