Erros
Todos os erros da API Ozzie seguem um formato consistente. Quando uma requisição falha, o corpo da resposta sempre contém um objeto error com um code legível por máquina, uma message legível por humano e um array details opcional com contexto adicional.
Formato da resposta de erro
{
"error": {
"code": "ERROR_CODE",
"message": "Uma descrição legível do que deu errado.",
"details": []
}
}
| Campo | Tipo | Descrição |
|---|---|---|
error.code | string | Código de erro legível por máquina — use para tratamento programático |
error.message | string | Descrição legível do erro |
error.details | array | Contexto adicional opcional, como quais campos falharam na validação |
Códigos de erro
INVALID_JSON
| Status HTTP | 400 Bad Request |
|---|---|
| Quando ocorre | O corpo da requisição não pôde ser analisado como JSON |
| Como resolver | Verifique se o header Content-Type é application/json e que o corpo é JSON válido. Problemas comuns: vírgulas no final, chaves sem aspas, aspas simples ao invés de duplas. |
{
"error": {
"code": "INVALID_JSON",
"message": "The request body could not be parsed as JSON.",
"details": []
}
}
VALIDATION_ERROR
| Status HTTP | 422 Unprocessable Entity |
|---|---|
| Quando ocorre | O corpo da requisição é JSON válido mas contém valores de campo inválidos (tipo errado, valor fora do intervalo, valor de enum inválido, etc.) |
| Como resolver | Verifique o array details — cada entrada identifica o campo específico e a falha de validação. |
{
"error": {
"code": "VALIDATION_ERROR",
"message": "One or more fields failed validation.",
"details": [
{
"field": "cadence",
"message": "Must be one of: weekly, biweekly, twice_monthly, monthly"
},
{
"field": "target_amount",
"message": "Must be a positive number"
}
]
}
}
INVALID_CODE
| Status HTTP | 400 Bad Request |
|---|---|
| Quando ocorre | O código OTP enviado para POST /v1/auth/otp/verify não corresponde ao hash armazenado, ou o código já foi utilizado. Os códigos são de uso único — uma verificação bem-sucedida invalida o código imediatamente. |
| Como resolver | Peça ao usuário que verifique o código e tente novamente. Se o código já foi utilizado, solicite um novo via POST /v1/auth/otp/request. |
{
"error": {
"code": "INVALID_CODE",
"message": "The OTP code is incorrect or has already been used.",
"details": []
}
}
CODE_EXPIRED
| Status HTTP | 400 Bad Request |
|---|---|
| Quando ocorre | O código OTP enviado para POST /v1/auth/otp/verify foi emitido há mais de 10 minutos e expirou. |
| Como resolver | Solicite um novo código via POST /v1/auth/otp/request e oriente o usuário a digitá-lo prontamente. |
{
"error": {
"code": "CODE_EXPIRED",
"message": "The OTP code has expired. Please request a new code.",
"details": []
}
}
UNAUTHORIZED
| Status HTTP | 401 Unauthorized |
|---|---|
| Quando ocorre | O header Authorization está ausente, malformado ou contém credenciais inválidas |
| Como resolver | Verifique se seu token está corretamente codificado como base64(client_id:client_secret). Confirme que você não rotacionou seu secret sem atualizar as variáveis de ambiente. Certifique-se de que o formato do header é exatamente Authorization: Bearer <token>. |
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or missing authorization credentials.",
"details": []
}
}
FORBIDDEN
| Status HTTP | 403 Forbidden |
|---|---|
| Quando ocorre | Suas credenciais são válidas, mas você está tentando acessar um recurso que pertence ao usuário de outro cliente |
| Como resolver | Verifique se o user_id no caminho pertence a um usuário que você criou sob seu client_id. Você não pode acessar usuários criados por outros clientes de API. |
{
"error": {
"code": "FORBIDDEN",
"message": "You do not have permission to access this resource.",
"details": []
}
}
NOT_FOUND
| Status HTTP | 404 Not Found |
|---|---|
| Quando ocorre | O recurso solicitado não existe. Causas comuns: um ID de usuário que não existe, uma meta que ainda não foi definida, um ID de movimento financeiro inválido. |
| Como resolver | Verifique se o ID na URL está correto e que o recurso foi criado antes de tentar recuperá-lo. |
{
"error": {
"code": "NOT_FOUND",
"message": "The requested resource could not be found.",
"details": [
{
"resource": "user",
"id": "usr_invalid_id"
}
]
}
}
CONFLICT
| Status HTTP | 409 Conflict |
|---|---|
| Quando ocorre | A requisição conflita com o estado atual de um recurso. Mais comumente: tentar criar um usuário com um external_user_id que já existe sob seu cliente. |
| Como resolver | Se você estiver criando um usuário, use GET /users?external_user_id=... para verificar se já existe antes de criar. Alternativamente, trate isso como uma operação sem efeito (no-op) e use o ID do usuário existente. |
{
"error": {
"code": "CONFLICT",
"message": "A user with this external_user_id already exists.",
"details": [
{
"field": "external_user_id",
"existing_id": "usr_4f8a1b2c3d"
}
]
}
}
INTAKE_REQUIRED
| Status HTTP | 422 Unprocessable Entity |
|---|---|
| Quando ocorre | Você chamou um endpoint que requer um intake financeiro (ex.: POST /plan, POST /chat/messages), mas nenhum intake foi enviado para este usuário. |
| Como resolver | Chame POST /users/{user_id}/financial-intake primeiro. O intake captura a renda, despesas e meta do usuário, que é a base para todos os recursos posteriores. |
{
"error": {
"code": "INTAKE_REQUIRED",
"message": "This user does not have a financial intake. Submit an intake before calling this endpoint.",
"details": []
}
}
PLAN_REQUIRED
| Status HTTP | 422 Unprocessable Entity |
|---|---|
| Quando ocorre | Você chamou um endpoint que requer um plano (ex.: GET /financial-profile), mas nenhum plano foi gerado para este usuário. |
| Como resolver | Chame POST /v1/users/{user_id}/plan/generate primeiro. Observe que a geração de plano em si requer um intake financeiro — veja INTAKE_REQUIRED. |
{
"error": {
"code": "PLAN_REQUIRED",
"message": "This user does not have a financial plan. Generate a plan before calling this endpoint.",
"details": []
}
}
PERSONALITY_REQUIRED
| Status HTTP | 422 Unprocessable Entity |
|---|---|
| Quando ocorre | Um endpoint que requer personality_type foi chamado sem ele, ou com um valor inválido. |
| Como resolver | Pontue a personalidade do usuário usando POST /v1/personality/score, armazene o resultado e passe-o como personality_type no corpo da requisição. Valores válidos: HOTSHOT, STOCKPILER, GIVER, INDEPENDENT. |
{
"error": {
"code": "PERSONALITY_REQUIRED",
"message": "The personality_type field is required for this endpoint.",
"details": []
}
}
RATE_LIMIT_EXCEEDED
| Status HTTP | 429 Too Many Requests |
|---|---|
| Quando ocorre | Seu cliente enviou mais requisições do que o limite de taxa permite na janela atual. |
| Como resolver | Aguarde até o tempo especificado no header X-RateLimit-Reset, depois tente novamente. Implemente backoff exponencial se estiver atingindo os limites regularmente. Entre em contato com commercial@ozzieapp.com se precisar de um limite maior para o seu tier. |
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "You have exceeded the rate limit for this window. Please retry after the reset time.",
"details": [
{
"reset_at": 1746489600,
"limit": 1000,
"window": "1 minute"
}
]
}
}
Headers de resposta quando o limite é atingido:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1746489600
Retry-After: 47
VERSION_MISMATCH
| Status HTTP | 400 Bad Request |
|---|---|
| Quando ocorre | A versão da API na sua URL não corresponde a uma versão suportada, ou um recurso foi criado em uma versão diferente da que você está usando para acessá-lo. |
| Como resolver | Verifique se sua URL base inclui /v1/. Se estiver migrando entre versões, consulte o Changelog para verificar mudanças incompatíveis. |
{
"error": {
"code": "VERSION_MISMATCH",
"message": "The requested API version is not supported.",
"details": [
{
"requested_version": "v0",
"supported_versions": ["v1"]
}
]
}
}
INTERNAL_ERROR
| Status HTTP | 500 Internal Server Error |
|---|---|
| Quando ocorre | Ocorreu um erro inesperado nos servidores do Ozzie. Esse é um problema do Ozzie, não seu. |
| Como resolver | Tente novamente após um breve delay. Esses erros geralmente são transitórios. Se o erro persistir por mais de alguns minutos, verifique a página de status do Ozzie ou entre em contato com o suporte. |
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred. Please try again.",
"details": []
}
}
Recomendações de estratégia de retentativa
Nem todos os erros devem ser retentados. Use este guia para decidir quando tentar novamente e quando expor o erro ao usuário ou registrá-lo para investigação:
| Código de erro | Tentar novamente? | Estratégia |
|---|---|---|
INVALID_JSON | Não | Corrija a requisição — tentar novamente não ajuda |
VALIDATION_ERROR | Não | Corrija os campos inválidos e reenvie |
INVALID_CODE | Não | Peça ao usuário para digitar novamente ou solicite um novo OTP |
CODE_EXPIRED | Não | Solicite um novo código OTP |
UNAUTHORIZED | Não | Corrija as credenciais — verifique as variáveis de ambiente |
FORBIDDEN | Não | Você está acessando o recurso errado |
NOT_FOUND | Não | O recurso não existe — verifique os IDs |
CONFLICT | Não | O recurso já existe — trate de forma idempotente |
INTAKE_REQUIRED | Não | Redirecione o usuário para completar o intake primeiro |
PLAN_REQUIRED | Não | Chame POST /plan primeiro |
PERSONALITY_REQUIRED | Não | Corrija o corpo da requisição |
RATE_LIMIT_EXCEEDED | Sim | Aguarde X-RateLimit-Reset, depois tente novamente |
VERSION_MISMATCH | Não | Corrija a URL |
INTERNAL_ERROR | Sim | Tente novamente com backoff exponencial |
Exemplo de backoff exponencial
async function retryableRequest(method, path, body, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await ozzieRequest(method, path, body);
} catch (err) {
const isRetryable =
err.message.includes('RATE_LIMIT_EXCEEDED') ||
err.message.includes('INTERNAL_ERROR');
if (!isRetryable || attempt === maxRetries) throw err;
const delay = Math.min(1000 * Math.pow(2, attempt), 30000); // Max 30s
console.warn(`Retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
await new Promise(r => setTimeout(r, delay));
}
}
}