Endpoints
Referência completa dos endpoints REST públicos disponíveis para integrações externas via API Key.
Endpoints da API
Estes são os endpoints REST públicos disponíveis para integrações externas via autenticação por API Key.
Todas as requisições devem incluir o header X-API-Key com uma API Key válida.
GET /api/salespersons
Lista os vendedores e supervisores da empresa autenticada.
Fluxo de Uso
- Chame
GET /api/salespersonspara obter a lista de vendedores da sua empresa. - Localize o vendedor desejado e copie o
iddele. - Use esse
idno camposalespersonIdao fazer uma requisiçãoPOST /api/analyses. Isso vincula a análise a esse vendedor.
Headers
| Header | Obrigatório | Descrição |
|---|---|---|
X-API-Key | Sim | Sua API Key |
Resposta
[
{
"id": "user_xxx",
"name": "João Silva",
"role": "salesperson",
"supervisorName": "Maria Souza"
},
{
"id": "user_yyy",
"name": "Maria Souza",
"role": "supervisor",
"supervisorName": null
}
]Campos da Resposta
| Campo | Tipo | Descrição |
|---|---|---|
id | string | ID único do usuário (use em POST /api/analyses) |
name | string | Nome completo |
role | string | "salesperson" ou "supervisor" |
supervisorName | string | null | Nome do supervisor atribuído (null quando não tem supervisor) |
Respostas de Erro
| Status | Código | Descrição |
|---|---|---|
| 401 | UNAUTHORIZED | API Key ausente, inválida ou expirada |
| 403 | FORBIDDEN | API Key sem o escopo api:salespersons:read |
POST /api/analyses
Cria uma nova análise de vendas com IA. Requer transcript ou audioKey.
Headers
| Header | Obrigatório | Descrição |
|---|---|---|
X-API-Key | Sim | Sua API Key |
Content-Type | Sim | application/json |
Corpo da Requisição
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
transcript | string | Um de transcript ou audioKey | Transcrição completa da ligação de vendas |
audioKey | string | Um de transcript ou audioKey | Fonte de áudio (veja Opções de Entrada de Áudio abaixo) |
salespersonId | string | Não | ID do vendedor (obtido via GET /api/salespersons) |
salespersonName | string | Não | Nome do vendedor |
clientName | string | Não | Nome do cliente |
interactionType | string | Não | Tipo de interação: key do catálogo ou tipo customizado ativo da empresa. Omitido, a IA detecta o tipo. Um tipo que não existe (ou customizado desativado) devolve 400 |
Opções de Entrada de Áudio
Você pode fornecer áudio via o campo audioKey de três formas:
- URL direta: URL HTTP/HTTPS de um arquivo de áudio (ex:
"https://exemplo.com/audio.mp3"), com limite de até 40MB. O caminho da URL precisa terminar em.mp3,.mp4,.wav,.oggou.opus(a query string não conta); sem essa extensão, o valor não é baixado como URL e passa a ser tratado como chave de storage. Se o servidor que hospeda o áudio enviarContent-Type, ele precisa ser de áudio (audio/mpeg,audio/mp3,audio/mp4,audio/wav,audio/wave,audio/ogg,audio/opus); outro valor devolve400 - Base64: Data URL com áudio codificado em base64 (ex:
"data:audio/mp3;base64,SGVsbG8g..."), com limite de até 10MB - Chave de storage: chave de objeto já existente no storage da Onvox AI (ex:
"uploads/<id-da-empresa>/audio-xyz.mp3"). A chave precisa começar comuploads/<id-da-empresa>/, onde<id-da-empresa>é ocompanyIdda empresa da API Key; qualquer outra chave devolve403comInvalid audioKey
Formatos suportados: mp3, mp4, wav, ogg, opus
Exemplo de Requisição (com transcript)
{
"salespersonId": "user_1234567890",
"clientName": "Cliente Teste",
"transcript": "Olá, meu nome é João da Onvox AI..."
}Exemplo de Requisição (com URL de áudio)
{
"salespersonId": "user_1234567890",
"clientName": "Cliente Teste",
"audioKey": "https://exemplo.com/audio.mp3"
}Resposta
{
"id": "anal_xxx",
"score": 82,
"summary": "O vendedor demonstrou forte rapport...",
"clientName": "Cliente Teste",
"salespersonName": "João Silva",
"audioDurationSeconds": 420,
"dimensionScores": {
"rapport": 90,
"listening": 85,
"objections": 80,
"clarity": 88
},
"interactionType": "outbound-prospecting",
"interactionScore": 82,
"interactionScorecard": {
"typeKey": "outbound-prospecting",
"typeLabel": "Prospecção Outbound",
"macroFront": "comercial",
"score": 82,
"confidence": 0.9,
"criteria": [
{
"key": "string",
"label": "string",
"polarity": "objective",
"met": true,
"evidence": "string",
"timestamp": "01:23"
}
]
},
"universalIndicators": {
"methodAdherence": {
"score": 75,
"steps": [
{ "key": "string", "label": "string", "present": true, "timestamp": "00:12" }
]
},
"talkToListen": { "attendantPct": 40, "clientPct": 60 },
"interruptions": { "count": 2, "perTenMinutes": 1.5 },
"emotionalClimate": {
"start": "neutral",
"middle": "positive",
"end": "positive",
"overall": "positive"
}
},
"objections": [{"objection": "Preocupação com preço", "resolved": true}],
"productExploration": {
"explored": true,
"products": ["Produto A"]
},
"keywordDetection": {
"client_terms": [
{ "term": "preço", "polarity": "negative", "category": "objection", "context": null }
],
"attendant_terms": []
},
"profile": "Consultivo",
"stage": "Qualify",
"bant": {
"budget": "string",
"authority": "string",
"need": "string",
"timing": "string"
},
"publicLink": "https://app.evoluai.com.br/analysis/anal_xxx",
"createdAt": "2024-01-15T12:00:00.000Z"
}Nota: os campos
strengths,improvementsephaseSuggestionsforam descontinuados e não são mais retornados.
Nota: o
scoredesta resposta já é a nota oficial da análise: é o mesmo valor queGET /api/analyses/{id}devolve emdisplayScore. Não confunda com oscoredoGET, que é a nota bruta de qualidade geral de conversa.
Respostas de Erro
| Status | Código | Descrição |
|---|---|---|
| 400 | BAD_REQUEST | transcript e audioKey ausentes, campo com tipo errado, interactionType inválido, ou áudio recusado (o servidor da URL respondeu com erro, Content-Type que não é de áudio, arquivo acima do limite) |
| 401 | UNAUTHORIZED | API Key inválida ou ausente |
| 403 | FORBIDDEN | Minutos/créditos insuficientes |
| 403 | FORBIDDEN | API Key sem o escopo api:analyses:write |
| 403 | FORBIDDEN | Invalid audioKey: chave de storage fora de uploads/<id-da-empresa>/ |
| 404 | NOT_FOUND | Vendedor não encontrado |
| 415 | UNSUPPORTED_MEDIA_TYPE | Header Content-Type: application/json ausente ou com outro tipo |
| 422 | UNPROCESSABLE_CONTENT | A IA não conseguiu produzir a análise (transcrição muito curta ou ambígua); ver Erros |
| 429 | TOO_MANY_REQUESTS | Rate limit atingido |
GET /api/analyses
Lista as análises da empresa autenticada, da mais recente para a mais antiga, com filtros e paginação por cursor. É o endpoint para varrer o histórico e correlacionar com o seu sistema (CDR do PABX, CRM): GET /api/analyses/{id} só serve quando você já sabe o ID.
A listagem devolve um índice, não o dossiê: sem transcrição, sem áudio, sem scorecard, sem custo. Depois de localizar a análise aqui, busque o dossiê completo em GET /api/analyses/{id}.
Headers
| Header | Obrigatório | Descrição |
|---|---|---|
X-API-Key | Sim | Sua API Key |
Parâmetros de Query
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
from | string | — | Data/hora inicial inclusive, sobre createdAt, em ISO-8601 (2026-09-01 ou 2026-09-01T00:00:00Z) |
to | string | — | Data/hora final inclusive, sobre createdAt, em ISO-8601. Só a data (2026-09-30) corta à meia-noite UTC do começo desse dia; para incluir o dia inteiro, mande data e hora (2026-09-30T23:59:59Z) |
salespersonId | string | — | ID do vendedor (obtido via GET /api/salespersons) |
externalCallId | string | — | ID da chamada no sistema externo (CDR do PABX, conferência do Meet). Correlação 1:1 |
status | string | — | pending, processing, completed ou failed |
interactionType | string | — | Key do catálogo, tipo customizado da empresa, ou __none__ para as análises sem tipo classificado |
limit | integer | 50 | Itens por página, de 1 a 100 |
cursor | string | — | O nextCursor devolvido pela página anterior |
Vários filtros na mesma requisição se somam (E lógico): ?status=completed&salespersonId=user_xxx traz só as análises concluídas daquele vendedor.
Paginação
A paginação é por cursor (keyset), não por offset:
- Chame
GET /api/analyses?limit=100. - Se
hasMorefortrue, repita a chamada passandocursorcom o valor exato denextCursor. - Pare quando
nextCursorviernull.
curl -s 'https://api.evolu-ai.com/api/analyses?limit=100&from=2026-09-01' \
-H 'X-API-Key: evai_sua_chave'O cursor é opaco: não tente ler, montar ou alterar o conteúdo dele. Ele posiciona a varredura por createdAt + id, então análises criadas durante a varredura aparecem só na página 1, sem repetir nem pular item das páginas seguintes. Um cursor mal formado responde 400, e nenhum cursor alcança dados de outra empresa: o escopo da empresa vem da API Key, não do cursor.
Resposta
{
"items": [
{
"id": "anal_xxx",
"createdAt": "2026-09-05T10:00:00.000Z",
"updatedAt": "2026-09-05T10:04:12.000Z",
"status": "completed",
"source": "yeastar",
"provider": "yeastar",
"externalCallId": "yeastar:1694000000.123",
"externalOwnerId": "1001",
"companyId": "company_xxx",
"salespersonId": "user_xxx",
"salespersonName": "João Silva",
"supervisorName": "Maria Souza",
"clientName": "Cliente Teste",
"displayScore": 82,
"score": 70,
"interactionScore": 82,
"interactionType": "outbound-prospecting",
"interactionTypeSource": "auto",
"interactionTypeLabel": "Prospecção Outbound",
"summary": "O vendedor demonstrou forte rapport...",
"audioDurationSeconds": 420,
"attributionStatus": "auto"
}
],
"nextCursor": "MjAyNi0wOS0wNSAxMDowMDowMC4xMjM0NTZ8YW5hbF94eHg",
"hasMore": true
}Campos da Resposta
| Campo | Tipo | Descrição |
|---|---|---|
items | array | Análises da página, da mais recente para a mais antiga |
nextCursor | string | null | Valor a passar em cursor na próxima chamada. null = acabou |
hasMore | boolean | true quando ainda existe página seguinte |
Campos de cada item
| Campo | Tipo | Descrição |
|---|---|---|
id | string | ID da análise. Use em GET /api/analyses/{id} para o dossiê completo |
createdAt | string | Data/hora de criação (ISO-8601). É o campo que from/to filtram e que define a ordem |
updatedAt | string | Data/hora da última atualização (ISO-8601) |
status | string | pending, processing, completed ou failed |
source | string | Produto de origem: upload, yeastar, google_meet |
provider | string | null | Provedor externo quando a análise veio de integração |
externalCallId | string | null | ID da chamada no sistema externo. A chave de correlação com o seu CDR |
externalOwnerId | string | null | Dono da conversa no sistema externo (ramal do PABX, e-mail do organizador) |
companyId | string | null | Empresa dona da análise. Sempre a empresa da sua API Key |
salespersonId | string | null | ID do vendedor atribuído |
salespersonName | string | null | Nome do vendedor registrado na análise |
supervisorName | string | null | Nome do supervisor do vendedor atribuído |
clientName | string | null | Nome do cliente |
displayScore | number | null | Nota oficial: interactionScore ?? score ?? null, com uma exceção: havendo scorecard do tipo de interação sem nota, vem null. null = sem nota (nunca 0 inventado) |
score | number | null | Qualidade geral de conversa, usada só como fallback do displayScore |
interactionScore | number | null | Nota do scorecard do tipo de interação |
interactionType | string | null | Key do tipo de interação. null = não classificada |
interactionTypeSource | string | null | auto (detectado pela IA) ou manual |
interactionTypeLabel | string | null | Nome legível do tipo, já resolvido para tipos customizados da empresa |
summary | string | null | Resumo curto da conversa |
audioDurationSeconds | number | null | Duração do áudio em segundos |
attributionStatus | string | auto, manual, pending ou auto_supervisor |
O que esta rota NÃO devolve
transcript, transcriptSegments, audioUrl, audioKey, interactionScorecard, universalIndicators, diagnostic, bant, objections, dimensionScores, keywordDetection, productExploration, tokensUsed, cost e o diagnóstico de falha.
Não é restrição de permissão (a mesma API Key alcança tudo isso), é tamanho: uma página de 100 análises de 30 minutos passaria de 10 MB só de transcrição, e cada audioUrl é uma assinatura no storage gasta antes de você decidir se quer o áudio. Use a listagem para achar e GET /api/analyses/{id} para abrir.
Limite de Requisições
120 requisições por minuto, por empresa. A 100 itens por página são 12.000 análises por minuto de varredura. Ao estourar, a resposta é 429. O corpo não traz um campo com o tempo de espera: ele vem só no texto de message, Rate limit exceeded. Try again in 60s.
Respostas de Erro
| Status | Código | Descrição |
|---|---|---|
| 400 | BAD_REQUEST | from/to com data não interpretável, limit fora de 1..100, ou cursor mal formado |
| 401 | UNAUTHORIZED | API Key ausente, inválida ou expirada |
| 403 | FORBIDDEN | API Key sem o escopo api:analyses:read |
| 429 | TOO_MANY_REQUESTS | Rate limit atingido |
Uma empresa nunca enxerga a análise de outra: o filtro por companyId da API Key é a primeira condição da consulta ao banco, e não depende de cache nem de nada que o cliente envie. Filtrar por um salespersonId ou externalCallId de outra empresa devolve lista vazia, não a linha dela.
GET /api/analyses/{id}
Retorna a análise completa por ID: todos os campos do dossiê (scorecard, indicadores, diagnóstico, transcrição) mais os metadados internos (custo, tokens, observabilidade, integrações). A análise precisa pertencer à empresa da API Key; requisições para análises de outra empresa retornam 404 (isolamento multi-tenant).
Parâmetros de Path
| Parâmetro | Tipo | Descrição |
|---|---|---|
id | string | ID (uuid) da análise |
Headers
| Header | Obrigatório | Descrição |
|---|---|---|
X-API-Key | Sim | Sua API Key |
Resposta
{
"id": "anal_xxx",
"status": "completed",
"source": "upload",
"createdAt": "2024-01-15T12:00:00.000Z",
"updatedAt": "2024-01-15T12:05:00.000Z",
"salespersonName": "João Silva",
"clientName": "Cliente Teste",
"supervisorName": "Maria Souza",
"displayScore": 82,
"score": 87,
"interactionScore": 82,
"summary": "O vendedor demonstrou forte rapport...",
"profile": "Consultivo",
"stage": "Qualify",
"interactionType": "outbound-prospecting",
"interactionTypeSource": "auto",
"interactionScorecard": {
"typeKey": "outbound-prospecting",
"typeLabel": "Prospecção Outbound",
"macroFront": "comercial",
"score": 82,
"confidence": 0.9,
"criteria": [
{
"key": "string",
"label": "string",
"polarity": "objective",
"met": true,
"evidence": "string",
"timestamp": "01:23"
}
]
},
"universalIndicators": {
"methodAdherence": {
"score": 75,
"steps": [
{ "key": "string", "label": "string", "present": true, "timestamp": "00:12" }
]
},
"talkToListen": { "attendantPct": 40, "clientPct": 60 },
"interruptions": { "count": 2, "perTenMinutes": 1.5 },
"emotionalClimate": {
"start": "neutral", "middle": "positive",
"end": "positive", "overall": "positive"
}
},
"methodAdherenceScore": 75,
"attendantTalkPct": 40,
"interruptionsPerTenMin": 1.5,
"climateLabel": "positive",
"diagnostic": {
"executive_summary": "Cliente interessado em automação...",
"client_profile": {
"tone": "analytical", "engagement": "active",
"confidence": "high", "details": "Fez perguntas técnicas."
},
"core_motivation": { "problem": "Processo manual", "impact": "Perda de vendas" },
"hard_data": { "collected": ["Equipe de 5"], "missing": ["Orçamento"] },
"agent_evaluation": { "posture": "Cordial", "solution_effectiveness": "Avançou a negociação" },
"next_steps": { "status": "scheduled", "responsibilities": ["Enviar proposta"] }
},
"dimensionScores": { "rapport": 90, "listening": 85, "objections": 80, "clarity": 88 },
"bant": { "budget": "string", "authority": "string", "need": "string", "timing": "string" },
"objections": [{ "objection": "Preço alto", "resolved": true }],
"productExploration": { "explored": true, "products": ["Produto A"] },
"keywordDetection": {
"client_terms": [
{ "term": "preço", "polarity": "negative", "category": "objection", "context": null }
],
"attendant_terms": []
},
"transcript": "Transcrição completa da conversa...",
"transcriptSegments": [
{ "start": 0, "end": 5, "text": "Olá", "speaker": "Speaker 1" }
],
"audioUrl": "https://storage.../audio.mp3",
"audioKey": "audio/xyz.mp3",
"audioDurationSeconds": 420,
"tokensUsed": 1234,
"cost": 0.42,
"jobId": null,
"errorSource": null,
"errorDetail": null,
"autoRetryCount": 0,
"archivedAt": null,
"provider": null,
"externalCallId": null,
"externalOwnerId": null,
"attributionStatus": "auto",
"participants": null,
"salespersonId": "user_xxx",
"companyId": "company_xxx"
}Regras do Contrato
| Regra | Descrição |
|---|---|
displayScore | Nota oficial da análise: interactionScore ?? score ?? null, com uma exceção: havendo interactionScorecard sem nota, vem null. null = sem nota (nunca retorna 0 inventado) |
| Blocos jsonb | interactionScorecard, universalIndicators, diagnostic, dimensionScores, bant, objections, productExploration, keywordDetection, transcriptSegments vêm null quando ausentes ou em formato legado, sem gerar erro 500 |
transcriptSegments[].start/end | Segundos (number) ou "MM:SS" legado (string) |
timestamps de critérios/etapas | Formato "MM:SS" ou null |
stage | Connect, Qualify, Present, Close, Negotiate ou null |
| Campos descontinuados | strengths, improvements, phaseSuggestions não existem mais na resposta |
audioUrl | URL de playback resolvida na leitura; audioKey é o valor de origem do áudio como foi recebido: a chave de storage, a URL enviada ou a Data URL |
onvox | Bloco de telefonia (id da chamada, ramal, arquivo de gravação no PABX). Só existe quando a análise veio do PABX Onvox. Em upload, Google Meet ou API o campo não vem — nem como null. Escreva o consumidor tolerando a ausência (data.onvox?.call_id), nunca assumindo que a chave existe |
source | Sempre "upload" mesmo para análises criadas via POST /api/analyses. O campo ainda não distingue a origem API/UI (triggeredBy, usado internamente para o webhook, é o campo que de fato marca isso) |
Respostas de Erro
| Status | Código | Descrição |
|---|---|---|
| 401 | UNAUTHORIZED | API Key ausente, inválida ou expirada |
| 403 | FORBIDDEN | API Key sem o escopo api:analyses:read |
| 404 | NOT_FOUND | Análise não encontrada ou pertence a outra empresa |