Erros e Limites
Códigos de erro, rate limit, limites de uso e estratégias de retry para a API da Onvox AI.
Erros e Limites
Códigos HTTP
| Código | Significado | Causa mais comum |
|---|---|---|
200 | OK | Sucesso |
400 | Bad Request | Parâmetros inválidos ou ausentes |
401 | Unauthorized | API Key inválida, expirada ou ausente |
403 | Forbidden | Sem permissão para o recurso |
404 | Not Found | Recurso não existe |
415 | Unsupported Media Type | POST sem o header Content-Type: application/json |
422 | Unprocessable | Áudio inválido ou transcrição falhou |
429 | Too Many Requests | Rate limit atingido |
500 | Internal Server Error | Erro interno, tente novamente |
Formato das respostas de erro
Todos os erros seguem o mesmo formato, sem wrapper externo: os campos ficam direto na raiz do corpo:
{
"message": "Descrição do erro",
"code": "CODIGO_TRPC",
"data": {
"code": "CODIGO_TRPC",
"httpStatus": 400,
"path": "analysis.submitAnalysis"
}
}data.path é o nome interno da operação que respondeu (no exemplo, a de POST /api/analyses).
Erros de validação (400) ganham um campo issues a mais, também na raiz (não dentro de data):
{
"message": "Input validation failed",
"code": "BAD_REQUEST",
"data": { "code": "BAD_REQUEST", "httpStatus": 400, "path": "analysis.submitAnalysis" },
"issues": [
{
"expected": "string",
"code": "invalid_type",
"path": ["salespersonId"],
"message": "Invalid input: expected string, received number"
}
]
}Erros comuns
401 Unauthorized
{
"message": "Invalid or expired API Key",
"code": "UNAUTHORIZED",
"data": { "code": "UNAUTHORIZED", "httpStatus": 401, "path": "analysis.getSalespersons" }
}Sem o header X-API-Key, a mensagem é API Key required. Use header X-API-Key.
Causas:
X-API-Keyausente no header- API Key inválida ou mal formatada
- API Key revogada pelo administrador
- API Key expirada (se foi criada com data de expiração)
Solução: Confirme a chave chamando GET /api/salespersons (veja Autenticação). Se continuar retornando 401, gere uma nova chave no dashboard. Se esse teste devolver 403, a chave é válida, mas não tem o escopo api:salespersons:read.
403 Forbidden
{
"message": "Chave de API sem o escopo necessário para este endpoint. Escopo exigido: api:analyses:write.",
"code": "FORBIDDEN",
"data": { "code": "FORBIDDEN", "httpStatus": 403, "path": "analysis.submitAnalysis" }
}Causas:
- Créditos insuficientes para processar a análise (ver "Créditos insuficientes" abaixo)
- API Key sem o escopo exigido pelo endpoint (ver Autenticação). A mensagem traz o escopo que falta, por exemplo:
Chave de API sem o escopo necessário para este endpoint. Escopo exigido: api:analyses:write. - API Key com permissões inválidas (valor que a API não reconhece):
Chave de API com permissões inválidas. Gere uma chave nova no painel (escopo exigido por este endpoint: <escopo>). - Empresa desativada: toda chave da empresa responde
403em qualquer endpoint, comA empresa "<nome>" está desativada desde <data>. As chaves de API não respondem enquanto isso. Invalid audioKey: emPOST /api/analyses, oaudioKeyé uma chave de storage que não começa comuploads/<id-da-empresa>/(ver Endpoints)
Acessar GET /api/analyses/{id} de uma análise que não pertence à sua empresa devolve 404 Not Found, não 403: o isolamento multi-tenant é implementado como "não existe", não como "sem permissão" (ver Endpoints).
400 Bad Request
Sem transcript e sem audioKey, o erro é simples, com a frase em message (sem issues):
{
"message": "Either 'transcript' or 'audioKey' is required",
"code": "BAD_REQUEST",
"data": { "code": "BAD_REQUEST", "httpStatus": 400, "path": "analysis.submitAnalysis" }
}Campo com tipo errado é erro de validação, com o detalhe em issues:
{
"message": "Input validation failed",
"code": "BAD_REQUEST",
"data": { "code": "BAD_REQUEST", "httpStatus": 400, "path": "analysis.submitAnalysis" },
"issues": [
{
"expected": "string",
"code": "invalid_type",
"path": ["salespersonId"],
"message": "Invalid input: expected string, received number"
}
]
}Causas frequentes:
- Nenhum dos campos
audioKeyoutranscriptinformado emPOST /api/analyses - Tipo de campo errado (ex.:
salespersonIdcomo número em vez de string)
422 Unprocessable
{
"message": "Não foi possível processar esta transcrição, tente novamente ou revise o conteúdo enviado (transcrições muito curtas ou ambíguas podem falhar).",
"code": "UNPROCESSABLE_CONTENT",
"data": { "code": "UNPROCESSABLE_CONTENT", "httpStatus": 422, "path": "analysis.submitAnalysis" }
}Causa:
transcriptmuito curto ou ambíguo: a IA não conseguiu produzir uma análise completa (score/diagnóstico). Isso é raro e não determinístico: a mesma transcrição pode passar numa tentativa e falhar em outra.
As causas relacionadas a áudio (arquivo corrompido, sem voz detectável, URL inacessível) documentadas em versões anteriores desta página nunca tiveram implementação real correspondente no código; foram removidas depois de uma auditoria completa (grep por UNPROCESSABLE_CONTENT em todo packages/api/src) que não encontrou nenhum caminho que as lançasse. O único gatilho real hoje deste erro é o listado acima.
Solução: tente novamente. Se persistir, envie mais contexto na transcrição (a conversa precisa ter conteúdo suficiente pra IA avaliar rapport, escuta, objeções e clareza).
Formatos de áudio aceitos: mp3, mp4, wav, ogg, opus
429 Rate Limit
{
"message": "Rate limit exceeded. Try again in 60s.",
"code": "TOO_MANY_REQUESTS",
"data": { "code": "TOO_MANY_REQUESTS", "httpStatus": 429, "path": "analysis.submitAnalysis" }
}Limite: POST /api/analyses aceita 15 requisições por minuto por empresa; GET /api/analyses, 120 requisições por minuto por empresa. GET /api/analyses/{id} e GET /api/salespersons não têm limite.
Quando atingido, espere os 60 segundos indicados na mensagem antes de tentar de novo. Requisições recusadas também contam na janela de 1 minuto, então insistir antes disso só prolonga o bloqueio. O tempo de espera vem só no texto de message: o corpo não traz um campo próprio para ele. Veja a estratégia de retry abaixo.
Créditos insuficientes
{
"message": "Insufficient minutes. Current balance: 0 min. Purchase more minutes to continue creating analyses.",
"code": "FORBIDDEN",
"data": { "code": "FORBIDDEN", "httpStatus": 403, "path": "analysis.submitAnalysis" }
}Causa: Saldo de créditos zerado ou abaixo do mínimo para processar o áudio. Com saldo abaixo de 1 minuto, a mensagem é a do exemplo; quando há saldo mas ele não cobre a duração do áudio, é Insufficient minutes. Analysis requires <X> min, but balance is <Y> min.
Solução: O saldo de créditos hoje só é consultável pelo painel web (sessão de usuário). Peça a um admin_company da sua empresa pra conferir em Planos & Creditos (menu lateral, grupo Sistema) e adquirir mais antes de continuar.
Créditos não são debitados em caso de erro. Se uma análise falhar (422, 500), nenhum crédito é consumido.
Limites da API
Rate limit
| Recurso | Limite |
|---|---|
POST /api/analyses | 15 requisições/min por empresa |
GET /api/analyses | 120 requisições/min por empresa |
GET /api/analyses/{id} e GET /api/salespersons | Sem limite |
Limites de arquivo e áudio
| Recurso | Limite |
|---|---|
| Tamanho máximo (Data URL base64) | 10 MB |
| Tamanho máximo (download por URL) | 40 MB |
| Duração máxima do áudio | Sem limite fixo: o custo em créditos escala com a duração |
Estratégia de retry com backoff exponencial
Para erros 429 (rate limit), espere os 60 segundos da janela antes de tentar de novo; para 500 (erro interno), use backoff exponencial. Esgotadas as tentativas, a função lança o último erro:
async function fetchComRetry(url, options, maxTentativas = 3) {
for (let tentativa = 0; tentativa < maxTentativas; tentativa++) {
const res = await fetch(url, options);
// Sucesso
if (res.ok) return res.json();
const erro = await res.json();
const ultimaTentativa = tentativa === maxTentativas - 1;
// Rate limit: espera a janela inteira (60 s). Esperar menos não adianta:
// a requisição recusada também conta na janela e prolonga o bloqueio.
if (res.status === 429 && !ultimaTentativa) {
console.warn('Rate limit atingido. Aguardando 60s...');
await new Promise(r => setTimeout(r, 60_000));
continue;
}
// Erro interno: tenta novamente com backoff
if (res.status === 500 && !ultimaTentativa) {
const espera = Math.pow(2, tentativa) * 2000; // 2s, 4s, ...
await new Promise(r => setTimeout(r, espera));
continue;
}
// Outros erros, ou 429/500 na última tentativa: lança
throw Object.assign(new Error(erro.message || 'Erro na requisição'), {
status: res.status,
code: erro.code
});
}
throw new Error('maxTentativas precisa ser pelo menos 1');
}
// Uso
const dados = await fetchComRetry(
'https://api.evolu-ai.com/api/analyses',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.EVOLUA_API_KEY
},
body: JSON.stringify({ salespersonId: 'user_abc', audioKey: '...' })
}
);Erros de validação Zod
Para requisições POST /api/analyses, os erros de validação incluem detalhes por campo no array issues (na raiz do corpo, não dentro de data):
{
"message": "Input validation failed",
"code": "BAD_REQUEST",
"data": { "code": "BAD_REQUEST", "httpStatus": 400, "path": "analysis.submitAnalysis" },
"issues": [
{
"expected": "string",
"code": "invalid_type",
"path": ["clientName"],
"message": "Invalid input: expected string, received number"
},
{
"origin": "string",
"code": "too_small",
"minimum": 1,
"inclusive": true,
"path": ["interactionType"],
"message": "Too small: expected string to have >=1 characters"
}
]
}Use o array issues para identificar exatamente qual campo está inválido e exibir mensagens de erro precisas ao usuário final.