Onvox AI Docs

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ódigoSignificadoCausa mais comum
200OKSucesso
400Bad RequestParâmetros inválidos ou ausentes
401UnauthorizedAPI Key inválida, expirada ou ausente
403ForbiddenSem permissão para o recurso
404Not FoundRecurso não existe
415Unsupported Media TypePOST sem o header Content-Type: application/json
422UnprocessableÁudio inválido ou transcrição falhou
429Too Many RequestsRate limit atingido
500Internal Server ErrorErro 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-Key ausente 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 403 em qualquer endpoint, com A empresa "<nome>" está desativada desde <data>. As chaves de API não respondem enquanto isso.
  • Invalid audioKey: em POST /api/analyses, o audioKey é uma chave de storage que não começa com uploads/<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 audioKey ou transcript informado em POST /api/analyses
  • Tipo de campo errado (ex.: salespersonId como 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:

  • transcript muito 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

RecursoLimite
POST /api/analyses15 requisições/min por empresa
GET /api/analyses120 requisições/min por empresa
GET /api/analyses/{id} e GET /api/salespersonsSem limite

Limites de arquivo e áudio

RecursoLimite
Tamanho máximo (Data URL base64)10 MB
Tamanho máximo (download por URL)40 MB
Duração máxima do áudioSem 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.

Nesta página