Sumário
- Endpoints
- 1. POST /api/v1/send-message
- 2. POST /api/v1/send-template
- 3. POST /api/external/messages/send
- 4. PATCH /api/v1/leads/{id}
- Webhooks Outbound
- Como configurar
- Headers enviados em todo webhook
- Eventos disponíveis
- message.received
- message.sent
- conversation.assigned
- conversation.resolved
- Fluxo recomendado para automações (n8n / Typebot)
- Resumo rápido
API Pública do Tivar
Documentação dos endpoints públicos e webhooks outbound do Tivar.
Base URL: https://<seu-dominio>
Autenticação: todos os endpoints exigem Authorization: Bearer <workspace_api_key>
Rate limit: 60 requisições por minuto por workspace (exceto /api/external/messages/send — sem rate limit próprio)
Obtenha sua API Key em: Configurações → Workspace → API Key
Endpoints
1. POST /api/v1/send-message
Envia uma mensagem de texto para um número de telefone. Cria o contato e a conversa automaticamente se ainda não existirem.
Canais suportados: WhatsApp Oficial (Meta Cloud API), WhatsApp Lite e WhatsApp via UAZAPI.
Headers
| Header | Valor |
|---|---|
Authorization | Bearer <workspace_api_key> |
Content-Type | application/json |
Body
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
phone | string | ✅ | Número do destinatário. Aceita E.164 (+5511999998888) ou só dígitos com DDD (5511999998888). Mínimo 10 dígitos. |
message | string | ✅ | Texto a enviar. Entre 1 e 4096 caracteres. |
channel | string | ✅ | "whatsapp-official" (Meta Cloud API), "uazapi" ou "waha" (WhatsApp Lite). |
integrationId | string (UUID) | ❌ | ID de uma integração específica. Se omitido, usa a integração conectada mais recente do canal escolhido. |
agentId | string (UUID) | ❌ | Atribui a conversa a um agente de IA. |
contactName | string | ❌ | Nome do contato exibido no Tivar. Recomendado: se omitido, o contato fica sem nome e o número de telefone é exibido como fallback no chat. |
Boa prática para automações: sempre passe
contactNamecom o nome real do lead/cliente. Sem ele, o contato é criado sem nome e fica difícil de identificar no painel.
Exemplo de requisição
curl -X POST https://<seu-dominio>/api/v1/send-message \
-H "Authorization: Bearer sk_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"phone": "5511999998888",
"message": "Olá! Seu pedido foi confirmado.",
"channel": "whatsapp-official",
"contactName": "João Silva"
}'
Resposta de sucesso — 200 OK
{
"success": true,
"conversationId": "uuid-da-conversa",
"contactId": "uuid-do-contato",
"messageId": "uuid-da-mensagem",
"waMessageId": "wamid.xxxxx",
"contactCreated": false,
"conversationCreated": false,
"sent_at": "2026-04-23T10:00:00.000Z"
}
| Campo | Descrição |
|---|---|
conversationId | UUID da conversa no Tivar |
contactId | UUID do contato |
messageId | UUID da mensagem no banco |
waMessageId | ID da mensagem retornado pelo provedor |
contactCreated | true se o contato foi criado agora |
conversationCreated | true se a conversa foi criada agora |
Erros possíveis
| Status | error | O que fazer |
|---|---|---|
400 | "Parâmetros obrigatórios: phone, message, channel" | Verifique os campos obrigatórios. |
400 | "channel inválido. Use \"whatsapp-official\", \"uazapi\" ou \"waha\"" | Corrija o valor de channel. |
502 | "Erro ao enviar via WhatsApp Lite" | Falha no provedor. Tente novamente. |
500 | "Configuração do WhatsApp Lite incompleta" | Integração WhatsApp Lite sem credenciais configuradas — reconecte em Configurações → Integrações. |
400 | "phone inválido. Forneça em E.164 ou só dígitos (mínimo 10)." | Formate o número corretamente. |
400 | "message deve ser string de 1–4096 caracteres" | Mensagem vazia ou muito longa. |
400 | "Integração não está conectada" | Reconecte a integração no painel. |
401 | "API Key inválida" | Verifique a API Key. |
403 | "Workspace inativo" | Workspace suspenso — entre em contato com o suporte. |
404 | "Nenhuma integração \"whatsapp-official\" conectada neste workspace" | Conecte uma integração do canal informado. |
429 | "Muitas requisições. Limite: 60/min por workspace." | Aguarde e reenvie. |
4xx + "requires_template": true | Erro de janela 24h (código Meta 131047) | O número não interagiu nas últimas 24h. Use /api/v1/send-template com um template HSM aprovado. |
2. POST /api/v1/send-template
Envia um template HSM (mensagem estruturada pré-aprovada pela Meta) via WhatsApp Oficial. Único método válido para iniciar contato com números que não interagiram nas últimas 24 horas.
Canal suportado: WhatsApp Oficial (whatsapp-official) apenas.
Headers
| Header | Valor |
|---|---|
Authorization | Bearer <workspace_api_key> |
Content-Type | application/json |
Body
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
phone | string | ✅ | Número do destinatário. Mesmo formato do send-message. |
channel | string | ✅ | Deve ser "whatsapp-official". |
templateName | string | ✅ | Nome exato do template aprovado no Meta Business Manager. |
language | string | ❌ | Código do idioma do template. Padrão: "pt_BR". |
variables | array | ❌ | Lista de valores para substituir as variáveis {{1}}, {{2}} etc. do corpo do template. Pode ser string ou número. |
headerMedia | object | ❌ | Necessário só se o template tiver header de mídia (imagem/vídeo/documento). Ver formato abaixo. |
integrationId | string (UUID) | ❌ | ID de integração específica. Se omitido, usa a conectada mais recente. |
agentId | string (UUID) | ❌ | Atribui a conversa a um agente de IA. |
contactName | string | ❌ | Nome do contato exibido no Tivar. Recomendado: se omitido, o contato fica sem nome e o número de telefone é exibido como fallback no chat. |
Boa prática para automações (n8n, Make, etc.): sempre passe
contactName. Este campo é o que define o nome exibido na conversa dentro do Tivar. Se não for enviado, o painel exibirá apenas o número de telefone no lugar do nome.
Exemplo de requisição
curl -X POST https://<seu-dominio>/api/v1/send-template \
-H "Authorization: Bearer sk_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"phone": "5511999998888",
"channel": "whatsapp-official",
"templateName": "confirmacao_pedido",
"language": "pt_BR",
"variables": ["João", "Pedido #1234", "R$ 150,00"],
"contactName": "João Silva"
}'
As variáveis substituem
{{1}},{{2}},{{3}}na ordem em que aparecem no array.
Template com header de mídia (imagem/vídeo/documento)
Se o template aprovado tiver um header de mídia, informe headerMedia — sem isso, o envio falha (Meta exige o header preenchido quando o template foi aprovado com ele).
{
"phone": "5511999998888",
"channel": "whatsapp-official",
"templateName": "confirmacao_com_video",
"language": "pt_BR",
"variables": ["João"],
"headerMedia": {
"type": "video",
"link": "https://exemplo.com/video-institucional.mp4"
},
"contactName": "João Silva"
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
headerMedia.type | string | ✅ | "image", "video" ou "document" — deve bater com o tipo de header aprovado no template. |
headerMedia.link | string | ✅ | URL pública (sem autenticação) do arquivo. A Meta baixa direto dessa URL no momento do envio — link expirado/privado falha o envio. |
headerMedia.filename | string | ❌ | Só pra type: "document" — nome do arquivo exibido pro destinatário. |
Template com botão dinâmico (URL com sufixo variável, ou quick reply com payload variável)
Só é necessário declarar buttons se o botão tiver parte dinâmica (ex.: link de rastreio único por pedido). Botão totalmente estático (telefone, ou quick reply fixo) já funciona sozinho, sem precisar disso.
{
"phone": "5511999998888",
"channel": "whatsapp-official",
"templateName": "pedido_confirmado",
"language": "pt_BR",
"variables": ["João", "Pedido #1234"],
"buttons": [
{ "index": 0, "type": "url", "text": "pedido-1234" }
],
"contactName": "João Silva"
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
buttons[].index | number | ✅ | Posição do botão no template, começando em 0 (ordem em que a Meta aprovou). |
buttons[].type | string | ✅ | "url" ou "quick_reply". |
buttons[].text | string | ✅ (se type: "url") | Valor que substitui a parte variável do link do botão (ex.: template aprovado com link https://seusite.com/pedido/{{1}} → text: "1234" gera .../pedido/1234). |
buttons[].payload | string | ✅ (se type: "quick_reply") | Valor dinâmico do payload retornado quando o contato clica no botão. |
Resposta de sucesso — 200 OK
{
"success": true,
"conversationId": "uuid-da-conversa",
"contactId": "uuid-do-contato",
"messageId": "uuid-da-mensagem",
"waMessageId": "wamid.xxxxx",
"contactCreated": false,
"conversationCreated": true,
"template": {
"name": "confirmacao_pedido",
"language": "pt_BR",
"variables": ["João", "Pedido #1234", "R$ 150,00"],
"headerMedia": null,
"buttons": null
},
"sent_at": "2026-04-23T10:00:00.000Z"
}
Erros possíveis
| Status | error / meta_error_code | O que fazer |
|---|---|---|
400 | "Parâmetros obrigatórios: phone, templateName, channel" | Verifique os campos. |
400 | "Templates HSM só são suportados pelo canal \"whatsapp-official\"" | channel deve ser "whatsapp-official". |
400 | "variables deve ser array" | Passe um array, mesmo que vazio: []. |
400 | "headerMedia.type inválido..." / "headerMedia.link é obrigatório..." | Corrija o objeto headerMedia. |
400 | "buttons deve ser array" / "buttons[].index..." / "buttons[].type..." / "buttons[].text..." / "buttons[].payload..." | Corrija o array buttons. |
401 | "API Key inválida" | Verifique a API Key. |
404 | "Nenhuma integração WhatsApp Oficial conectada" | Conecte a integração no painel. |
429 | "Muitas requisições. Limite: 60/min por workspace." | Aguarde e reenvie. |
4xx + meta_error_code: 132001 | Template não encontrado ou não aprovado | Confirme o templateName e o idioma no Meta Business Manager. |
4xx + meta_error_code: 132000 | Número de variáveis incorreto | O array variables deve ter exatamente o número de {{N}} do template. |
4xx + meta_error_code: 131053 | Mídia do header inválida/inacessível | Confirme que headerMedia.link é público e o type bate com o aprovado no template. |
3. POST /api/external/messages/send
Envia uma mensagem em uma conversa já existente no Tivar. Ideal para integrações como n8n, Typebot e Voiceflow que recebem o conversation_id via webhook e querem responder na mesma conversa.
Canais suportados: WhatsApp Oficial, WhatsApp (UAZAPI), WhatsApp Lite, Telegram, Live Chat.
Diferença em relação ao
/api/v1/send-message: este endpoint não cria contato nem conversa — a conversa deve existir previamente. Use quando já tem oconversation_id(por exemplo, recebido num webhookmessage.received).
Headers
| Header | Valor |
|---|---|
Authorization | Bearer <workspace_api_key> |
Content-Type | application/json |
Body
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
conversation_id | string (UUID) | ✅ | ID da conversa no Tivar. |
text | string | ✅ | Texto da mensagem. |
sender_type | string | ❌ | "agent" (padrão) ou "system". Afeta a exibição no histórico. |
sender_name | string | ❌ | Nome exibido como remetente. Máx. 120 caracteres. |
Exemplo de requisição
curl -X POST https://<seu-dominio>/api/external/messages/send \
-H "Authorization: Bearer sk_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": "uuid-da-conversa",
"text": "Olá! Em que posso ajudar?",
"sender_type": "agent",
"sender_name": "Bot Atendimento"
}'
Resposta de sucesso — 200 OK
{
"success": true,
"message_id": "uuid-da-mensagem",
"conversation_id": "uuid-da-conversa",
"workspace_id": "uuid-do-workspace",
"sent_at": "2026-04-23T10:00:00.000Z"
}
Erros possíveis
| Status | error | O que fazer |
|---|---|---|
400 | "Parâmetros obrigatórios: conversation_id, text" | Verifique os campos. |
400 | "sender_type deve ser \"agent\" ou \"system\"" | Use "agent" ou "system". |
400 | "Integração não está conectada" | Reconecte a integração no painel. |
401 | "Authorization header ausente" | Adicione o header Authorization. |
401 | "API Key inválida ou não encontrada" | Verifique a API Key. |
403 | "Workspace inativo" | Workspace suspenso. |
404 | "Conversa não encontrada ou não pertence a este workspace" | O conversation_id não existe ou pertence a outro workspace. |
502 | "Erro ao enviar via WhatsApp" / "Erro ao enviar via WhatsApp Lite" / "Erro ao enviar via Telegram" | Falha no provedor. Tente novamente. |
4. PATCH /api/v1/leads/{id}
Move um contato para outra etapa de um funil (Kanban) sem passar pelo agente de IA. Pensado para integrações externas (n8n, Make) que já decidem o roteamento sozinhas e não devem depender de um agente de IA do Tivar rodando na mesma conversa — evita dois sistemas respondendo o mesmo lead.
Diferença em relação a mover pelo agente de IA: o agente de IA (
move_funnel_stage) analisa a conversa e decide sozinho a etapa. Este endpoint espera que você já saibapipeline_idestagee só quer aplicar a mudança.
Headers
| Header | Valor |
|---|---|
Authorization | Bearer <workspace_api_key> |
Content-Type | application/json |
Parâmetro de URL
| Parâmetro | Tipo | Descrição |
|---|---|---|
id | string (UUID) | ID do contato no Tivar. |
Body
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
pipeline_id | string (UUID) | ✅ | ID do funil. Precisa pertencer ao mesmo workspace da API Key. |
stage | string | ✅ | Chave da etapa dentro do funil (a mesma usada em pipelines.stages_config, ex.: "Aquecendo", "Novo Lead"). |
conversation_id | string (UUID) | ❌ | Se informado, registra um evento interno (type: system) nessa conversa — visível só pra equipe no Tivar, nunca é enviado ao lead. Sem ele, o contato é movido sem deixar rastro na conversa. |
Exemplo de requisição
curl -X PATCH https://<seu-dominio>/api/v1/leads/uuid-do-contato \
-H "Authorization: Bearer sk_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"pipeline_id": "uuid-do-funil",
"stage": "Qualificado",
"conversation_id": "uuid-da-conversa"
}'
Resposta de sucesso — 200 OK
{
"success": true,
"contact_id": "uuid-do-contato",
"pipeline_id": "uuid-do-funil",
"stage": "Qualificado"
}
Erros possíveis
| Status | error | O que fazer |
|---|---|---|
400 | Erro de validação do body (pipeline_id/stage ausentes ou inválidos) | Verifique os campos obrigatórios. |
400 | "pipeline_id não encontrado neste workspace" | Confirme que o funil pertence ao workspace da API Key usada. |
400 | "stage \"X\" não existe no pipeline \"Y\"" | Use a chave exata da etapa (mesma de stages_config, não o nome de exibição). |
400 | "conversation_id não pertence a este contato/workspace" | Omita conversation_id ou confirme que ela é do mesmo contato. |
401 | "API Key inválida" | Verifique a API Key. |
404 | "Contato não encontrado" | O id na URL não existe ou pertence a outro workspace. |
429 | "Muitas requisições. Limite: 60/min por workspace." | Aguarde e reenvie. |
Webhooks Outbound
O Tivar dispara eventos para URLs externas configuradas no painel, permitindo integração com n8n, Typebot, Zapier e outros.
Não confundir com o webhook de um agente de IA. Se você já tem um agente externo (aba "Webhook" dentro do agente) respondendo pelas conversas, não cadastre a mesma URL aqui também — os dois disparam de forma independente, e a mesma mensagem chegaria 2x no seu sistema. Use o webhook genérico só pra eventos que não são sobre "responder o cliente" (log de mensagens, integrações de CRM/BI, notificações internas).
Como configurar
- Acesse Configurações → Webhooks.
- Clique em Novo webhook.
- Informe a URL de destino (deve aceitar
POSTcom JSON). - Selecione os eventos desejados.
- Recomendado: informe um segredo para validar a autenticidade dos payloads via HMAC.
Por que configurar o segredo. A URL do seu webhook é um endpoint aberto na internet: sem segredo, qualquer pessoa que descubra esse endereço pode enviar um
POSTfingindo ser o Tivar, e seu fluxo não tem como diferenciar. Dependendo do que ele faz — criar pedido, disparar cobrança, responder cliente — o estrago é real. Com o segredo configurado, todo payload chega assinado e você rejeita o que não bater.
Headers enviados em todo webhook
| Header | Descrição |
|---|---|
Content-Type | application/json |
X-Webhook-Event | Nome do evento (ex.: message.received) |
X-Webhook-Timestamp | Unix timestamp (segundos) do envio |
X-Webhook-Signature | sha256=<hmac-hex> — presente apenas se um segredo estiver configurado |
User-Agent | Webhooks/1.0 |
Compatibilidade. Cada um desses três também é enviado com o prefixo antigo —
X-Tivar-Event,X-Tivar-TimestampeX-Tivar-Signature— com exatamente o mesmo valor. Integrações existentes continuam funcionando sem alteração; em integrações novas, prefira os nomesX-Webhook-*.
Validando a assinatura
Com o segredo configurado, valide assim — e rejeite a requisição quando a assinatura não bater ou o header não vier:
const crypto = require('crypto')
function isValidSignature(body, secret, signatureHeader) {
// Header ausente = requisição não confiável. Nunca deixe passar.
if (!signatureHeader) return false
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(signatureHeader)
// timingSafeEqual lança exceção se os buffers tiverem tamanhos diferentes —
// compare o tamanho antes, senão uma assinatura torta derruba seu endpoint.
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
body = string JSON raw do payload recebido (antes de parsear).
signatureHeader = valor do header X-Webhook-Signature.
n8n: cuidado com "raw body". O node Webhook do n8n já entrega o payload PARSEADO (objeto JS) pro node seguinte — se um Code node faz
JSON.stringify()nesse objeto pra calcular o HMAC, a string quase nunca bate byte a byte com a que o Tivar assinou (ordem de chave, espaçamento, escape) e a assinatura nunca vai validar, mesmo com o segredo certo. Achado real (parceiro white label, 2026-08-24). Fix: no node Webhook, aba Options → Raw Body, ative — aí o Code node recebe a string exata ($input.item.binary.dataou o campo que o n8n expõe pra raw body, dependendo da versão) pra assinar/comparar antes de qualquer parse.
Eventos disponíveis
| Evento | Quando dispara |
|---|---|
message.received | Nova mensagem recebida de um contato |
message.sent | Mensagem enviada pelo Tivar (agente, sistema ou API) |
conversation.assigned | Conversa atribuída ou reatribuída (humano ou agente de IA) |
conversation.resolved | Conversa marcada como resolvida |
Ao configurar o webhook você pode selecionar eventos individuais ou * para receber todos.
message.received
Disparado quando uma nova mensagem chega de um contato externo.
{
"event": "message.received",
"workspace_id": "uuid-do-workspace",
"conversation_id": "uuid-da-conversa",
"contact": {
"id": "uuid-do-contato",
"phone": "5511999998888",
"name": "João Silva"
},
"message": {
"id": "uuid-da-mensagem",
"text": "Oi, quero saber sobre meu pedido.",
"type": "text"
},
"status": {
"assigned_to_type": "agent",
"assigned_to_id": "uuid-do-agente"
},
"integration": {
"id": "uuid-da-integracao",
"provider": "whatsapp-official"
}
}
| Campo | Tipo | Descrição |
|---|---|---|
contact.id | string | null | UUID do contato no Tivar |
contact.phone | string | null | Número normalizado |
contact.name | string | null | Nome do contato |
message.type | string | text, image, audio, video, document, location, etc. |
status.assigned_to_type | "human" | "agent" | null | Tipo do responsável pela conversa |
status.assigned_to_id | string | null | UUID do agente/membro responsável |
integration.provider | string | whatsapp-official, whatsapp, telegram, instagram, messenger, livechat, mercado_livre |
message.sent
Disparado quando o Tivar envia uma mensagem para um contato (via painel, API ou agente de IA).
{
"event": "message.sent",
"workspace_id": "uuid-do-workspace",
"conversation_id": "uuid-da-conversa",
"message": {
"id": "uuid-da-mensagem",
"text": "Seu pedido foi confirmado!",
"type": "text"
},
"status": {
"sender_type": "human"
},
"integration": {
"id": "uuid-da-integracao",
"provider": "whatsapp-official"
}
}
conversation.assigned
Disparado quando uma conversa é atribuída ou reatribuída a um membro humano ou agente de IA. Também disparado ao remover a atribuição (assigned_to_id: null).
{
"event": "conversation.assigned",
"workspace_id": "uuid-do-workspace",
"conversation_id": "uuid-da-conversa",
"status": {
"assigned_to_type": "agent",
"assigned_to_id": "uuid-do-agente"
}
}
assigned_to_type | Cenário |
|---|---|
"human" | Atribuída a um membro da equipe |
"agent" | Atribuída a um agente de IA |
null | Atribuição removida |
conversation.resolved
Disparado quando uma conversa é marcada como resolvida.
{
"event": "conversation.resolved",
"workspace_id": "uuid-do-workspace",
"conversation_id": "uuid-da-conversa",
"status": {
"value": "resolved"
}
}
Fluxo recomendado para automações (n8n / Typebot)
1. Configure um webhook "message.received" no Tivar apontando para seu fluxo n8n.
2. O n8n recebe o payload e processa a mensagem (contact.phone, message.text).
3. Para responder na mesma conversa, use POST /api/external/messages/send
com o conversation_id recebido no payload.
4. Para iniciar uma conversa nova (disparo ativo), use POST /api/v1/send-message.
5. Para disparar fora da janela 24h do WhatsApp Oficial, use POST /api/v1/send-template.
Resumo rápido
| Caso de uso | Endpoint |
|---|---|
| Enviar mensagem de texto para um número | POST /api/v1/send-message |
| Enviar template HSM (fora da janela 24h) | POST /api/v1/send-template |
| Responder numa conversa existente | POST /api/external/messages/send |
| Mover contato de etapa no funil (sem agente de IA) | PATCH /api/v1/leads/{id} |
| Receber eventos em tempo real | Webhooks outbound |