Tudo o que você precisa para enviar um vídeo, criar uma análise e receber o resultado no seu produto. A URL base é https://www.tellia.com.br/api/v1. O contrato completo está em OpenAPI 3.1.
Do zero à primeira análise simulada, sem custo e sem enviar vídeo.
1
Crie a conta
Crie a sua conta e cadastre a empresa (razão social e CNPJ) em /desenvolvedores. Chaves, saldo e webhook ficam na empresa, no painel em Minha empresa > API.
2
Aceite os Termos da API
No painel, em Minha empresa > API, leia e aceite os Termos da API. Sem o aceite da versão atual, toda chamada responde termos_pendentes.
3
Crie uma chave de teste
No mesmo painel, crie uma chave de teste (tl_test_...). Ela aparece uma única vez: copie e guarde como segredo, numa variável de ambiente do seu servidor.
4
Crie uma análise no sandbox
Com a chave de teste, o vídeo é opcional e a resposta é simulada, no mesmo formato da produção:Terminal
export TELLIA_API_KEY="tl_test_..." # sua chave de teste
curl https://www.tellia.com.br/api/v1/analises \
-H "Authorization: Bearer $TELLIA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: primeiro-teste-001" \
-d '{
"duracaoSeg": 95,
"regua": "vendas",
"contexto": {
"situacao": "Pitch de vendas",
"publico": "Diretores de compras",
"objetivo": "Agendar uma demonstração"
},
"consentimento": {
"coletadoEm": "2026-10-30T13:58:00Z",
"versaoTexto": "consentimento-v1"
},
"referencia": "usuario-4821"
}'
A resposta vem na hora, com 201, status: "pronta" e simulado: true. Para ler de novo depois:Terminal
Cadastre no painel o webhook de teste: a URL HTTPS que recebe os eventos da chave de teste. Guarde o segredo whsec_... dele (aparece só na criação e quando você gera outro; o webhook de produção tem URL e segredo próprios). Repita o passo 4 com outra Idempotency-Key (com a mesma, a API devolve a análise já criada e não dispara evento): o evento analise.concluida chega na URL de teste, assinado. Para testar na sua máquina, use um túnel HTTPS com domínio público (endereço local, IP, porta e usuário e senha na URL não são aceitos). Como verificar a assinatura.
Toda requisição leva a chave no cabeçalho Authorization: Bearer tl_test_... (ou tl_live_...). A chave é o prefixo seguido de 40 letras e números. O ambiente é decidido pelo prefixo. O nome do esquema não diferencia maiúsculas de minúsculas (Bearer, bearer e BEARER valem), como manda o HTTP. Sem chave válida, a resposta é 401 com o cabeçalho WWW-Authenticate: Bearer.
Ambientes da API
Teste (sandbox)
Produção
Prefixo
tl_test_
tl_live_
Resultado
Simulado, marcado com simulado: true e textos [SIMULADO]
Análise real
Custo
Zero. Nada vai para a IA
Desconta do saldo o preço da sua faixa
Vídeo
Opcional
Obrigatório (uploadId e fileName)
Criação responde
201, status pronta
202, status na_fila
Webhook
Só o webhook de teste, com analise.concluida logo depois da criação
Só o webhook de produção, quando a análise termina
Resultado guardado
Até 12 meses, depois apagado
Até 12 meses, depois apagado
Liberação
Depois do aceite dos Termos da API
Liberada pela equipe, com Termos aceitos e saldo
Cada empresa tem até 10 chaves ativas. Use chaves separadas por serviço e ambiente, e revogue no painel qualquer chave que possa ter vazado: ela para de funcionar na hora (chave_revogada). Cada chave só enxerga as análises do próprio ambiente: listar, consultar e apagar uma análise do outro ambiente responde como se ela não existisse. O mesmo vale para os eventos: os da chave de teste vão só para o webhook de teste, e os da produção, só para o de produção.
O vídeo vai em duas etapas: você pede um envio (POST /v1/uploads) e manda os bytes em partes para a URL devolvida. Formatos aceitos: video/mp4, video/webm, video/quicktime e video/x-matroska, até 1 GB e 10 minutos.
POST/v1/uploads
Campos
Campo
Tipo
Obrigatório
Descrição
mimeType
string
sim
Um de: video/mp4, video/webm, video/quicktime, video/x-matroska.
tamanhoBytes
inteiro
sim
Tamanho exato do arquivo, de 1 a 1.000.000.000 bytes.
{
"objeto": "upload",
"uploadId": "up_4Zt9QmW2xLr7Kp1Vb8Nc3Hs6",
"expiraEm": "2026-10-30T15:00:00.000Z",
"simulado": false,
"envio": {
"url": "https://envio.example.com/",
"metodo": "POST",
"cabecalhos": {
"x-session-url": "https://envio.example.com/sessao/8c1f",
"x-exp": "1793372400000",
"x-sig": "4b7e0c9a..."
},
"parteBytes": 16777216,
"instrucoes": "Envie os bytes do vídeo em partes de `parteBytes` (a última pode ser menor), em ordem. Cada parte é um POST para `envio.url` com os cabeçalhos de `envio.cabecalhos` e mais `x-offset` (a posição, em bytes, do início da parte). Na última parte, mande também `x-final: 1`. A resposta da última parte traz o JSON do arquivo, `{ file: { name: \"files/...\" } }`: use esse `name` como `fileName` em POST /v1/analises. A assinatura vale até `expiraEm`."
}
}
O envio vale por 60 minutos (expiraEm): mande o vídeo e crie a análise antes disso, e use um envio para cada vídeo. Em produção, abrir um envio já exige saldo para ao menos uma análise (senão, 402saldo_insuficiente); nada é descontado aqui, o valor sai na criação. Com a chave de teste, a resposta vem com simulado: true e envio: null, e não há o que enviar.
Envios abertos e ainda sem análise também têm limite, em produção: no máximo 5 nas últimas 48 horas, e nunca mais do que o saldo paga (com saldo para 2 análises, até 2 envios sem análise). Acima disso, POST /v1/uploads responde 429limite_requisicoes: crie as análises dos vídeos já enviados antes de abrir outro envio. Um vídeo maior que 1 GB cuja duração não pôde ser medida é recusado: na criação, com 400upload_invalido (nada é descontado), ou, se o vídeo ainda estava em processamento na criação, na fila, e então a análise termina em falhou e o valor volta ao saldo.
Protocolo de envio em partes
Corte o arquivo em partes de parteBytes (16 MiB, múltiplo de 256 KiB). Só a última pode ser menor.
Mande as partes em ordem, uma de cada vez, com POST para envio.url, levando os cabeçalhos de envio.cabecalhos exatamente como vieram e mais x-offset: o byte inicial da parte (0, 16777216, 33554432...).
Na última parte, inclua também x-final: 1. A resposta dela traz o arquivo, { "file": { "name": "files/..." } }: esse name é o fileName da criação.
Se uma parte falhar, abra um envio novo (POST /v1/uploads) e recomece do início.
Exemplo em Node, com um cliente mínimo que os outros exemplos reaproveitam. Todo erro sai como um Error com status (o HTTP) e erro (o objeto de erro da API, ou null quando a resposta não veio da API), e resposta sem corpo devolve null:
tellia.mjs
// tellia.mjs (Node 18 ou mais novo: fetch nativo)
const API = "https://www.tellia.com.br/api/v1";
const CHAVE = process.env.TELLIA_API_KEY; // segredo do SEU servidor
export async function tellia(caminho, init = {}) {
const r = await fetch(`${API}${caminho}`, {
...init,
headers: {
Authorization: `Bearer ${CHAVE}`,
"Content-Type": "application/json",
...init.headers,
},
});
// Lê como texto: resposta sem corpo vem vazia, e um erro da plataforma no caminho
// (tempo esgotado, por exemplo) pode chegar sem JSON.
const texto = await r.text();
let corpo = null;
let semJson = false;
try {
corpo = texto ? JSON.parse(texto) : null;
} catch {
semJson = true;
}
if (!r.ok || semJson) {
const erro = corpo?.erro ?? null; // o erro da API, ou null se não veio dela
const e = new Error(erro ? `${erro.codigo}: ${erro.mensagem}` : `HTTP ${r.status} sem o erro da API`);
e.status = r.status; // guie as retentativas por aqui (429 e 5xx)
e.erro = erro;
throw e;
}
return corpo; // null quando a resposta não tem corpo
}
enviar-video.mjs
import { open, stat } from "node:fs/promises";
import { tellia } from "./tellia.mjs";
/** Envia um arquivo de vídeo e devolve { uploadId, fileName }. */
export async function enviarVideo(caminho, mimeType = "video/mp4") {
const { size } = await stat(caminho);
const upload = await tellia("/uploads", {
method: "POST",
body: JSON.stringify({ mimeType, tamanhoBytes: size }),
});
// Chave de teste: não há o que enviar. Crie a análise sem uploadId e fileName.
if (!upload.envio) return {};
const { url, cabecalhos, parteBytes } = upload.envio;
const arquivo = await open(caminho, "r");
try {
let resposta = null;
for (let offset = 0; offset < size; offset += parteBytes) {
const tamanho = Math.min(parteBytes, size - offset);
const parte = Buffer.alloc(tamanho);
await arquivo.read(parte, 0, tamanho, offset);
const final = offset + tamanho >= size;
const r = await fetch(url, {
method: "POST",
headers: {
...cabecalhos, // x-session-url, x-exp, x-sig
"x-offset": String(offset), // byte inicial desta parte
...(final ? { "x-final": "1" } : {}),
},
body: parte,
});
if (!r.ok) throw new Error(`Envio falhou no byte ${offset}: HTTP ${r.status}`);
resposta = await r.json();
}
// A resposta da última parte traz o arquivo: { file: { name: "files/..." } }
return { uploadId: upload.uploadId, fileName: resposta.file.name };
} finally {
await arquivo.close();
}
}
O id devolvido por POST /v1/uploads (up_...). No sandbox, opcional.
fileName
string
produção
O name devolvido no fim do envio (files/...). No sandbox, opcional; se mandar um dos dois, mande os dois.
duracaoSeg
número
sim
Duração do vídeo em segundos, até 600 (10 minutos). É arredondada para o segundo mais próximo e precisa dar pelo menos 1 (de 0,5 para cima); abaixo disso, a criação é recusada.
regua
string
não
Id de uma régua de GET /v1/reguas. Padrão: universal. A régua universal vem sempre; com uma régua de cenário, o resultado traz também cenario.
contexto.situacao
string
não
O momento da fala, como "Pitch de vendas" (até 120 caracteres). Com régua de cenário, vale a situação da própria régua e este campo não é usado: descreva o caso em publico e objetivo.
contexto.publico
string
não
Para quem a pessoa fala (até 120 caracteres).
contexto.objetivo
string
não
O que a fala quer conseguir (até 300 caracteres).
consentimento.coletadoEm
string ISO 8601
sim
Quando a pessoa filmada consentiu: data e hora com fuso explícito (Z ou ±hh:mm), com ou sem segundos, como 2026-10-30T13:58:00Z ou 2026-10-30T10:58:00-03:00. Sem fuso, só a data ou outro formato é recusado. Não pode estar no futuro: aceitamos até 5 minutos à frente do nosso relógio, pela diferença entre relógios.
consentimento.versaoTexto
string
sim
Qual texto de consentimento a pessoa aceitou (até 60 caracteres; acima disso, a criação é recusada).
referencia
string
não
Seu id interno, devolvido no resultado (até 100 caracteres). Não use nome nem e-mail.
produto
string
não
analise (o único hoje, e o padrão).
Em contexto e referencia, texto além do limite é cortado. O limite conta pontos de código Unicode, como o maxLength do JSON Schema, e o corte nunca parte um caractere ao meio; um emoji feito de vários pontos de código, como uma bandeira, pode perder a parte final. Valor que não é texto, como um número em referencia, ou um contexto que não é objeto, é recusado com 400pedido_invalido e o campo; ausente ou null é o mesmo que não informar. O cabeçalho opcional Idempotency-Key (de 1 a 255 caracteres visíveis, sem espaço) protege contra cobrança em dobro: a mesma chave, pela mesma empresa, devolve a mesma análise no estado atual, com o mesmo status HTTP e o cabeçalho Idempotent-Replayed: true, sem cobrar de novo e sem disparar outro webhook. Use um valor único por análise (um UUID, por exemplo) e reaproveite ao repetir depois de um erro de rede. A mesma chave usada com a chave do outro ambiente, ou depois de a análise ser apagada, responde 409 (conflito).
criar-analise.mjs
import { randomUUID } from "node:crypto";
import { tellia } from "./tellia.mjs";
import { enviarVideo } from "./enviar-video.mjs";
const { uploadId, fileName } = await enviarVideo("./pitch.mp4");
const analise = await tellia("/analises", {
method: "POST",
headers: { "Idempotency-Key": randomUUID() }, // guarde e reuse se precisar repetir
body: JSON.stringify({
uploadId,
fileName,
duracaoSeg: 95,
regua: "vendas",
contexto: {
situacao: "Pitch de vendas",
publico: "Diretores de compras",
objetivo: "Agendar uma demonstração",
},
consentimento: {
coletadoEm: consentimentoDoUsuario.aceitoEm, // quando a pessoa marcou a caixa
versaoTexto: "consentimento-v1", // qual texto ela viu
},
referencia: "usuario-4821", // seu id interno, sem nome nem e-mail
}),
});
console.log(analise.id, analise.status); // produção: "na_fila"
Em produção, a resposta é 202 com o resultado em status: "na_fila"; o webhook avisa quando terminar. No sandbox, é 201 já pronta.
Imagem e voz são dados pessoais sensíveis. Antes de enviar o vídeo, mostre à pessoa o texto de consentimento, guarde a prova do aceite e informe em cada criação quando ele foi coletado e qual versão do texto ela viu. Texto modelo (adapte o tom, não o conteúdo; mudou o texto, mude a versaoTexto):
Autorizo [nome da sua empresa] a enviar a gravação da minha imagem e voz para análise de comunicação feita pela Tellia, que mede expressões observáveis (como ritmo, clareza e hesitações) para gerar um retorno sobre a minha comunicação. O vídeo não é guardado: é apagado logo depois da análise. O resultado não é diagnóstico e não decide nada sozinho sobre mim. Posso pedir para ver ou apagar o resultado quando quiser.
Sem o atestado, a criação é recusada com pedido_invalido. Regras completas nos Termos da API (seção 5).
Devolve o resultado da análise (o mesmo objeto que chega no webhook). Análise de outra empresa, do outro ambiente (a chave de teste não lê a produção, e vice-versa) ou apagada responde 404: a API não confirma a existência. Exemplo completo de uma análise do sandbox com a régua de vendas:
200
{
"id": "sb_7Hq2Lm9Xc4Vt1Rk8Pw3Z",
"objeto": "analise",
"ambiente": "test",
"simulado": true,
"status": "pronta",
"criadoEm": "2026-10-30T14:00:00.000Z",
"referencia": "usuario-4821",
"duracaoSeg": 95,
"regua": {
"id": "vendas",
"versao": "1.0"
},
"notaGeral": 72,
"faixa": "Bom",
"dimensoes": [
{
"id": "clareza",
"nome": "Clareza",
"nota": 78,
"faixa": "Bom"
},
{
"id": "dominioAssunto",
"nome": "Domínio do assunto",
"nota": 74,
"faixa": "Bom"
},
{
"id": "seguranca",
"nome": "Segurança",
"nota": 64,
"faixa": "Médio"
},
{
"id": "engajamento",
"nome": "Engajamento",
"nota": 70,
"faixa": "Médio"
}
],
"cenario": {
"reguaId": "vendas",
"nome": "Pitch e conversa de vendas",
"versao": "1.0",
"nota": 68,
"faixa": "consistente",
"criterios": [
{
"id": "ven-problema",
"nome": "Problema do cliente antes do produto",
"nota": 2,
"escala": 4
},
{
"id": "ven-implicacao",
"nome": "Consequência e valor quantificados",
"nota": 3,
"escala": 4
},
{
"id": "ven-prova",
"nome": "Prova concreta",
"nota": 4,
"escala": 4
},
{
"id": "ven-objecao",
"nome": "Antecipa a objeção",
"nota": 2,
"escala": 4
},
{
"id": "ven-proximo-passo",
"nome": "Próximo passo explícito",
"nota": 3,
"escala": 4
}
]
},
"metricas": {
"palavrasPorMinuto": 140,
"totalPalavras": 222,
"abertura": {
"segundosAteIdeiaPrincipal": 9,
"estimado": true,
"faixa": "direta"
}
},
"relatorio": {
"resumo": "[SIMULADO] Comunicação clara, com abertura direta. O ponto a treinar é a segurança na conclusão.",
"pontosFortes": [
"[SIMULADO] Vai direto ao ponto nos primeiros segundos.",
"[SIMULADO] Usa um exemplo concreto para explicar a ideia."
],
"ajustes": [
"[SIMULADO] A voz cai no fim das frases.",
"[SIMULADO] A conclusão termina sem um próximo passo."
],
"momentos": [
{
"inicioSeg": 4,
"categoria": "Abertura",
"descricao": "[SIMULADO] Começou direto na ideia principal."
},
{
"inicioSeg": 76,
"categoria": "Volume",
"descricao": "[SIMULADO] A voz caiu no fim da frase."
}
],
"plano7Dias": {
"foco": "seguranca",
"objetivo": "[SIMULADO] Sustentar o volume até a última palavra.",
"dias": [
{
"dia": 1,
"titulo": "[SIMULADO] Dia 1",
"exercicio": "Leitura em voz alta",
"instrucoes": "[SIMULADO] Leia um parágrafo mantendo o volume até o ponto final.",
"duracaoMin": 10
},
{
"dia": 2,
"titulo": "[SIMULADO] Dia 2",
"exercicio": "Leitura em voz alta",
"instrucoes": "[SIMULADO] Leia um parágrafo mantendo o volume até o ponto final.",
"duracaoMin": 10
},
{
"dia": 3,
"titulo": "[SIMULADO] Dia 3",
"exercicio": "Leitura em voz alta",
"instrucoes": "[SIMULADO] Leia um parágrafo mantendo o volume até o ponto final.",
"duracaoMin": 10
},
{
"dia": 4,
"titulo": "[SIMULADO] Dia 4",
"exercicio": "Leitura em voz alta",
"instrucoes": "[SIMULADO] Leia um parágrafo mantendo o volume até o ponto final.",
"duracaoMin": 10
},
{
"dia": 5,
"titulo": "[SIMULADO] Dia 5",
"exercicio": "Leitura em voz alta",
"instrucoes": "[SIMULADO] Leia um parágrafo mantendo o volume até o ponto final.",
"duracaoMin": 10
},
{
"dia": 6,
"titulo": "[SIMULADO] Dia 6",
"exercicio": "Leitura em voz alta",
"instrucoes": "[SIMULADO] Leia um parágrafo mantendo o volume até o ponto final.",
"duracaoMin": 10
},
{
"dia": 7,
"titulo": "[SIMULADO] Dia 7",
"exercicio": "Leitura em voz alta",
"instrucoes": "[SIMULADO] Leia um parágrafo mantendo o volume até o ponto final.",
"duracaoMin": 10
}
]
}
}
}
Estados
Estados da análise
status
O que significa
na_fila
Recebida, esperando a vez. Nota e relatório vêm vazios.
processando
Em análise agora.
pronta
Resultado completo. É o único estado com nota, métricas e relatório.
em_revisao
Retida para conferência da equipe (Termos da API, seção 7). O motivo não é informado.
nao_entregue
A equipe confirmou a retenção: esta análise não será entregue, e o valor não volta (Termos da API, seção 3).
falhou
Não foi possível analisar. Se a falha foi nossa, o valor volta ao saldo.
Fora de pronta, notaGeral, faixa, cenario, metricas e relatorio vêm null e dimensoes vem vazia.
Campos
Campos
Campo
Tipo
Descrição
id
string
Id da análise. No sandbox começa com sb_.
objeto
string
Sempre analise.
ambiente
string
test (sandbox) ou live (produção).
simulado
booleano
true no sandbox: o resultado não veio de um vídeo.
status
string
Um dos estados acima.
criadoEm
string ISO 8601
Quando a análise foi criada.
referencia
string | null
O seu id interno, como foi enviado na criação (sem os espaços das pontas e cortado no limite).
duracaoSeg
número | null
Duração do vídeo em segundos.
regua.id
string
A régua pedida (universal ou um cenário).
regua.versao
string | null
Versão da régua universal usada na nota geral. A versão da régua de cenário vem em cenario.versao.
notaGeral
inteiro | null
Nota de 0 a 100 na régua universal. É a única comparável entre análises (com a mesma versão).
faixa
string | null
Faixa da nota geral: Baixo (0 a 50), Médio (51 a 70), Bom (71 a 90), Excelente (91 a 100).
dimensoes[]
lista
Uma por dimensão (clareza: Clareza; dominioAssunto: Domínio do assunto; seguranca: Segurança; engajamento: Engajamento), com id, nome, nota de 0 a 100 e faixa.
cenario
objeto | null
Só com régua de cenário: reguaId, nome, versao, nota de 0 a 100 e faixa. Pode vir null mesmo com régua de cenário, quando ela não pôde ser aplicada nesta análise (a nota geral e o relatório vêm normalmente): trate como opcional.
cenario.faixa
string
Faixa da régua de cenário: inicial (0 a 39), em desenvolvimento (40 a 64), consistente (65 a 84), referência (85 a 100).
cenario.criterios[]
lista
Cada critério da régua com id, nome, nota de 0 a 4 (escala: 4) ou null quando não pôde ser avaliado nesta fala.
metricas.palavrasPorMinuto
inteiro | null
Ritmo médio da fala. null quando não houve fala.
metricas.totalPalavras
inteiro | null
Palavras faladas no vídeo.
metricas.abertura
objeto | null
Segundos até a ideia principal (segundosAteIdeiaPrincipal, sempre estimado: true) e a faixa: direta (até 15 s), adequada (até 30 s) ou longa. null quando não deu para medir.
relatorio.resumo
string
Resumo da comunicação, pronto para mostrar à pessoa.
relatorio.pontosFortes[]
lista de string
O que a pessoa já faz bem.
relatorio.ajustes[]
lista de string
O que treinar primeiro.
relatorio.momentos[]
lista
Momentos do vídeo: inicioSeg, categoria e descricao.
relatorio.plano7Dias
objeto | null
Plano de treino: foco (id da dimensão), objetivo e dias (dia, titulo, exercicio, instrucoes, duracaoMin).
Os textos descrevem expressões observáveis (ritmo, clareza, hesitações), nunca emoção, personalidade ou diagnóstico. Trate campos desconhecidos como opcionais: campos novos podem entrar na v1 sem quebrar nada, e mudança que quebra sai numa versão nova.
Só análises criadas antes desta data e hora, com fuso explícito (a mesma regra de coletadoEm). Use o proximo da página anterior; um fuso com + vai na URL como %2B.
Das mais novas para as mais antigas, só do ambiente da chave usada. Enquanto temMais for true, passe proximo como antesDe para buscar a página seguinte. Cada item de dados é um resultado completo (encurtado aqui).
import { tellia } from "./tellia.mjs";
let antesDe = null;
do {
const qs = new URLSearchParams({ limite: "50" });
if (antesDe) qs.set("antesDe", antesDe);
const pagina = await tellia(`/analises?${qs}`);
for (const a of pagina.dados) console.log(a.id, a.status, a.notaGeral);
antesDe = pagina.temMais ? pagina.proximo : null;
} while (antesDe);
Apaga o resultado de vez. Use quando a pessoa analisada pedir a exclusão dos dados: você é o controlador e a Tellia apaga na hora, pela API. Depois disso, GET responde 404. Enquanto a análise está na_fila ou processando, a resposta é 409 (conflito): espere o estado final e apague depois. Apagar não devolve o valor da análise.
O vídeo nunca fica guardado: ele é apagado assim que as medições são extraídas. Os resultados, de produção e do sandbox, ficam guardados por até 12 meses e então são apagados automaticamente (depois disso, GET responde 404 e a análise sai da lista). Para apagar antes, use esta rota (Termos da API, seção 6). Se precisar do resultado por mais tempo, guarde do seu lado o que for necessário.
A régua universal vem em toda análise. As réguas de cenário somam uma nota própria, com critérios de 0 a 4, para um tipo de fala. Toda régua tem versão: mudou âncora, peso ou critério, a versão muda.
universaluniversal · versão 1.0
Régua universal de comunicação
Nota de 0 a 100 em clareza, domínio do assunto, segurança e engajamento. Vem em toda análise e é a única comparável entre análises.
vendascenário · versão 1.0
Pitch e conversa de vendas
Se a fala parte do problema do cliente, mostra valor com prova, antecipa a objeção e fecha com um próximo passo.
liderancacenário · versão 1.0
Comunicar mudança ou direção ao time
Se a fala explica o porquê, diz o que muda para quem ouve, é transparente sobre o que está em aberto e combina os próximos passos.
conversa-dificilcenário · versão 1.0
Feedback e conversa difícil
Se a fala trata de comportamento e não da pessoa, explica o impacto, diz a mensagem difícil sem rodeio, ouve o outro e combina um caminho.
atendimentocenário · versão 1.0
Atendimento a cliente insatisfeito
Se a fala reconhece o problema concreto, assume sem culpar ninguém, resolve com prazo e não se perde em desculpas.
imprensacenário · versão 1.0
Porta-voz e entrevista à imprensa
Se a fala responde à pergunta, entrega uma mensagem citável, põe as pessoas antes da defesa, fica nos fatos e usa linguagem do público.
ensinocenário · versão 1.0
Ensinar e explicar
Se a fala diz o que o ouvinte vai aprender, sinaliza a estrutura, liga o novo ao conhecido, mostra um exemplo passo a passo e checa a compreensão.
entrevista-candidatocenário · versão 1.0
Respostas de entrevista de emprego
Se a resposta conta um caso real com situação, ação e resultado, deixa claro o que você fez, mede o resultado, liga à vaga e mostra aprendizado.
Para treino de entrevista. Não use o resultado para decidir uma seleção (Termos da API, seção 4).
200 (encurtado)
{
"objeto": "lista",
"dados": [
{
"id": "universal",
"nome": "Régua universal de comunicação",
"descricao": "Nota de 0 a 100 em clareza, domínio do assunto, segurança e engajamento. Vem em toda análise e é a única comparável entre análises.",
"versao": "1.0",
"tipo": "universal",
"aviso": null
},
{
"id": "vendas",
"nome": "Pitch e conversa de vendas",
"descricao": "Se a fala parte do problema do cliente, mostra valor com prova, antecipa a objeção e fecha com um próximo passo.",
"versao": "1.0",
"tipo": "cenario",
"aviso": null
}
]
}
Saldo disponível agora, em centavos de real. Saldo vencido conta como zero.
saldoValidoAte
string ISO 8601 | null
Até quando o saldo vale (12 meses a partir da compra).
precoAnaliseCentavos
inteiro
Quanto cada análise desconta do saldo com a chave usada. Com chave de teste, vem 0 (nada é descontado).
hoje
objeto
Chamadas e gasto da chave hoje (horário de Brasília) e o teto do dia.
limites
objeto
Requisições por minuto e duração máxima do vídeo.
Valores sempre em centavos de real. O saldo é comprado no painel (Minha empresa > API), com Pix, boleto ou cartão: a primeira compra é de pelo menos R$ 7.450 e as seguintes a partir de R$ 1.490. O saldo vale 12 meses a partir da compra. Compra nova com o saldo ainda válido soma, e a validade de todo o saldo vai para 12 meses a partir dela (nunca encurta). Saldo que já venceu não volta com uma compra nova: ele sai do saldo como vencimento (aparece nos movimentos do painel) e só o valor da compra nova entra. Cada análise em produção desconta o preço da sua faixa na hora da criação; se a falha for nossa, o valor volta sozinho. Ver preços.
São dois webhooks, configurados no painel (Minha empresa > API), cada um com URL e segredo próprios: o de produção e o de teste. Os eventos das análises da chave de teste vão só para o webhook de teste, e os da produção, só para o de produção: um teste nunca chega assinado ao seu servidor de produção. A cada evento, a Tellia faz um POST com o corpo em JSON e o cabeçalho de assinatura.
A URL precisa ser HTTPS, num domínio público com ponto (como api.suaempresa.com.br), na porta 443 ou sem porta, sem usuário e senha (a Tellia se identifica pela assinatura Tellia-Assinatura) e sem espaço ou caractere de controle. IP e endereço local não são aceitos no cadastro, e um domínio que resolve para rede interna é recusado na hora do envio (o evento não é entregue). A URL é gravada na forma normalizada (por exemplo, sem :443 e com o domínio em minúsculas), e é essa forma que o painel mostra.
Eventos de webhook
tipo
Quando
analise.concluida
A análise ficou pronta (status pronta). No sandbox, chega logo depois da criação.
analise.falhou
A análise não será entregue: falhou (status falhou; se a falha foi nossa, o valor volta ao saldo) ou a equipe confirmou a retenção (status nao_entregue). Confira o status em dados.
analise.retida
A análise foi retida para conferência (status em_revisao). Depois da conferência chega um segundo evento: analise.concluida (liberada) ou analise.falhou (não entregue).
O corpo traz o evento e, em dados, o mesmo objeto do GET /v1/analises/{id} no momento da entrega (encurtado aqui):
O cabeçalho Tellia-Assinatura tem o formato t=<unix em segundos>,v1=<hex>. O v1 é o HMAC-SHA256, com o segredo whsec_... inteiro do webhook daquele ambiente como chave, de `${t}.${corpo}`, onde corpo são os bytes exatos recebidos. Para conferir:
Leia o corpo bruto, antes de qualquer parse de JSON.
Recuse se t estiver a mais de 300 segundos do seu relógio (protege contra reenvio de um evento antigo).
Calcule o HMAC e compare com o v1 em tempo constante.
Node (Express)
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
// whsec_... do webhook deste ambiente (o de teste e o de produção são diferentes)
const SEGREDO = process.env.TELLIA_WEBHOOK_SECRET;
const TOLERANCIA_SEG = 300;
function assinaturaValida(cabecalho, corpo) {
const partes = Object.fromEntries(
String(cabecalho ?? "").split(",").map((p) => {
const i = p.indexOf("=");
return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
}),
);
const t = Number(partes.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > TOLERANCIA_SEG) return false;
const esperado = Buffer.from(createHmac("sha256", SEGREDO).update(`${t}.${corpo}`).digest("hex"));
const recebido = Buffer.from(String(partes.v1 ?? ""));
return recebido.length === esperado.length && timingSafeEqual(recebido, esperado);
}
const app = express();
// Corpo BRUTO: a assinatura vale para os bytes exatos, antes do JSON.parse.
app.post("/webhooks/tellia", express.raw({ type: "application/json" }), (req, res) => {
const corpo = req.body.toString("utf8");
if (!assinaturaValida(req.get("Tellia-Assinatura"), corpo)) return res.sendStatus(400);
res.sendStatus(204); // responda rápido...
processarEvento(JSON.parse(corpo)); // ...e trabalhe depois (descarte ids já vistos)
});
app.listen(3000);
Python (Flask)
import hashlib, hmac, os, time
from flask import Flask, abort, request
SEGREDO = os.environ["TELLIA_WEBHOOK_SECRET"] # whsec_... do webhook deste ambiente
TOLERANCIA_SEG = 300
app = Flask(__name__)
def assinatura_valida(cabecalho: str, corpo: bytes) -> bool:
partes = dict(p.strip().split("=", 1) for p in cabecalho.split(",") if "=" in p)
try:
t = int(partes.get("t", ""))
except ValueError:
return False
if abs(time.time() - t) > TOLERANCIA_SEG:
return False
esperado = hmac.new(SEGREDO.encode(), f"{t}.".encode() + corpo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperado.encode(), partes.get("v1", "").encode())
@app.post("/webhooks/tellia")
def webhook_tellia():
corpo = request.get_data() # bytes exatos, antes de qualquer parse
if not assinatura_valida(request.headers.get("Tellia-Assinatura", ""), corpo):
abort(400)
evento = request.get_json()
# guarde evento["id"] para não processar duas vezes; trabalhe fora da requisição
return "", 204
Para testar o seu verificador, este exemplo é válido (ignore a tolerância de tempo ao testar):
Responda com qualquer 2xx em até 10 segundos. Faça o trabalho pesado depois de responder.
Sem 2xx, tentamos de novo depois de 1 min, 5 min, 30 min, 2 h e 6 h (até 6 tentativas no total). Redirecionamento não é seguido: cadastre a URL final.
As esperas são o mínimo. As retentativas de cada empresa saem em pequenos lotes, em paralelo: com vários eventos pendentes, uma retentativa pode chegar um pouco depois da espera, e o seu endpoint pode receber mais de um evento ao mesmo tempo e fora de ordem. Decida pelo dados.status, não pela ordem de chegada.
O mesmo evento pode chegar mais de uma vez, sempre com o mesmo id: guarde os ids já processados e descarte repetições.
O webhook avisa; a verdade é o GET /v1/analises/{id}. Se perder um evento, consulte a análise.
Todo erro tem o mesmo formato, com o status HTTP do código. Alguns trazem campos a mais (como campo ou saldoCentavos). Método errado numa rota que existe responde 405 (metodo_invalido) no mesmo formato, com o cabeçalho Allow listando os métodos aceitos; caminho que não existe, inclusive a URL base /v1 sozinha, responde 404 (nao_encontrada), também em JSON. Os 401 trazem o cabeçalho WWW-Authenticate: Bearer. Um erro da plataforma no caminho (tempo esgotado, por exemplo) pode chegar sem esse corpo: leia a resposta como texto e guie as retentativas pelo status HTTP, como faz o cliente tellia.mjs.
Sem o cabeçalho Authorization, com outro esquema que não Bearer, chave em formato errado ou inexistente, ou conta da empresa desativada. Vem com o cabeçalho WWW-Authenticate: Bearer.
Confira o cabeçalho Authorization: Bearer tl_... e a chave no painel.
chave_revogada
401
A chave foi revogada no painel. Vem com o cabeçalho WWW-Authenticate: Bearer.
Crie outra chave e troque no seu servidor.
termos_pendentes
403
A versão atual dos Termos da API ainda não foi aceita pela empresa.
Aceite no painel (Minha empresa > API) e repita a chamada.
producao_desligada
403
Chave de produção usada antes de a equipe liberar a produção.
Use a chave de teste enquanto isso, ou fale com a equipe.
limite_requisicoes
429
Mais de 60 requisições no mesmo minuto com a mesma chave, inclusive numa rajada de chamadas ao mesmo tempo. Em POST /v1/uploads, também quando a empresa já tem envios demais sem análise (veja Envio do vídeo).
Espere o minuto seguinte e repita, com espera crescente entre as tentativas. Nos envios, crie as análises dos vídeos já enviados antes de abrir outro.
teto_diario
429
A chave chegou ao teto de gasto do dia (R$ 3.000).
Espere o dia seguinte (horário de Brasília) ou fale com a equipe para ajustar.
saldo_insuficiente
402
O saldo disponível não cobre uma análise, ou o saldo venceu. Em produção vale na criação e já na abertura do envio.
Compre saldo no painel. O erro traz saldoCentavos e precoCentavos.
pedido_invalido
400
Corpo ou parâmetro inválido, inclusive um campo com o tipo errado (como um número onde se espera texto).
Leia mensagem (e campo, quando vier) e corrija. Repetir o mesmo pedido não adianta.
upload_invalido
400
O envio não existe, venceu, já foi usado ou não é da sua empresa; o fileName não confere com o envio; o vídeo não pôde ser lido; o vídeo medido passa de 10 minutos; ou a duração não pôde ser medida e o vídeo tem mais de 1 GB.
Abra um envio novo em POST /v1/uploads e mande o vídeo de novo (um envio por vídeo).
nao_encontrada
404
A análise não existe, foi apagada (por você ou depois da guarda de 12 meses), é do outro ambiente (teste ou produção) ou não é da sua empresa. Também numa rota que não existe em /v1, inclusive a própria URL base (/v1 sozinha).
Confira o id e a chave usada. Por segurança, a API não diz qual dos casos é.
metodo_invalido
405
Método HTTP que a rota não aceita, numa rota que existe (por exemplo, PUT em /v1/analises). A resposta traz o cabeçalho Allow com os métodos aceitos.
Use um dos métodos do cabeçalho Allow, os mesmos desta documentação.
conflito
409
Pedido para apagar uma análise que ainda está na fila ou em processamento, ou Idempotency-Key já usada com a chave do outro ambiente ou numa análise que foi apagada.
Para apagar, espere o estado final (o webhook avisa). Na criação, use outra Idempotency-Key.
erro_interno
500
Falha do nosso lado.
Repita com a mesma Idempotency-Key. Se persistir, abra um chamado no painel.
60 por minuto, em todas as rotas (vale também para uma rajada de chamadas ao mesmo tempo)
limite_requisicoes (429)
Gasto por chave
R$ 3.000 por dia (horário de Brasília)
teto_diario (429)
Duração do vídeo
Até 10 minutos (600 s), conferida também no vídeo enviado
pedido_invalido (400) pela duracaoSeg; upload_invalido (400) pelo vídeo medido na criação. Se o vídeo ainda estava em processamento na criação, a medida sai depois: a análise termina em falhou e o valor volta ao saldo
Tamanho do arquivo
Até 1 GB (1.000.000.000 bytes)
pedido_invalido (400) na abertura do envio. Vídeo acima de 1 GB sem a duração medida é recusado: upload_invalido (400) na criação ou, se ainda estava em processamento, na fila (termina em falhou e o valor volta ao saldo)
Envios sem análise
Até 5 nas últimas 48 h, e nunca mais do que o saldo paga (produção)
Nunca chame a API do navegador ou do app do usuário final com a chave. O app fala com o seu servidor, e o seu servidor fala com a Tellia.
Guarde a chave como segredo (variável de ambiente ou cofre). Nada de chave em repositório, log, print ou ticket. Vazou? Revogue no painel e crie outra.
Use Idempotency-Key em toda criação e reaproveite a mesma ao repetir: erro de rede não vira cobrança em dobro.
Prefira o webhook a consultar em laço. Se precisar consultar, espace as consultas e respeite o limite por minuto.
Verifique a assinatura de todo webhook, com o segredo do ambiente certo, e responda rápido. Não aponte o webhook de teste para o seu servidor de produção.
Em 429 e 500, repita com espera crescente. Em 400, corrija o pedido.
Mostre o texto de consentimento antes de gravar, guarde a prova e mande o atestado verdadeiro em cada criação.
Em referencia, use um id seu, sem nome, e-mail ou documento da pessoa.
Apague pela API quando a pessoa pedir a exclusão, e não guarde o vídeo do seu lado além do necessário.
O resultado apoia o desenvolvimento da comunicação: não use como única base para decidir sobre uma pessoa e não use a análise completa em processo seletivo, porque ela considera expressões do rosto e da voz (Termos da API, seção 4).
Lançamento: envio do vídeo em partes, criação, consulta, listagem e exclusão de análises, catálogo de réguas, uso e saldo, webhooks assinados (um de teste e um de produção) e sandbox.
Em breve: métricas leves, entrevista e reuniões.
Mudança que quebra integração sai numa versão nova (por exemplo, /v2), e a anterior continua funcionando por pelo menos 6 meses (Termos da API, seção 9). Dúvidas técnicas: chamados do painel ou fale com a equipe.