API v1

ZapYou API
Documentação

Referência completa para integrar mensagens do WhatsApp Business em suas aplicações. Construa ferramentas poderosas de automação e engajamento.

66
Endpoints
14
Webhooks
RESTful
API Design

Introdução

Bem-vindo à API do ZapYou

A API do ZapYou permite que você integre o poder do WhatsApp Business em suas aplicações. Envie mensagens, gerencie instâncias, consulte históricos e muito mais, tudo através de uma API RESTful simples e poderosa.

66 Endpoints
Cobertura completa de funcionalidades
14 Webhooks
Receba eventos em tempo real
RESTful
Padrões modernos e consistentes

URL Base

https://server.zapyou.com/v1/api/

Formato de Resposta

Todas as respostas são JSON, mas não há um envelope único. O formato depende do grupo do endpoint — não escreva um cliente que assuma success ou data em toda resposta.

// Campanhas, tickets e lookups: envelope { data } (+ pagination nas listagens)
{ "data": [ ... ], "pagination": { "page": 1, "limit": 50, "total": 120, "hasMore": true } }

// Instancias, mensagens, templates e AI agents: objeto cru, sem envelope
{ "id": "507f1f77bcf86cd799439011" }

// Erros: { message } na maior parte da API; { error, details } na validacao Zod
{ "message": "Instance not found" }
{ "error": "INVALID_FIELDS", "details": [ { "path": ["name"], "code": "invalid_type" } ] }

Duas exceções valem atenção: GET /instances devolve um objeto indexado por posição ("0", "1"...) em vez de um array, e GET /messages devolve as mensagens agrupadas por instância, não em lista plana.

Autenticação

Company Member Token

Gerenciamento

Cobre tudo exceto o envio de mensagens. Escopo: a empresa inteira, com cada endpoint checando ainda uma permissão do grupo do membro:

  • Instâncias e webhooks
  • Consulta de mensagens
  • Campanhas e templates Meta
  • Tickets (atendimento)
Onde obter (são dois valores):
Configurações → aba API → Member ID + Token

Instance Token

Envio de Mensagens

Usado somente nos seis endpoints de envio. Cada instância tem seu próprio token, comparado literalmente — sem middleware e sem checagem de permissão:

  • Enviar texto
  • Enviar imagem
  • Enviar vídeo/áudio
  • Enviar documento e template
Onde obter:
Conexões → [Conexão] → Chave
Na mesma tela, o campo UUID é o :instanceId das rotas de envio.

AI Agent Token

Integração Externa

Usado para integração externa com AI Agents. Cada agente possui seus tokens:

  • Conversar com o agente
  • Gerenciar sessões
  • Obter histórico
  • Consultar informações
Onde obter:
AI Agents → [Agente] → Tokens → Criar

Como Usar

# Company Member Token — dois cabecalhos separados, sem "Bearer".
# Usado em: instancias, templates, mensagens, campanhas, tickets, lookups.
member: <ID do membro, aba API>
token:  <token gerado na aba API: zpy_...>

# Instance Token — valor CRU no Authorization, sem "Bearer".
# Usado apenas nos seis POST /instances/send/*.
Authorization: <Instance.token>

# AI Agent Token — este sim usa Bearer (64 caracteres hexadecimais).
# Usado apenas em /aiagents/*.
Authorization: Bearer <ai-agent-token>

Boas Práticas para AI Agent Token

Defina expiração adequada: Configure uma data de expiração alinhada com seu caso de uso
Armazene com segurança: Use variáveis de ambiente, nunca hardcode o token no código
Um token por aplicação: Crie tokens separados para cada integração
Monitore o uso: Acompanhe as estatísticas de cada token no painel
Revogue quando necessário: Se um token for comprometido, expire-o imediatamente
Trate erros 402: Implemente tratamento para saldo de IA insuficiente

Referência da API

Explore todos os endpoints disponíveis na API do ZapYou, organizados por categoria.

Gerenciamento de Instâncias

6 endpoints

GET/instances

Listar Instâncias

Retorna todas as instâncias da empresa do token. ATENÇÃO ao formato: a resposta NÃO é um array nem tem envelope — o backend serializa a lista como objeto indexado por posição ("0", "1", "2"...). Use Object.values(resposta) para iterar. Cada item é o registro completo da instância (inclusive o campo token, que é o Instance Token usado nos endpoints de envio), com businessAccountId achatado na raiz (null quando a instância não é meta_cloud). Autenticação por Company Member Token (cabeçalhos member + token, sem "Bearer") — exige a permissão instance:list. Em stats, sended e received são as mensagens enviadas e recebidas, files a quantidade de arquivos e storage o tamanho desses arquivos em KB; os valores são atualizados cerca de uma vez por hora.

POST/instances/poweron/:instanceId

Ligar Instância

Liga uma instância desligada. Antes de ligar, o backend valida a assinatura: instância sem subscription ACTIVE/TRIALING, com período vencido ou em trial expirado recebe 402 com um code específico. Autenticação por Company Member Token (cabeçalhos member + token, sem "Bearer") — exige a permissão instance:start.

POST/instances/poweroff/:instanceId

Desligar Instância

Desliga uma instância ligada (grava on=false, connected=false e limpa poweredOnAt). A sessão do WhatsApp é preservada — para encerrar a sessão use o endpoint de desconexão. Autenticação por Company Member Token (cabeçalhos member + token, sem "Bearer") — exige a permissão instance:stop.

POST/instances/disconnect/:instanceId

Desconectar Instância

Encerra a sessão do WhatsApp (logout). Em instâncias Baileys o auth_state local é apagado, então a próxima conexão exige novo QR Code. A desconexão é assíncrona: o 200 confirma que o evento foi disparado, não que já concluiu — acompanhe pelo webhook instance:disconnected. Autenticação por Company Member Token (cabeçalhos member + token, sem "Bearer") — exige a permissão instance:stop.

PUT/instances/update/:instanceId

Atualizar Nome da Instância

Atualiza o nome da instância. Único campo editável por este endpoint. Autenticação por Company Member Token (cabeçalhos member + token, sem "Bearer") — exige a permissão instance:update. Este endpoint não passa por rate limit.

PUT/instances/webhooks/:instanceId

Atualizar URLs de Webhook

Define as URLs de webhook da instância. Chave omitida do corpo permanece inalterada; para limpar uma URL, envie-a como string vazia (""). Cada URL preenchida passa por validação anti-SSRF (protocolo, porta, domínio e faixas de IP privadas/reservadas) antes de ser salva; uma URL reprovada devolve 400 e nenhuma das cinco é gravada. Autenticação por Company Member Token (cabeçalhos member + token, sem "Bearer") — exige a permissão instance:webhooks. Este endpoint não passa por rate limit.

Envio de Mensagens

5 endpoints

POST/instances/send/text/:instanceId

Enviar Mensagem de Texto

Envia uma mensagem de texto para um contato do WhatsApp.

POST/instances/send/image/:instanceId

Enviar Imagem

Envia uma imagem com legenda opcional para um contato do WhatsApp.

POST/instances/send/video/:instanceId

Enviar Vídeo

Envia um vídeo para um contato do WhatsApp.

POST/instances/send/audio/:instanceId

Enviar Áudio

Envia um áudio ou nota de voz (PTT) para um contato do WhatsApp.

POST/instances/send/document/:instanceId

Enviar Documento

Envia um documento (PDF, DOC, etc) para um contato do WhatsApp.

Consulta de Mensagens

3 endpoints

GET/messages

Listar Mensagens

Lista as mensagens da empresa com paginação por cursor (ObjectId decrescente, da mais recente para a mais antiga). O resultado NÃO é uma lista plana: as mensagens vêm agrupadas por instância, junto de um resumo em stats e do eco dos filtros aplicados. Respostas são cacheadas no Redis por 5 minutos por combinação de filtros — use POST /messages/clear-cache para invalidar. Autenticação por Company Member Token (cabeçalhos member + token, sem "Bearer") — exige a permissão message:show.

GET/messages/:phone

Buscar Mensagens por Telefone

Retorna as mensagens trocadas com um número, agrupadas por instância. Diferente de GET /messages, aqui cada mensagem vem com TODOS os campos do registro e o arquivo completo (include: file) — resposta mais pesada. Não há cache. Toda instância da empresa aparece no array, inclusive as sem mensagens para esse número (com messages vazio). Autenticação por Company Member Token (cabeçalhos member + token, sem "Bearer") — exige a permissão message:show.

POST/messages/clear-cache

Limpar Cache de Mensagens

Invalida TODO o cache Redis de GET /messages da empresa do token (todas as combinações de filtro). Não recebe corpo — enviar um é inofensivo, mas ignorado; não há como limpar o cache de uma instância específica. Desde o backend 2.89.3, este endpoint exige a permissão message:show (sem ela, responde 403) e não passa por rate limit.

Campanhas

38 endpoints

GET/campaigns

Listar Campanhas

Lista as campanhas ativas da empresa (as desativadas via DELETE /campaigns/:campaignId nunca aparecem), da mais recente para a mais antiga, com paginação por página. Cada item é um resumo — para o objeto completo com contagens, use GET /campaigns/:campaignId. Autenticação por Company Member Token (cabeçalhos member + token, sem "Bearer") — exige a permissão campaign:list.

GET/departments

Listar Departamentos

Lookup dos departamentos ativos da empresa, para preencher o campo departmentId ao configurar o roteamento de respostas de campanha (PUT /campaigns/:campaignId/reply-routing). Devolve apenas id e nome, sem paginação. Autenticação por Company Member Token (cabeçalhos member + token, sem "Bearer") — exige a permissão campaign:show.

GET/members

Listar Atendentes

Lookup dos membros da empresa, para preencher o campo memberId ao rotear respostas de campanha para um atendente específico. O id devolvido é o User.id (não o CompanyMember.id). Sem paginação e sem filtro por status. Autenticação por Company Member Token (cabeçalhos member + token, sem "Bearer") — exige a permissão campaign:show.

POST/campaigns

Criar Campanha

Cria uma nova campanha. Use type "default" para campanhas comuns (texto/mídia) ou "meta_template" para campanhas baseadas em templates Meta aprovados.

GET/campaigns/:campaignId

Obter Campanha

Retorna os detalhes completos de uma campanha pelo seu ID.

GET/campaigns/scheduled

Listar Campanhas Agendadas

Retorna as campanhas com agendamento ativo (status "scheduled").

PUT/campaigns/:campaignId/name

Renomear Campanha

Altera o nome de uma campanha existente.

PUT/campaigns/:campaignId/interval

Alterar Intervalo de Envio

Define o intervalo (delay) em segundos entre cada disparo da campanha.

POST/campaigns/:campaignId/duplicate

Duplicar Campanha

Cria uma cópia da campanha. Use duplicateType para escolher o que copiar: "full" (tudo), "messages-only" (apenas mensagens) ou "payloads-only" (apenas destinatários).

DELETE/campaigns/:campaignId

Desativar Campanha

Desativa (remove) uma campanha pelo seu ID.

GET/campaigns/:campaignId/messages/models

Listar Modelos de Mensagem

Retorna os modelos de mensagem (texto/mídia) configurados na campanha, ordenados por "order".

POST/campaigns/:campaignId/messages/models

Adicionar Modelo de Mensagem

Adiciona um modelo de mensagem à campanha. Pode ser um modelo comum (texto/mídia, com type + options) ou o anexo de um template Meta aprovado (type "template" + templateId). Campanhas meta_template aceitam apenas 1 template.

PUT/campaigns/:campaignId/messages/models/:messageId

Atualizar Modelo de Mensagem

Atualiza um modelo de mensagem existente. Usa o mesmo schema do create (type + options obrigatórios).

PUT/campaigns/:campaignId/messages/models/:messageId/order

Reordenar Modelo de Mensagem

Altera a posição (order) de um modelo de mensagem na sequência de envio.

DELETE/campaigns/:campaignId/messages/models/:messageId

Remover Modelo de Mensagem

Remove um modelo de mensagem da campanha.

POST/campaigns/:campaignId/template/preview

Pré-visualizar Template

Renderiza o template Meta da campanha usando os campos de um destinatário existente, identificado pelo índice (payloadIndex).

GET/campaigns/:campaignId/template/variables

Listar Variáveis do Template

Retorna as variáveis declaradas pelo template Meta anexado à campanha, aninhadas por componente (header/body/buttons), além do mapeamento atual (currentMapping), dos campos disponíveis dos destinatários (availableFields) e da mídia de header (headerMediaType, staticHeaderMedia). O objeto `variables` segue o mesmo shape esperado pelo `variableMapping` em PUT /template/mapping.

PUT/campaigns/:campaignId/template/mapping

Mapear Variáveis do Template

Define o mapeamento entre as variáveis do template Meta e os campos dos destinatários. O `variableMapping` é aninhado por componente (header/body/buttons); cada sub-objeto é um Record<string,string> (índice da variável → nome da coluna). Botões usam a chave "índiceBotão_índiceVar" (ex.: "0_1") e o header de mídia dinâmica usa a chave "media".

POST/campaigns/:campaignId/media

Upload de Mídia da Campanha

Envia um arquivo e devolve uma URL pública utilizável no header de mídia do template (`staticHeaderMedia`) ou em uma ação de botão (`buttonActions[].url`). O upload **não** grava configuração: persista a URL você mesmo, via `PUT /campaigns/:campaignId/template/mapping` ou `PUT /campaigns/:campaignId/messages/models/:messageId`. Autenticação por Company Member Token (cabeçalhos member + token, sem "Bearer"). A resposta vem **fora** do envelope `{ data }` usado no restante da API pública. A URL gerada é pública — é o servidor da Meta que baixa o arquivo a cada disparo.

GET/campaigns/:campaignId/payloads

Listar Destinatários

Retorna os destinatários (payloads) da campanha de forma paginada. Cada payload contém telefone e campos personalizados.

POST/campaigns/:campaignId/payloads

Adicionar Destinatários

Adiciona destinatários à campanha em lote. Cada item é um objeto com o telefone (campo definido por phoneKey, default "phone") e campos personalizados.

POST/campaigns/:campaignId/payloads/import-url

Importar Destinatários por URL

Importa destinatários a partir de um arquivo acessível por URL (ex.: CSV). O backend baixa o arquivo, faz o parse e adiciona os destinatários à campanha.

GET/campaigns/:campaignId/contacts

Listar Contatos

Retorna os contatos da campanha de forma paginada (telefone + status ativo).

POST/campaigns/:campaignId/instances

Definir Instâncias da Campanha

Define as instâncias (conexões WhatsApp) usadas para enviar a campanha. Substitui a lista atual.

POST/campaigns/:campaignId/testing

Testar Campanha

Envia a sequência de mensagens da campanha para um número de teste, sem afetar a lista real de destinatários.

POST/campaigns/:campaignId/start

Iniciar Campanha

Inicia o disparo da campanha. Envie o header Idempotency-Key (UUID único por clique) para evitar inícios duplicados.

POST/campaigns/:campaignId/pause

Pausar Campanha

Pausa uma campanha em andamento.

POST/campaigns/:campaignId/resume

Retomar Campanha

Retoma uma campanha que estava pausada.

POST/campaigns/:campaignId/schedule/start

Agendar Início

Agenda o início automático da campanha para uma data/hora (ISO 8601).

POST/campaigns/:campaignId/schedule/pause

Agendar Pausa

Agenda a pausa automática da campanha para uma data/hora (ISO 8601).

POST/campaigns/:campaignId/schedule/resume

Agendar Retomada

Agenda a retomada automática da campanha para uma data/hora (ISO 8601).

PUT/campaigns/:campaignId/schedule

Atualizar Agendamento Completo

Define em uma única chamada todas as janelas de agendamento (início, pausa, retomada) e o auto-agendamento.

DELETE/campaigns/:campaignId/schedule

Cancelar Agendamento

Remove todas as janelas de agendamento da campanha, desativando o auto-agendamento.

PUT/campaigns/:campaignId/reply-routing

Configurar Roteamento de Respostas

Define para onde as respostas dos contatos são roteadas: fila ("queue"), departamento ("department") ou atendente ("member"). Envie replyRouting: null para desabilitar.

PUT/campaigns/:campaignId/reply-routing

Desabilitar Roteamento de Respostas

Desabilita o roteamento de respostas da campanha. Usa o mesmo endpoint do reply-routing, enviando replyRouting: null no corpo.

GET/campaigns/:campaignId/submissions

Listar Envios

Retorna os envios (submissions) da campanha de forma paginada, com status individual de cada disparo. Pode filtrar por status.

GET/campaigns/:campaignId/metrics

Obter Métricas da Campanha

Retorna as métricas consolidadas da campanha. Desde a v2.49.0 as contagens são por CONTATO (antes eram por mensagem): os baldes pending, sent, delivered, read e failed somam sempre o total. Inclui o detalhamento das falhas por motivo e, em campanhas de template, os cliques por botão.

GET/campaigns/:campaignId/metrics/recipients

Listar Destinatários com Status

Lista os contatos da campanha com o status de entrega de cada um, o motivo da falha quando houver e os botões que o contato clicou. Permite filtrar por status, por motivo da falha e por botão — é o caminho para responder "quem ainda não recebeu", "quem falhou por qual motivo" e "quem clicou em qual botão".

Meta Templates

4 endpoints

Renderizando o preview do template com components

Estrutura crua do template

components carrega a estrutura crua do template — header, body, footer e botões, com os exemplos de preenchimento vindos da Meta. É o que permite mostrar ao usuário como a mensagem vai ficar antes de disparar. Ele vem tanto na listagem quanto no detalhe.

  • Percorra o array por type: HEADER, BODY, FOOTER e BUTTONS. A ordem no array é a ordem de exibição.
  • No HEADER, use format para saber se é texto (TEXT) ou mídia (IMAGE, VIDEO, DOCUMENT). Esse mesmo format define o type do headerMedia no envio.
  • Use example para pré-preencher a visualização com valores realistas — o BODY traz example.body_text[0], um array na ordem das variáveis.
// Substitui {{1}}, {{2}}... pelos valores informados
const fill = (text, values) =>
  text.replace(/\{\{(\d+)\}\}/g, (_, i) => values[i] ?? '{{' + i + '}}');

function renderPreview(template, values = {}) {
  const preview = { header: null, body: '', footer: '', buttons: [] };

  for (const c of template.components) {
    switch (c.type) {
      case 'HEADER':
        // format diz se o header e texto ou midia
        preview.header =
          c.format === 'TEXT'
            ? { kind: 'text', value: fill(c.text, values) }
            : { kind: 'media', mediaType: c.format }; // IMAGE | VIDEO | DOCUMENT
        break;
      case 'BODY':
        preview.body = fill(c.text, values);
        break;
      case 'FOOTER':
        preview.footer = c.text; // footer nao aceita variavel
        break;
      case 'BUTTONS':
        preview.buttons = c.buttons.map((b) => ({ type: b.type, text: b.text }));
        break;
    }
  }

  return preview;
}

// Pre-preenchendo com os exemplos que a Meta devolve:
const body = template.components.find((c) => c.type === 'BODY');
const sample = body?.example?.body_text?.[0] ?? []; // ["Joao", "12345"]
const values = Object.fromEntries(sample.map((v, i) => [String(i + 1), v]));

renderPreview(template, values);
GET/templates

Listar Templates WABA

Lista os templates Meta Cloud da empresa, com filtros e paginação. Cada item traz components (a estrutura crua do template — header, body, footer e botões, com os exemplos vindos da Meta), que é o que permite renderizar um preview antes de disparar. Atenção: o campo variables lista APENAS as variáveis do BODY — variáveis de header ou de URL de botão não aparecem aqui; para montar o formulário de envio, use GET /templates/:id (expectedVariables). Autenticação por Company Member Token (cabeçalhos member + token, sem "Bearer") — exige permissão instance:list.

GET/templates/:id

Detalhe de Template

Retorna um template específico — os mesmos campos de um item da lista (inclusive components, plano na raiz, sem o envelope data), acrescidos de expectedVariables. Este é o ÚNICO endpoint que expõe expectedVariables, o contrato completo de variáveis (header, body, botões e total). Liste com GET /templates, mas monte o formulário de envio com GET /templates/:id: quem deriva o formulário do campo variables da listagem perde as variáveis de header e de botão silenciosamente e recebe 422 MISSING_VARIABLES no envio. Autenticação por Company Member Token (cabeçalhos member + token) — exige permissão instance:list.

GET/instances/:instanceId

Detalhe de Instância

Retorna os dados públicos (seguros) de uma instância. Nunca expõe access token, segredos ou credenciais. Autenticação por Company Member Token (cabeçalhos member + token) — exige permissão instance:show.

POST/instances/send/template/:instanceId

Enviar Mensagem via Template

Envia uma mensagem usando um template Meta Cloud aprovado. Requer instância meta_cloud ligada e conectada. A resposta é SÍNCRONA: o backend aguarda o resultado real do envio (ou 30s de timeout) antes de responder — não há polling a fazer. Autenticação por Instance Token: cabeçalho Authorization com o valor CRU do token (sem "Bearer") — enviar "Bearer <token>" causa 401.

AI Agents

5 endpoints

AI Agent Token

Integração Externa

Tokens exclusivos para cada AI Agent, permitindo acesso programático sem autenticação de usuário. Crie chatbots inteligentes, assistentes virtuais e automações.

Onde obter:
AI Agents → [Agente] → Aba Tokens → Criar Token
POST/aiagents/chat

Chat com AI Agent

Envia uma mensagem para o AI Agent e recebe a resposta. Usa o contexto da sessão para manter a conversa.

GET/aiagents/agent

Informações do Agente

Retorna informações básicas do AI Agent associado ao token.

GET/aiagents/widget/config

Configuração do Widget

Devolve os dados públicos usados pela página hospedada do widget de chat (https://zapyou.com/widget/SEU_TOKEN). Só responde 200 se o widget estiver habilitado para ESTE token — habilite em AI Agents → [Agente] → Tokens. O script carregador do widget é servido pela própria API, em https://server.zapyou.com/v1/widget/loader.js.

GET/aiagents/sessions/:sessionId

Buscar Histórico da Sessão

Retorna o histórico de uma sessão. As sessões vivem no Redis com TTL de 24 horas a partir da última mensagem e guardam no máximo as 50 mensagens mais recentes (janela deslizante) — este endpoint devolve até 50. Sessão inexistente ou expirada NÃO é erro: responde 200 com messages vazio e totalMessages 0. O escopo é o token: uma sessão criada por outro token não é visível aqui.

DELETE/aiagents/sessions/:sessionId

Limpar Sessão

Apaga o histórico da sessão no Redis, permitindo recomeçar a conversa com o mesmo ID. Devolve 404 quando não havia nada para apagar (sessão inexistente ou já expirada).

Tickets (Atendimento)

5 endpoints

API para montar dashboards externos de atendimento e mover tickets: listar tickets com filtros e paginação, ver o detalhe de um ticket, o histórico de movimentação, estatísticas de mensagens e transferir um ticket para a fila, um departamento ou um atendente. O escopo é a empresa inteira do token — diferente da tela interna, não há recorte por canais do membro nem por regras de fila.

GET/tickets

Listar Tickets

Lista os tickets (atendimentos) da empresa com filtros ricos e paginação por página. Autenticação por Company Member Token (cabeçalhos member + token, sem "Bearer") — exige a permissão attendance:reports:view.

GET/tickets/:ticketId

Detalhe de um Ticket

Retorna um ticket específico. Mesmo objeto da listagem, sem messageCount, acrescido de customFields, unreadCount, transferredFromUserId, transferReason e um contato mais completo. Autenticação por Company Member Token (cabeçalhos member + token) — exige attendance:reports:view.

GET/tickets/:ticketId/history

Histórico de Movimentação

Retorna o histórico de movimentação do ticket (mudanças de status, transferências, notas). Autenticação por Company Member Token (cabeçalhos member + token) — exige attendance:reports:view.

GET/tickets/:ticketId/stats

Estatísticas de Mensagens

Retorna estatísticas de mensagens do ticket: contagens por remetente/tipo/status de entrega e tempos. NUNCA inclui conteúdo de mensagem (texto ou mídia). Autenticação por Company Member Token (cabeçalhos member + token) — exige attendance:reports:view.

POST/tickets/:ticketId/transfer

Transferir Ticket

Move um ticket PENDING, OPEN ou AUTOMATION para a fila, um departamento ou um atendente. O target descreve onde o ticket deve TERMINAR: se ele já está lá, a resposta é 200 com transfer.changed=false e nada é gravado, então repetir a chamada é seguro. Um ticket AUTOMATION tem as execuções de workflow em andamento canceladas antes da transferência. Autenticação por Company Member Token (cabeçalhos member + token). A permissão depende do target: "attendant" exige attendance:tickets:assign-others, "department" exige attendance:tickets:transfer-department e "queue" aceita qualquer uma das duas. Conta no mesmo limite de 120 requisições/minuto dos endpoints de tickets.

Webhooks

O que são Webhooks?

Webhooks são notificações HTTP POST enviadas automaticamente para URLs configuradas quando eventos específicos ocorrem no ZapYou. Existem hoje duas famílias, configuradas em lugares diferentes e cobrindo assuntos diferentes — não confunda uma com a outra ao integrar:

Webhooks de instância
Configurados via PUT /instances/webhooks/:instanceId (API Pública). Cobrem a conexão com o WhatsApp: mensagens, status de entrega, conexão/desconexão da instância, clique em botão de template e chegada por anúncio. Toda URL passa por validação anti-SSRF: endereços privados, portas bloqueadas e protocolos fora de http/https são recusados com 400.
Webhooks de canal
Configurados na aba Webhook das configurações do canal de atendimento, no Painel de Controle — não há endpoint na API Pública para isso. Cobrem o ciclo de vida do ticket: criação, reabertura, atribuição, transferência, atualização, fechamento e mensagens. No máximo um webhook por canal.

Em ambas as famílias, toda requisição é POST com Content-Type: application/json. Os webhooks de instância usam o corpo { event, datetime, payload }; os webhooks de canal acrescentam um id de entrega ao envelope — { id, event, datetime, payload } — veja a seção própria abaixo.

Webhooks de Instância

messageURL

Disparado a cada mensagem recebida ou enviada — é o webhook de maior frequência. O campo event vale sempre "message", mas o payload difere conforme o provider da instância: instâncias Meta Cloud incluem provider: "meta_cloud" e um objeto file com mediaId/mimeType, enquanto instâncias Baileys não enviam provider e trazem mentions e registryType. Entrega: Baileys chama uma única vez, com timeout de 5s e sem retentativa; Meta Cloud usa timeout de 10s com até 3 retentativas em backoff exponencial. Uma falha na entrega não é reprocessada depois — trate o webhook como best-effort e reconcilie por GET /messages quando precisar de garantia.

Payload Fields

FieldTypeDescription
eventstringSempre "message"
datetimestringTimestamp da mensagem em ISO 8601
payload.providerstringPresente APENAS em instâncias Meta Cloud, com o valor "meta_cloud". Ausente em Baileys
payload.instancestringInstance.id (ObjectId de 24 caracteres)
payload.codestringInstance.code (UUID da instância)
payload.messagestringID da mensagem no WhatsApp (wamid em Meta Cloud)
payload.typestringTipo da mensagem: text, image, video, gif, audio, document, sticker, location, contact...
payload.fromMebooleantrue quando a mensagem saiu desta linha
payload.sourcestringSomente em ecos do Meta Cloud (fromMe: true): "whatsapp_app"
payload.remotePhonestringNúmero do contato remoto
payload.connectedPhonestringNúmero da instância conectada
payload.textstringTexto da mensagem ou legenda da mídia. String vazia quando não há texto
payload.fileobjectBaileys: { is } quando não há arquivo; { is, name, type, mimeType, size, url } quando há. Meta Cloud: { is, mediaId, mimeType, caption, fileName, url }
payload.groupobject{ is, id } — id é string vazia fora de grupo. Meta Cloud sempre envia { is: false, id: "" }
payload.forwardedobject{ is, score } — o padrão é { is: false, score: 0 }
payload.quotedobject{ is, id } — id é o stanzaId da mensagem citada; o padrão é { is: false, id: "" }
payload.mentionsarrayApenas em Baileys: JIDs mencionados na mensagem
payload.contactsarrayCartões de contato anexados
payload.localizationobjectObjeto vazio quando não há localização; { degreesLatitude, degreesLongitude, live } quando há
payload.statusstringBaileys: received, error, pending, server, delivery, read ou played. Meta Cloud: RECEIVED (entrada) ou SENT (eco)
payload.pushNamestringNome do contato no WhatsApp. Cai para "Unknown" quando indisponível
payload.createdAtstringTimestamp da mensagem em ISO 8601
payload.registryTypestringApenas em Baileys: tipo bruto do evento interno de origem

Example Payload

JSON
{
  "event": "message",
  "datetime": "2026-01-08T14:30:45.000Z",
  "payload": {
    "instance": "507f1f77bcf86cd799439011",
    "code": "9f8b4c21-5d3e-4a17-9c62-2b7e0f5a1d38",
    "message": "3EB0F7C8E2D4A3B1",
    "type": "text",
    "fromMe": false,
    "remotePhone": "551199999000",
    "connectedPhone": "551151183618",
    "text": "Olá, gostaria de informações sobre produtos",
    "file": {
      "is": false
    },
    "group": {
      "is": false,
      "id": ""
    },
    "forwarded": {
      "is": false,
      "score": 0
    },
    "quoted": {
      "is": false,
      "id": ""
    },
    "mentions": [],
    "contacts": [],
    "localization": {},
    "status": "received",
    "pushName": "João Silva",
    "createdAt": "2026-01-08T14:30:45.000Z",
    "registryType": "text"
  }
}
statusMessageURL

Disparado quando o status de entrega de uma mensagem muda. ATENÇÃO: o nome do evento e o formato do payload dependem do provider. Baileys envia event "ack" com um payload mínimo; Meta Cloud envia event "status" com os metadados da Meta (destinatário, conversa, tarifação e erro). Os valores de status também não coincidem entre os dois — não trate o campo como um enum único.

Payload Fields

FieldTypeDescription
eventstring"ack" em instâncias Baileys; "status" em instâncias Meta Cloud
datetimestringMomento do processamento do evento, em ISO 8601
payload.instancestringInstance.id (ObjectId de 24 caracteres)
payload.codestringInstance.code (UUID da instância)
payload.messagestringID da mensagem (wamid em Meta Cloud)
payload.statusstringBaileys: error, pending, server, delivery, read ou played. Meta Cloud: os valores da Meta (sent, delivered, read, failed)
payload.recipientstringSomente Meta Cloud: número do destinatário
payload.timestampnumberSomente Meta Cloud: timestamp Unix informado pela Meta
payload.conversationIdstringSomente Meta Cloud: ID da conversa tarifada
payload.pricingobjectSomente Meta Cloud: dados de tarifação da conversa
payload.errorobjectSomente Meta Cloud: detalhes do erro quando status é failed

Example Payload

JSON
{
  "event": "ack",
  "datetime": "2026-01-08T14:31:00.000Z",
  "payload": {
    "instance": "507f1f77bcf86cd799439011",
    "code": "9f8b4c21-5d3e-4a17-9c62-2b7e0f5a1d38",
    "message": "3EB0F7C8E2D4A3B1",
    "status": "read"
  }
}
connectedURL

Disparado quando a instância conclui a conexão com o WhatsApp. Evento de baixa frequência. O nome do evento é "instance:connected" — com o prefixo instance:, não apenas "connected". O payload traz somente a identificação da instância; para saber o número conectado, consulte GET /instances/:instanceId após receber o evento.

Payload Fields

FieldTypeDescription
eventstringSempre "instance:connected"
datetimestringMomento do evento em ISO 8601
payload.instancestringInstance.id (ObjectId de 24 caracteres)
payload.codestringInstance.code (UUID da instância)

Example Payload

JSON
{
  "event": "instance:connected",
  "datetime": "2026-01-08T14:30:00.000Z",
  "payload": {
    "instance": "507f1f77bcf86cd799439011",
    "code": "9f8b4c21-5d3e-4a17-9c62-2b7e0f5a1d38"
  }
}
disconnectedURL

Disparado quando a instância perde a conexão com o WhatsApp — por logout via API, sessão expirada ou queda de conexão. Evento de baixa frequência. O nome do evento é "instance:disconnected" e o payload NÃO informa o motivo da desconexão: traz apenas a identificação da instância.

Payload Fields

FieldTypeDescription
eventstringSempre "instance:disconnected"
datetimestringMomento do evento em ISO 8601
payload.instancestringInstance.id (ObjectId de 24 caracteres)
payload.codestringInstance.code (UUID da instância)

Example Payload

JSON
{
  "event": "instance:disconnected",
  "datetime": "2026-01-08T15:00:00.000Z",
  "payload": {
    "instance": "507f1f77bcf86cd799439011",
    "code": "9f8b4c21-5d3e-4a17-9c62-2b7e0f5a1d38"
  }
}
templateButtonURL

Disparado quando um contato toca um botão de RESPOSTA RÁPIDA em uma mensagem de template. Exclusivo de instâncias Meta Cloud — instâncias Baileys não enviam template, então a URL nunca produz evento nelas. ATENÇÃO ao limite da plataforma: botões de URL, telefone e copiar código NÃO geram evento algum, porque a Meta não reporta esses toques ao servidor; respostas de Flow também estão fora do escopo. Assinado, assim como o webhook de anúncio (adReferralURL): cada requisição traz o cabeçalho X-ZapYou-Signature no formato sha256=<hex>, um HMAC-SHA256 do corpo CRU (antes de qualquer parse) usando o token da instância como chave. Entrega: timeout de 10s e até 4 tentativas com backoff exponencial; assim como o webhook de anúncio (adReferralURL) e diferente dos demais, qualquer resposta com status maior ou igual a 400 conta como falha e é retentada — se o seu endpoint devolve 401 ao rejeitar uma assinatura, isso aparece como falha de entrega e não como sucesso. Use payload.click.messageId como chave de idempotência.

Payload Fields

FieldTypeDescription
eventstringSempre "template:button"
datetimestringMomento em que a ZapYou processou o clique, em ISO 8601
payload.providerstringSempre "meta_cloud"
payload.click.messageIdstringwamid da mensagem do clique — use como chave de idempotência
payload.click.clickedAtstringMomento do clique informado pela Meta, em ISO 8601
payload.connection.instancestringInstance.id (ObjectId de 24 caracteres)
payload.connection.codestringInstance.code (UUID da instância)
payload.connection.namestringNome da instância na ZapYou
payload.connection.connectedPhonestringNúmero da instância conectada. null quando não cadastrado
payload.connection.phoneNumberIdstringphone_number_id do número na Meta
payload.connection.businessAccountIdstringWABA ID da conta. null quando indisponível
payload.contact.idstringContact.id na ZapYou. null quando o contato não está cadastrado
payload.contact.phonestringTelefone do contato, normalizado
payload.contact.pushNamestringNome do contato no WhatsApp. null quando indisponível
payload.contact.namestringNome cadastrado na ZapYou. null quando o contato não está cadastrado
payload.contact.emailstringE-mail cadastrado. null quando ausente
payload.contact.profilePicUrlstringFoto de perfil. null quando ausente
payload.contact.tagsarrayTags do contato como { id, name, color }. Array vazio quando não há. Campos personalizados (customFields) NÃO são enviados
payload.template.messageIdstringwamid da mensagem de template original. Sempre presente
payload.template.sentAtstringQuando o template foi enviado, em ISO 8601. Presente sempre que o envio deixou registro — inclusive com resolved: false. NÃO use este campo para inferir se o template foi resolvido
payload.template.idstringMetaTemplate.id na ZapYou. null quando resolved é false
payload.template.metaIdstringID do template na Meta. null quando resolved é false
payload.template.namestringNome do template. null quando resolved é false
payload.template.languagestringIdioma do template (ex.: pt_BR). null quando resolved é false
payload.template.categorystringMARKETING, UTILITY ou AUTHENTICATION. null quando resolved é false
payload.template.resolvedbooleanÚNICO campo que indica se o template foi identificado. false quando o envio é anterior à v2.45.0 ou quando o registro não foi encontrado — nesse caso o evento é entregue mesmo assim, com os dados de clique, conexão, contato e botão
payload.button.typestringSempre "QUICK_REPLY" quando identificado. null quando resolved é false
payload.button.textstringRótulo do botão tocado, como veio da Meta. Sempre presente
payload.button.payloadstringPayload do botão. Em disparos de campanha vem como btn_<índice>; nos demais envios a Meta ecoa o próprio rótulo do botão
payload.button.indexnumberPosição do botão no template (base 0). null quando resolved é false ou quando o botão é ambíguo (dois botões com o mesmo rótulo)
payload.context.campaignobject{ id, name } quando o template saiu de uma campanha; null caso contrário. Sempre presente como chave
payload.context.ticketobject{ id, protocol } quando o template pertence a um atendimento; null caso contrário. Sempre presente como chave

Example Payload

JSON
{
  "event": "template:button",
  "datetime": "2026-09-03T14:22:10.000Z",
  "payload": {
    "provider": "meta_cloud",
    "click": {
      "messageId": "wamid.HBgNNTUxMTk5OTk5MDAwFQ",
      "clickedAt": "2026-09-03T14:22:09.000Z"
    },
    "connection": {
      "instance": "507f1f77bcf86cd799439011",
      "code": "9f8b4c21-5d3e-4a17-9c62-2b7e0f5a1d38",
      "name": "Comercial",
      "connectedPhone": "551151183618",
      "phoneNumberId": "123456789012345",
      "businessAccountId": "987654321098765"
    },
    "contact": {
      "id": "6512f8a1b2c3d4e5f6a7b8c9",
      "phone": "551199999000",
      "pushName": "João",
      "name": "João Silva",
      "email": "[email protected]",
      "profilePicUrl": "https://cdn.exemplo.com/joao.jpg",
      "tags": [
        {
          "id": "6512bb00000000000000bb01",
          "name": "Lead quente",
          "color": "#FF5722"
        }
      ]
    },
    "template": {
      "messageId": "wamid.HBgMNTUxMTk5OTk5MDAwFQ",
      "sentAt": "2026-09-03T09:00:00.000Z",
      "id": "6512aa00000000000000aa01",
      "metaId": "123456789012345",
      "name": "promo_setembro",
      "language": "pt_BR",
      "category": "MARKETING",
      "resolved": true
    },
    "button": {
      "type": "QUICK_REPLY",
      "text": "Quero saber mais",
      "payload": "btn_0",
      "index": 0
    },
    "context": {
      "campaign": {
        "id": "6512cc00000000000000cc01",
        "name": "Promo Setembro"
      },
      "ticket": null
    }
  }
}
adReferralURL

Disparado quando chega, em uma instância Meta Cloud, a primeira mensagem que uma pessoa envia depois de tocar em um anúncio Click-to-WhatsApp ou no botão de WhatsApp de uma publicação impulsionada. Exclusivo de instâncias Meta Cloud. A mensagem continua chegando normalmente no messageURL; relacione os dois eventos pelo wamid (payload.message.id aqui, payload.message no webhook de mensagem). O ad:referral costuma chegar ANTES do evento de mensagem da mesma mensagem — não dependa da ordem. Assinado como o templateButtonURL: cabeçalho X-ZapYou-Signature no formato sha256=<hex>, um HMAC-SHA256 do corpo CRU usando o token da instância como chave. Entrega: timeout de 10s e até 4 tentativas com backoff exponencial; qualquer resposta com status maior ou igual a 400 conta como falha e é retentada. Use payload.message.id como chave de idempotência.

Payload Fields

FieldTypeDescription
eventstringSempre "ad:referral"
datetimestringMomento em que a ZapYou processou a mensagem, em ISO 8601
payload.providerstringSempre "meta_cloud"
payload.message.idstringwamid da mensagem — use como chave de idempotência
payload.message.receivedAtstringMomento em que o contato enviou a mensagem, em ISO 8601
payload.message.typestringTipo da mensagem (text, image, video…)
payload.message.textstringTexto ou legenda da mensagem. null quando vazio
payload.referral.sourceTypestring"ad" (anúncio) ou "post" (publicação impulsionada). null quando a Meta não enviou
payload.referral.sourceIdstringID do anúncio ou da publicação. null quando a Meta não enviou
payload.referral.sourceUrlstringLink do anúncio ou da publicação. null quando ausente
payload.referral.headlinestringTítulo do anúncio. null quando ausente
payload.referral.bodystringTexto do anúncio. null quando ausente
payload.referral.mediaTypestring"image" ou "video". null quando ausente
payload.referral.imageUrlstringImagem do anúncio — link temporário da Meta, baixe logo se precisar guardar. null quando ausente
payload.referral.videoUrlstringVídeo do anúncio — link temporário da Meta. null quando ausente
payload.referral.thumbnailUrlstringMiniatura do anúncio — link temporário da Meta. null quando ausente
payload.referral.ctwaClidstringID do clique, usado pela API de Conversões da Meta para atribuir uma venda ao anúncio. null quando ausente
payload.referral.welcomeMessagestringMensagem de boas-vindas configurada no anúncio. null quando ausente
payload.connection.instancestringInstance.id (ObjectId de 24 caracteres)
payload.connection.codestringInstance.code (UUID da instância)
payload.connection.namestringNome da instância na ZapYou
payload.connection.connectedPhonestringNúmero da instância conectada. null quando não cadastrado
payload.connection.phoneNumberIdstringphone_number_id do número na Meta
payload.connection.businessAccountIdstringWABA ID da conta. null quando indisponível
payload.contact.idstringContact.id na ZapYou. Na primeira mensagem de uma pessoa o contato normalmente ainda não foi criado, então vem null — identifique pelo payload.contact.phone
payload.contact.phonestringTelefone do contato, normalizado
payload.contact.pushNamestringNome do contato no WhatsApp. null quando indisponível
payload.contact.namestringNome cadastrado na ZapYou. null quando o contato não está cadastrado
payload.contact.emailstringE-mail cadastrado. null quando ausente
payload.contact.profilePicUrlstringFoto de perfil. null quando ausente
payload.contact.tagsarrayTags do contato como { id, name, color }. Array vazio quando não há. Campos personalizados (customFields) NÃO são enviados

Example Payload

JSON
{
  "event": "ad:referral",
  "datetime": "2026-09-29T14:22:10.000Z",
  "payload": {
    "provider": "meta_cloud",
    "message": {
      "id": "wamid.HBgNNTUxMTk5OTk5MDAwFQ",
      "receivedAt": "2026-09-29T14:22:09.000Z",
      "type": "text",
      "text": "Olá! Quero saber mais sobre a promoção"
    },
    "referral": {
      "sourceType": "ad",
      "sourceId": "120210000000001",
      "sourceUrl": "https://fb.me/abc123",
      "headline": "Black Friday -50%",
      "body": "Fale com a gente pelo WhatsApp",
      "mediaType": "image",
      "imageUrl": "https://scontent.xx.fbcdn.net/v/image.jpg",
      "videoUrl": null,
      "thumbnailUrl": "https://scontent.xx.fbcdn.net/v/thumb.jpg",
      "ctwaClid": "ARAkLkA8rmlFeiCktEJQ",
      "welcomeMessage": null
    },
    "connection": {
      "instance": "507f1f77bcf86cd799439011",
      "code": "9f8b4c21-5d3e-4a17-9c62-2b7e0f5a1d38",
      "name": "Comercial",
      "connectedPhone": "551151183618",
      "phoneNumberId": "123456789012345",
      "businessAccountId": "987654321098765"
    },
    "contact": {
      "id": null,
      "phone": "551199999000",
      "pushName": "João",
      "name": null,
      "email": null,
      "profilePicUrl": null,
      "tags": []
    }
  }
}

Webhooks de Canal (Ciclo de Vida do Ticket)

Onde configurar: aba Webhook das configurações do canal de atendimento, ao lado de Roteamento — requer a permissão de edição das configurações do atendimento. Cada canal pode ter no máximo um webhook: uma URL (obrigatoriamente HTTPS), a lista de eventos aos quais ele está inscrito (dos 8 abaixo) e um estado ativo/inativo. Funciona igualmente em canais Baileys e Meta Cloud — é independente do provedor de WhatsApp por trás do canal.
ticket:created

Disparado quando um novo ticket é criado — manualmente, a partir de uma mensagem recebida, por uma automação de workflow, ou pelo atendente iniciando uma conversa (startConversation). Diferente dos seis webhooks acima, este é configurado por CANAL de atendimento (aba Webhook das configurações do canal, no Painel de Controle), não pela API Pública — não existe endpoint de configuração para ele. Todo evento chega assinado no header X-ZapYou-Signature (HMAC-SHA256 sobre o corpo cru), usando o segredo do próprio canal (o valor whsec_... mostrado no painel) como chave, não o token da instância. Mesmo quando o ticket já nasce atribuído (roteamento por membro do canal, ou startConversation), NENHUM ticket:assigned adicional é disparado: leia o atendente responsável direto em payload.ticket.assignedTo deste mesmo evento. A entrega não garante ordem entre os 8 eventos — um ticket recém-criado e imediatamente atribuído pelo roteamento de departamento pode entregar ticket:assigned antes deste evento — por isso todo evento "cheio" carrega o retrato completo do ticket, não um diff; resolva conflitos de ordem por payload.ticket.updatedAt. Use payload.id (também disponível no header X-ZapYou-Event-Id) como chave de deduplicação. Em caso de falha, a entrega tenta novamente até 4 vezes ao longo de cerca de 47 segundos e depois é perdida — não há log de entregas nem reenvio manual.

Payload Fields

FieldTypeDescription
idstringUUID gerado por entrega (crypto.randomUUID()) — chave de deduplicação, também disponível no header X-ZapYou-Event-Id
eventstringSempre "ticket:created"
datetimestringMomento em que o ticket foi criado, em ISO 8601 — não o momento da tentativa de entrega
payload.company.idstringCompany.id do tenant
payload.channel.idstringAttendanceChannel.id do canal onde o ticket foi criado
payload.channel.namestringNome do canal
payload.actor.typestringQuem originou a criação: attendant, contact, campaign, api, workflow ou system
payload.actor.idstringID de quem originou a ação. Ausente quando o tipo não tem um id associado
payload.actor.namestringNome de quem originou a ação, quando disponível
payload.ticket.idstringTicket.id
payload.ticket.protocolstringNúmero de protocolo do ticket, exibido ao cliente
payload.ticket.statusstringPENDING, OPEN, AUTOMATION ou CLOSED no momento deste envio
payload.ticket.prioritystringPrioridade do ticket (ex.: NORMAL, HIGH)
payload.ticket.assignedToobject{ id, name, email } do atendente responsável — CADA evento com retrato completo do ticket envia o nome e o e-mail do atendente atribuído, não só o id. null quando o ticket ainda não tem atendente — mesmo quando o ticket nasce já roteado para um membro, este campo já vem preenchido aqui, sem um ticket:assigned separado
payload.ticket.channelobject{ id, name } do canal — mesmo valor de payload.channel
payload.ticket.departmentobject{ id, name } do departamento do ticket, quando houver
payload.ticket.instanceobject{ id, code } da instância de WhatsApp (Baileys ou Meta Cloud) por trás do canal
payload.ticket.contactobject{ id, phone, name, pushName, email, tags } do contato. tags é um array de { id, name, color }; Contact.customFields nunca é enviado
payload.ticket.createdAtstringCriação do ticket, em ISO 8601
payload.ticket.firstResponseAtstringQuando o primeiro atendente respondeu. null enquanto não houver resposta
payload.ticket.resolvedAtstringQuando o ticket foi marcado como resolvido. null quando não se aplica
payload.ticket.closedAtstringQuando o ticket foi fechado. null enquanto estiver aberto
payload.ticket.lastMessageAtstringTimestamp da última mensagem do ticket
payload.ticket.updatedAtstringÚltima modificação do ticket — use para resolver conflitos quando eventos chegarem fora de ordem
payload.ticket.transferredAtstringQuando o ticket foi transferido pela última vez. null quando nunca foi transferido

Example Payload

JSON
{
  "id": "b3f1c2a4-8e1b-4c3d-9f2a-1234567890ab",
  "event": "ticket:created",
  "datetime": "2026-09-11T14:22:10.000Z",
  "payload": {
    "company": {
      "id": "6512aa00b2c3d4e5f6a7b8c1"
    },
    "channel": {
      "id": "6512bb00b2c3d4e5f6a7b8c2",
      "name": "Suporte"
    },
    "actor": {
      "type": "contact",
      "id": "6512cc00b2c3d4e5f6a7b8c3",
      "name": "João da Silva"
    },
    "ticket": {
      "id": "6512dd00b2c3d4e5f6a7b8c4",
      "protocol": "2026091100123",
      "status": "PENDING",
      "priority": "NORMAL",
      "assignedTo": null,
      "channel": {
        "id": "6512bb00b2c3d4e5f6a7b8c2",
        "name": "Suporte"
      },
      "department": {
        "id": "6512ee00b2c3d4e5f6a7b8c5",
        "name": "Comercial"
      },
      "instance": {
        "id": "6512ff00b2c3d4e5f6a7b8c6",
        "code": "INST-001"
      },
      "contact": {
        "id": "6512cc00b2c3d4e5f6a7b8c3",
        "phone": "551199999000",
        "name": "João da Silva",
        "pushName": "João",
        "email": null,
        "tags": [
          {
            "id": "6512bb00000000000000bb01",
            "name": "Lead quente",
            "color": "#FF5722"
          }
        ]
      },
      "createdAt": "2026-09-11T14:22:10.000Z",
      "firstResponseAt": null,
      "resolvedAt": null,
      "closedAt": null,
      "lastMessageAt": "2026-09-11T14:22:10.000Z",
      "updatedAt": "2026-09-11T14:22:10.000Z",
      "transferredAt": null
    }
  }
}
ticket:reopened

Disparado quando um ticket fechado (CLOSED) volta a ficar ativo — por DOIS caminhos bem distintos, e apenas um deles envolve mensagem nova. (1) Uma nova mensagem chega em um ticket recém-fechado: aqui o evento de Socket.IO emitido ao mesmo tempo para o painel é ticket:created, não reopened — o contrato deste webhook é deliberadamente mais rígido: mesmo id e protocol de antes do fechamento, isso é reaproveitamento, não um ticket novo. (2) Um atendente ou supervisor muda o status manualmente para fora de CLOSED (endpoint de troca de status) — sem nenhuma mensagem nova envolvida. NÃO assuma que ticket:reopened significa que o cliente escreveu de novo: compare payload.ticket.lastMessageAt com payload.datetime quando essa distinção importar para sua integração.

Payload Fields

FieldTypeDescription
idstringUUID da entrega — chave de deduplicação
eventstringSempre "ticket:reopened"
datetimestringMomento da reabertura, em ISO 8601
payload.company.idstringCompany.id do tenant
payload.channel.idstringAttendanceChannel.id do canal
payload.channel.namestringNome do canal
payload.actor.typestring"contact" quando reaberto por mensagem nova; "attendant" (ou outro tipo) quando reaberto por troca manual de status
payload.actor.idstringID de quem originou a reabertura
payload.actor.namestringNome de quem originou a reabertura, quando disponível
payload.ticketobjectRetrato completo do ticket no momento deste envio — mesmo formato de payload.ticket em ticket:created (id, protocol, status, priority, assignedTo, channel, department, instance, contact, createdAt, firstResponseAt, resolvedAt, closedAt, lastMessageAt, updatedAt, transferredAt), já refletindo o status de volta a PENDING ou OPEN
payload.reopen.previousClosedAtstringclosedAt do ticket antes desta reabertura, em ISO 8601

Example Payload

JSON
{
  "id": "c4a2d3b5-9f2c-4d4e-8a3b-234567890abc",
  "event": "ticket:reopened",
  "datetime": "2026-09-11T15:00:00.000Z",
  "payload": {
    "company": {
      "id": "6512aa00b2c3d4e5f6a7b8c1"
    },
    "channel": {
      "id": "6512bb00b2c3d4e5f6a7b8c2",
      "name": "Suporte"
    },
    "actor": {
      "type": "attendant",
      "id": "6512aa11b2c3d4e5f6a7b8c7",
      "name": "Ana"
    },
    "ticket": {
      "id": "6512dd00b2c3d4e5f6a7b8c4",
      "protocol": "2026091100123",
      "status": "PENDING",
      "priority": "NORMAL",
      "assignedTo": null,
      "channel": {
        "id": "6512bb00b2c3d4e5f6a7b8c2",
        "name": "Suporte"
      },
      "department": {
        "id": "6512ee00b2c3d4e5f6a7b8c5",
        "name": "Comercial"
      },
      "instance": {
        "id": "6512ff00b2c3d4e5f6a7b8c6",
        "code": "INST-001"
      },
      "contact": {
        "id": "6512cc00b2c3d4e5f6a7b8c3",
        "phone": "551199999000",
        "name": "João da Silva",
        "pushName": "João",
        "email": null,
        "tags": [
          {
            "id": "6512bb00000000000000bb01",
            "name": "Lead quente",
            "color": "#FF5722"
          }
        ]
      },
      "createdAt": "2026-09-10T10:00:00.000Z",
      "firstResponseAt": "2026-09-10T10:05:00.000Z",
      "resolvedAt": "2026-09-10T18:00:00.000Z",
      "closedAt": null,
      "lastMessageAt": "2026-09-10T17:55:00.000Z",
      "updatedAt": "2026-09-11T15:00:00.000Z",
      "transferredAt": null
    },
    "reopen": {
      "previousClosedAt": "2026-09-10T18:00:00.000Z"
    }
  }
}
ticket:assigned

Disparado quando um ticket passa a ter um atendente responsável: reivindicação manual da fila (assignment.mode: "claimed"), um atendente retomando um ticket PENDING pelo início de conversa (mode: "conversation"), ou o motor de roteamento automático por departamento (mode: "auto", round-robin/menos ocupado). ATENÇÃO: o modo "conversation" também carrega uma mudança de status que nenhum outro evento reporta — retomar um ticket PENDING move o status para OPEN, e essa transição só aparece aqui, nunca em um ticket:updated correspondente. Quem está inscrito só em ticket:updated (e não em ticket:assigned) nunca vai ver essa mudança de status; leia payload.ticket.status a partir dos eventos que você efetivamente recebe, sem assumir que toda troca de status chega como ticket:updated.

Payload Fields

FieldTypeDescription
idstringUUID da entrega — chave de deduplicação
eventstringSempre "ticket:assigned"
datetimestringMomento da atribuição, em ISO 8601
payload.company.idstringCompany.id do tenant
payload.channel.idstringAttendanceChannel.id do canal
payload.channel.namestringNome do canal
payload.actor.typestring"attendant" no claim manual ou na retomada de conversa; "system" no roteamento automático (mode: "auto")
payload.actor.idstringID de quem (ou do que) originou a atribuição
payload.actor.namestringNome de quem originou a atribuição, quando disponível
payload.ticketobjectRetrato completo do ticket, mesmo formato de ticket:created, já com assignedTo populado pelo novo atendente responsável
payload.assignment.fromobject{ id } do atendente anterior. null na primeira atribuição
payload.assignment.toobject{ id } do novo atendente responsável
payload.assignment.modestring"claimed" (reivindicado da fila), "conversation" (atendente retomando um ticket PENDING pelo início de conversa — também move o status para OPEN, sem ticket:updated correspondente) ou "auto" (motor de roteamento automático)

Example Payload

JSON
{
  "id": "d5b3e4c6-0a3d-4e5f-9b4c-34567890abcd",
  "event": "ticket:assigned",
  "datetime": "2026-09-11T14:25:00.000Z",
  "payload": {
    "company": {
      "id": "6512aa00b2c3d4e5f6a7b8c1"
    },
    "channel": {
      "id": "6512bb00b2c3d4e5f6a7b8c2",
      "name": "Suporte"
    },
    "actor": {
      "type": "attendant",
      "id": "6512aa11b2c3d4e5f6a7b8c7"
    },
    "ticket": {
      "id": "6512dd00b2c3d4e5f6a7b8c4",
      "protocol": "2026091100123",
      "status": "OPEN",
      "priority": "NORMAL",
      "assignedTo": {
        "id": "6512aa11b2c3d4e5f6a7b8c7",
        "name": "Ana Souza",
        "email": "[email protected]"
      },
      "channel": {
        "id": "6512bb00b2c3d4e5f6a7b8c2",
        "name": "Suporte"
      },
      "department": {
        "id": "6512ee00b2c3d4e5f6a7b8c5",
        "name": "Comercial"
      },
      "instance": {
        "id": "6512ff00b2c3d4e5f6a7b8c6",
        "code": "INST-001"
      },
      "contact": {
        "id": "6512cc00b2c3d4e5f6a7b8c3",
        "phone": "551199999000",
        "name": "João da Silva",
        "pushName": "João",
        "email": null,
        "tags": [
          {
            "id": "6512bb00000000000000bb01",
            "name": "Lead quente",
            "color": "#FF5722"
          }
        ]
      },
      "createdAt": "2026-09-11T14:22:10.000Z",
      "firstResponseAt": null,
      "resolvedAt": null,
      "closedAt": null,
      "lastMessageAt": "2026-09-11T14:22:10.000Z",
      "updatedAt": "2026-09-11T14:25:00.000Z",
      "transferredAt": null
    },
    "assignment": {
      "from": null,
      "to": {
        "id": "6512aa11b2c3d4e5f6a7b8c7"
      },
      "mode": "claimed"
    }
  }
}
ticket:transferred

Disparado quando um ticket muda de atendente e/ou de departamento. transfer.type é "attendant" (transferência direta entre atendentes, roteamento de resposta de campanha, ou transferência para um atendente feita pela API Pública em POST /tickets/:ticketId/transfer ou pelo nó "Transferir Ticket" de um workflow) ou "department" (transferência entre departamentos — manual, pela API ou por um workflow — que sempre limpa o atendente em to: to.attendant vem null e o ticket volta para PENDING na fila do departamento de destino). Quando um ticket em AUTOMATION é movido para outro departamento pela API ou por um workflow, o backend dispara primeiro um ticket:updated (changes.status: { from: "AUTOMATION", to: "PENDING" }) e só depois este ticket:transferred — quem escuta só ticket:transferred não vê a passagem por AUTOMATION, apenas o ticket já em PENDING no departamento novo. transfer.reason é null quando quem transferiu não informou um motivo. ATENÇÃO ao formato de transfer.from/transfer.to: department nunca traz name, em nenhum caminho — só { id }. attendant traz name na transferência direta entre atendentes e em to.attendant das transferências para um atendente feitas pela API ou por um workflow; no roteamento de resposta de campanha, nas transferências de departamento e em from.attendant das transferências pela API/workflow, attendant vem só com { id }.

Payload Fields

FieldTypeDescription
idstringUUID da entrega — chave de deduplicação
eventstringSempre "ticket:transferred"
datetimestringMomento da transferência, em ISO 8601
payload.company.idstringCompany.id do tenant
payload.channel.idstringAttendanceChannel.id do canal
payload.channel.namestringNome do canal
payload.actor.typestringQuem originou a transferência: attendant, campaign (roteamento de resposta), api (API Pública), workflow (nó Transferir Ticket) ou system
payload.actor.idstringID de quem originou a transferência
payload.actor.namestringNome de quem originou a transferência, quando disponível
payload.ticketobjectRetrato completo do ticket, mesmo formato de ticket:created, já com assignedTo e department refletindo o destino da transferência
payload.transfer.typestring"attendant" (entre atendentes, roteamento de resposta de campanha, ou transferência para um atendente pela API/workflow) ou "department" (entre departamentos, manual, pela API ou por um workflow — sempre limpa o atendente em to)
payload.transfer.fromobject{ attendant: { id } | { id, name } | null, department: { id } | null } — estado antes da transferência. attendant só inclui name na transferência direta entre atendentes; no roteamento de resposta de campanha, na transferência de departamento e nas transferências feitas pela API ou por um workflow, vem só { id } (ou null). department nunca inclui name, em nenhum caminho
payload.transfer.toobject{ attendant: { id } | { id, name } | null, department: { id } | null } — estado depois da transferência. attendant inclui name na transferência direta entre atendentes e nas transferências para um atendente feitas pela API ou por um workflow; no roteamento de resposta de campanha e nas transferências de departamento (to.attendant sempre null) não há name. department nunca inclui name
payload.transfer.reasonstringMotivo informado por quem transferiu. null quando nenhum foi dado

Example Payload

JSON
{
  "id": "e6c4f5d7-1b4e-4f60-ac5d-4567890abcde",
  "event": "ticket:transferred",
  "datetime": "2026-09-11T14:40:00.000Z",
  "payload": {
    "company": {
      "id": "6512aa00b2c3d4e5f6a7b8c1"
    },
    "channel": {
      "id": "6512bb00b2c3d4e5f6a7b8c2",
      "name": "Suporte"
    },
    "actor": {
      "type": "attendant",
      "id": "6512aa11b2c3d4e5f6a7b8c7",
      "name": "Ana"
    },
    "ticket": {
      "id": "6512dd00b2c3d4e5f6a7b8c4",
      "protocol": "2026091100123",
      "status": "OPEN",
      "priority": "NORMAL",
      "assignedTo": {
        "id": "6512aa22b2c3d4e5f6a7b8c8",
        "name": "Bruno Lima",
        "email": "[email protected]"
      },
      "channel": {
        "id": "6512bb00b2c3d4e5f6a7b8c2",
        "name": "Suporte"
      },
      "department": {
        "id": "6512ee00b2c3d4e5f6a7b8c5",
        "name": "Comercial"
      },
      "instance": {
        "id": "6512ff00b2c3d4e5f6a7b8c6",
        "code": "INST-001"
      },
      "contact": {
        "id": "6512cc00b2c3d4e5f6a7b8c3",
        "phone": "551199999000",
        "name": "João da Silva",
        "pushName": "João",
        "email": null,
        "tags": [
          {
            "id": "6512bb00000000000000bb01",
            "name": "Lead quente",
            "color": "#FF5722"
          }
        ]
      },
      "createdAt": "2026-09-11T14:22:10.000Z",
      "firstResponseAt": "2026-09-11T14:25:00.000Z",
      "resolvedAt": null,
      "closedAt": null,
      "lastMessageAt": "2026-09-11T14:38:00.000Z",
      "updatedAt": "2026-09-11T14:40:00.000Z",
      "transferredAt": "2026-09-11T14:40:00.000Z"
    },
    "transfer": {
      "type": "attendant",
      "from": {
        "attendant": {
          "id": "6512aa11b2c3d4e5f6a7b8c7",
          "name": "Ana"
        },
        "department": null
      },
      "to": {
        "attendant": {
          "id": "6512aa22b2c3d4e5f6a7b8c8",
          "name": "Bruno"
        },
        "department": null
      },
      "reason": "Cliente pediu para falar com o financeiro"
    }
  }
}
ticket:updated

O evento genérico para qualquer outra transição pontual que não seja criação, atribuição, transferência, fechamento ou reabertura: troca de prioridade (changes.priority), devolução para a fila (changes.status: { from, to: "PENDING" }), ou qualquer outra troca de status que não entre nem saia de CLOSED. changes normalmente tem uma única chave — trate-o como um objeto aberto, sem assumir que priority e status são as únicas chaves possíveis no futuro. EXCEÇÃO com duas chaves: acontece quando um supervisor remove um membro do canal pedindo para devolver os tickets dele à fila, e também quando um ticket PENDING ou OPEN é devolvido para a fila via POST /tickets/:ticketId/transfer com target: "queue" (API Pública ou nó "Transferir Ticket" de um workflow) — nos dois casos, cada ticket afetado dispara este evento com AMBAS changes.status: { from, to: "PENDING" } e changes.assignedTo: { from: <id do atendente removido ou desvinculado>, to: null }. A chave status sozinha pode parecer um no-op — um ticket já PENDING reporta { from: "PENDING", to: "PENDING" } — é assignedTo que mostra a mudança real. Um ticket já PENDING, sem atendente e no mesmo departamento é um no-op de verdade: a transferência para "queue" não dispara evento algum nesse caso.

Payload Fields

FieldTypeDescription
idstringUUID da entrega — chave de deduplicação
eventstringSempre "ticket:updated"
datetimestringMomento da mudança, em ISO 8601
payload.company.idstringCompany.id do tenant
payload.channel.idstringAttendanceChannel.id do canal
payload.channel.namestringNome do canal
payload.actor.typestringQuem originou a mudança: attendant, api (API Pública), workflow ou system
payload.actor.idstringID de quem originou a mudança
payload.actor.namestringNome de quem originou a mudança, quando disponível
payload.ticketobjectRetrato completo do ticket, mesmo formato de ticket:created, já refletindo os novos valores
payload.changesobjectObjeto aberto com uma transição por chave, no formato { from, to }. Normalmente uma única chave (ex.: priority, ou status para uma troca que não seja fechamento nem reabertura). Vem com DUAS chaves — status ({ from, to: "PENDING" }) e assignedTo ({ from: <id removido ou desvinculado>, to: null }) juntas — na remoção de membro com devolução à fila e na devolução à fila via POST /tickets/:ticketId/transfer com target: "queue"; não assuma que status sozinho conta a história toda

Example Payload

JSON
{
  "id": "f7d5a6e8-2c5f-4a71-bd6e-567890abcdef",
  "event": "ticket:updated",
  "datetime": "2026-09-11T14:45:00.000Z",
  "payload": {
    "company": {
      "id": "6512aa00b2c3d4e5f6a7b8c1"
    },
    "channel": {
      "id": "6512bb00b2c3d4e5f6a7b8c2",
      "name": "Suporte"
    },
    "actor": {
      "type": "attendant",
      "id": "6512aa11b2c3d4e5f6a7b8c7"
    },
    "ticket": {
      "id": "6512dd00b2c3d4e5f6a7b8c4",
      "protocol": "2026091100123",
      "status": "OPEN",
      "priority": "HIGH",
      "assignedTo": {
        "id": "6512aa11b2c3d4e5f6a7b8c7",
        "name": "Ana Souza",
        "email": "[email protected]"
      },
      "channel": {
        "id": "6512bb00b2c3d4e5f6a7b8c2",
        "name": "Suporte"
      },
      "department": {
        "id": "6512ee00b2c3d4e5f6a7b8c5",
        "name": "Comercial"
      },
      "instance": {
        "id": "6512ff00b2c3d4e5f6a7b8c6",
        "code": "INST-001"
      },
      "contact": {
        "id": "6512cc00b2c3d4e5f6a7b8c3",
        "phone": "551199999000",
        "name": "João da Silva",
        "pushName": "João",
        "email": null,
        "tags": [
          {
            "id": "6512bb00000000000000bb01",
            "name": "Lead quente",
            "color": "#FF5722"
          }
        ]
      },
      "createdAt": "2026-09-11T14:22:10.000Z",
      "firstResponseAt": "2026-09-11T14:25:00.000Z",
      "resolvedAt": null,
      "closedAt": null,
      "lastMessageAt": "2026-09-11T14:38:00.000Z",
      "updatedAt": "2026-09-11T14:45:00.000Z",
      "transferredAt": null
    },
    "changes": {
      "priority": {
        "from": "NORMAL",
        "to": "HIGH"
      }
    }
  }
}
ticket:closed

Disparado quando o status do ticket se torna CLOSED — por QUATRO caminhos: a ação de fechar, o fim de uma automação de workflow, uma troca direta de status para CLOSED, ou o rollback automático de startConversation (actor.type: "system") quando a primeira mensagem de uma conversa recém-iniciada falha ao enviar — o ticket é fechado sozinho para não ficar como um "ticket fantasma", e nesse caminho específico close.note vem com o valor fixo "start_conversation_rollback" em vez de uma nota de resolução real. Fora desse caso, close.note é null quando nenhuma nota de resolução foi informada. Fechamento em massa dispara este evento UMA VEZ POR TICKET: fechar 200 tickets num bulk-close dispara 200 entregas separadas de ticket:closed, mesmo o painel recebendo um único evento de Socket.IO para a mesma operação.

Payload Fields

FieldTypeDescription
idstringUUID da entrega — chave de deduplicação
eventstringSempre "ticket:closed"
datetimestringMomento do fechamento, em ISO 8601
payload.company.idstringCompany.id do tenant
payload.channel.idstringAttendanceChannel.id do canal
payload.channel.namestringNome do canal
payload.actor.typestringQuem fechou o ticket: attendant, workflow (fim de automação), system (fechamento automático de rollback do startConversation) ou o tipo de ator que originou o fechamento direto de status
payload.actor.idstringID de quem fechou o ticket
payload.actor.namestringNome de quem fechou o ticket, quando disponível
payload.ticketobjectRetrato completo do ticket, mesmo formato de ticket:created, com status: "CLOSED" e closedAt já preenchido
payload.close.notestringNota de resolução informada ao fechar. null quando nenhuma foi dada. No fechamento automático de rollback (startConversation, primeira mensagem falhou), vem com o valor fixo "start_conversation_rollback"

Example Payload

JSON
{
  "id": "a8e6b7f9-3d60-4b82-ce7f-67890abcdef0",
  "event": "ticket:closed",
  "datetime": "2026-09-11T16:00:00.000Z",
  "payload": {
    "company": {
      "id": "6512aa00b2c3d4e5f6a7b8c1"
    },
    "channel": {
      "id": "6512bb00b2c3d4e5f6a7b8c2",
      "name": "Suporte"
    },
    "actor": {
      "type": "attendant",
      "id": "6512aa11b2c3d4e5f6a7b8c7"
    },
    "ticket": {
      "id": "6512dd00b2c3d4e5f6a7b8c4",
      "protocol": "2026091100123",
      "status": "CLOSED",
      "priority": "HIGH",
      "assignedTo": {
        "id": "6512aa11b2c3d4e5f6a7b8c7",
        "name": "Ana Souza",
        "email": "[email protected]"
      },
      "channel": {
        "id": "6512bb00b2c3d4e5f6a7b8c2",
        "name": "Suporte"
      },
      "department": {
        "id": "6512ee00b2c3d4e5f6a7b8c5",
        "name": "Comercial"
      },
      "instance": {
        "id": "6512ff00b2c3d4e5f6a7b8c6",
        "code": "INST-001"
      },
      "contact": {
        "id": "6512cc00b2c3d4e5f6a7b8c3",
        "phone": "551199999000",
        "name": "João da Silva",
        "pushName": "João",
        "email": null,
        "tags": [
          {
            "id": "6512bb00000000000000bb01",
            "name": "Lead quente",
            "color": "#FF5722"
          }
        ]
      },
      "createdAt": "2026-09-11T14:22:10.000Z",
      "firstResponseAt": "2026-09-11T14:25:00.000Z",
      "resolvedAt": "2026-09-11T16:00:00.000Z",
      "closedAt": "2026-09-11T16:00:00.000Z",
      "lastMessageAt": "2026-09-11T15:58:00.000Z",
      "updatedAt": "2026-09-11T16:00:00.000Z",
      "transferredAt": null
    },
    "close": {
      "note": "Resolvido — cliente confirmou recebimento"
    }
  }
}
ticket:message

Disparado quando uma mensagem — em qualquer direção — é adicionada ao ticket. payload.message.direction deriva de sender: "inbound" apenas quando sender é CONTACT, "outbound" para qualquer outro remetente (ATTENDANT, CAMPAIGN, API, WORKFLOW, SYSTEM). Uma mensagem deduplicada por WAMID ou por hash de conteúdo NÃO dispara evento algum, nem parcial; o backfill de reconciliação de 24h também não dispara nada — é uma leitura local de conserto, não atividade nova. ATENÇÃO: payload.message.status reporta o resultado da TENTATIVA de envio, que pode divergir brevemente do que fica gravado no banco — no envio de template do Meta Cloud pelo início de conversa, um envio bem-sucedido reporta status: "SENT" aqui mesmo que o registro da mensagem no banco permaneça PENDING até a Meta confirmar a entrega pelo webhook de status dela; se essa confirmação divergir, a correção chega depois como um ticket:message:status separado — não trate este status como definitivo nesse caso específico.

Payload Fields

FieldTypeDescription
idstringUUID da entrega — chave de deduplicação
eventstringSempre "ticket:message"
datetimestringMomento da mensagem, em ISO 8601
payload.company.idstringCompany.id do tenant
payload.channel.idstringAttendanceChannel.id do canal
payload.channel.namestringNome do canal
payload.actor.typestringOrigem da mensagem: attendant, contact, campaign, api, workflow ou system
payload.actor.idstringID de quem enviou a mensagem, quando aplicável
payload.actor.namestringNome de quem enviou a mensagem, quando disponível
payload.ticketobjectRetrato completo do ticket, mesmo formato de ticket:created
payload.message.idstringTicketMessage.id
payload.message.wamidstringID da mensagem no WhatsApp
payload.message.directionstring"inbound" apenas quando sender é CONTACT; "outbound" para qualquer outro remetente
payload.message.senderstringCONTACT, ATTENDANT, CAMPAIGN, API, WORKFLOW ou SYSTEM
payload.message.senderUserobject{ id } quando a mensagem tem um remetente interno identificado (um atendente). null caso contrário
payload.message.typestringTipo da mensagem (text, image, etc.)
payload.message.contentstringTexto ou legenda da mensagem
payload.message.mediaobject{ url, mimeType } quando a mensagem tem anexo. null para mensagens de texto
payload.message.statusstringSENT, DELIVERED, READ, FAILED ou PENDING — resultado da TENTATIVA de envio no momento deste evento, que pode divergir do que fica gravado no banco (ver descrição acima)
payload.message.timestampstringMomento da mensagem, em ISO 8601

Example Payload

JSON
{
  "id": "b9f7c8a0-4e71-4c93-df80-7890abcdef01",
  "event": "ticket:message",
  "datetime": "2026-09-11T14:23:00.000Z",
  "payload": {
    "company": {
      "id": "6512aa00b2c3d4e5f6a7b8c1"
    },
    "channel": {
      "id": "6512bb00b2c3d4e5f6a7b8c2",
      "name": "Suporte"
    },
    "actor": {
      "type": "attendant",
      "id": "6512aa11b2c3d4e5f6a7b8c7"
    },
    "ticket": {
      "id": "6512dd00b2c3d4e5f6a7b8c4",
      "protocol": "2026091100123",
      "status": "OPEN",
      "priority": "NORMAL",
      "assignedTo": {
        "id": "6512aa11b2c3d4e5f6a7b8c7",
        "name": "Ana Souza",
        "email": "[email protected]"
      },
      "channel": {
        "id": "6512bb00b2c3d4e5f6a7b8c2",
        "name": "Suporte"
      },
      "department": {
        "id": "6512ee00b2c3d4e5f6a7b8c5",
        "name": "Comercial"
      },
      "instance": {
        "id": "6512ff00b2c3d4e5f6a7b8c6",
        "code": "INST-001"
      },
      "contact": {
        "id": "6512cc00b2c3d4e5f6a7b8c3",
        "phone": "551199999000",
        "name": "João da Silva",
        "pushName": "João",
        "email": null,
        "tags": [
          {
            "id": "6512bb00000000000000bb01",
            "name": "Lead quente",
            "color": "#FF5722"
          }
        ]
      },
      "createdAt": "2026-09-11T14:22:10.000Z",
      "firstResponseAt": "2026-09-11T14:23:00.000Z",
      "resolvedAt": null,
      "closedAt": null,
      "lastMessageAt": "2026-09-11T14:23:00.000Z",
      "updatedAt": "2026-09-11T14:23:00.000Z",
      "transferredAt": null
    },
    "message": {
      "id": "6512ff11b2c3d4e5f6a7b8c9",
      "wamid": "wamid.HBgNNTUxMTk5OTk5MDAwFQ",
      "direction": "outbound",
      "sender": "ATTENDANT",
      "senderUser": {
        "id": "6512aa11b2c3d4e5f6a7b8c7"
      },
      "type": "text",
      "content": "Olá! Em que posso ajudar?",
      "media": null,
      "status": "SENT",
      "timestamp": "2026-09-11T14:23:00.000Z"
    }
  }
}
ticket:message:status

O evento de maior frequência do sistema — um por confirmação de entrega (SENT → DELIVERED → READ, ou FAILED) de cada mensagem enviada. Por isso o payload é deliberadamente enxuto: payload.channel traz só { id } (sem name), payload.ticket traz só { id } (SEM protocol e sem mais nenhum campo do ticket), e payload.actor é sempre null. Se sua integração precisar do protocolo ou de outro campo do ticket para este evento, use um ticket:created/ticket:updated que você já recebeu e guardou em cache, ou consulte a API de leitura de tickets. payload.message.status é sempre SENT, DELIVERED, READ ou FAILED — NUNCA PENDING: uma confirmação que volta uma mensagem para PENDING (ex.: retentativa interna de formato de áudio AAC) nunca publica este evento.

Payload Fields

FieldTypeDescription
idstringUUID da entrega — chave de deduplicação
eventstringSempre "ticket:message:status"
datetimestringMomento da confirmação de entrega, em ISO 8601
payload.company.idstringCompany.id do tenant
payload.channel.idstringAttendanceChannel.id do canal — único campo de channel neste evento, sem name
payload.actorobjectnull sempre neste evento — este é o único dos 8 eventos em que actor nunca é populado, mesmo quando a mudança de status é conhecida
payload.ticket.idstringTicket.id — único campo do ticket neste evento, sem protocol e sem o restante do retrato
payload.message.idstringTicketMessage.id
payload.message.wamidstringID da mensagem no WhatsApp
payload.message.statusstringSENT, DELIVERED, READ ou FAILED — nunca PENDING
payload.message.errorstringMensagem de erro quando status é FAILED e uma está disponível. null caso contrário

Example Payload

JSON
{
  "id": "c0a8d9b1-5f82-4da4-e091-890abcdef012",
  "event": "ticket:message:status",
  "datetime": "2026-09-11T14:23:05.000Z",
  "payload": {
    "company": {
      "id": "6512aa00b2c3d4e5f6a7b8c1"
    },
    "channel": {
      "id": "6512bb00b2c3d4e5f6a7b8c2"
    },
    "actor": null,
    "ticket": {
      "id": "6512dd00b2c3d4e5f6a7b8c4"
    },
    "message": {
      "id": "6512ff11b2c3d4e5f6a7b8c9",
      "wamid": "wamid.HBgNNTUxMTk5OTk5MDAwFQ",
      "status": "DELIVERED",
      "error": null
    }
  }
}

Códigos de Erro

A API do ZapYou usa códigos de status HTTP convencionais para indicar sucesso ou falha de uma requisição. Códigos na faixa 2xx indicam sucesso, códigos na faixa 4xx indicam erro do cliente, e códigos na faixa 5xx indicam erro do servidor.

CodeStatusNameDescriptionSolution
—200SucessoRequisição processada com sucesso
Nenhuma ação necessária
—201CriadoRecurso criado. Devolvido apenas por POST /campaigns
Nenhuma ação necessária
(sem code)400Requisição InválidaCampos obrigatórios ausentes ou inválidos nos endpoints de envio e de instância. O corpo traz apenas { message }, ex.: "Invalid fields", "Number not found", "Invalid type. Must be 'PN' or 'LID'"
Confira os campos obrigatórios do endpoint e o formato do número
INVALID_FIELDS400Campos Inválidos (validação Zod)Usado nos endpoints validados por Zod (campanhas, tickets e envio de template). O corpo é { error: "INVALID_FIELDS", details: [...] }, onde details são as issues cruas do Zod (path, code, message) — não há campo message no nível raiz
Percorra details[].path para mapear cada erro ao campo correspondente do formulário
TAG_FILTER_TOO_BROAD400Filtro de Tags Muito AmploNa listagem de tickets, o tagIds enviado casa mais de 5.000 contatos; a requisição é rejeitada, não truncada
Restrinja o filtro (menos tags / tags mais específicas) ou combine com contactId, channelId, departmentId ou um range de datas
(sem code)401Não AutorizadoCobre dois casos distintos: credenciais inválidas (cabeçalhos member/token ausentes ou não conferem; Authorization diferente do token da instância; Bearer de AI Agent inválido ou expirado) E falta de permissão nos endpoints de instância e mensagem, que respondem 401 — não 403 — com { message: "User without permission" }. Para o par member/token, também: { message: "User is blocked" } (o usuário do membro está bloqueado) e { message: "Token revoked" } (o usuário teve as sessões encerradas — bloqueio, troca ou redefinição de senha, ou revogação pelo administrador — depois que o token foi gerado). Tokens gerados antes desta mudança deixam de valer se o usuário teve as sessões encerradas em qualquer momento desde a versão anterior, mesmo antes de o token ter sido gerado.
Verifique o esquema de autenticação do endpoint (são três, incompatíveis entre si) e, se as credenciais estiverem certas, a permissão do grupo do membro. Com "Token revoked", gere um novo token no painel (Configurações da organização, aba API). O token do membro é mostrado uma única vez, ao ser gerado na aba API das Configurações da organização (formato zpy_ seguido de 43 caracteres; tokens antigos de 24 caracteres hexadecimais continuam valendo). Perdeu o token? Gere um novo: o anterior para de funcionar na hora.
SUBSCRIPTION_REQUIRED402Assinatura NecessáriaPOST /instances/poweron/:instanceId em instância sem subscription ACTIVE ou TRIALING
Regularize a assinatura da instância antes de ligá-la
SUBSCRIPTION_EXPIRED402Assinatura ExpiradaPOST /instances/poweron/:instanceId com o período da assinatura vencido
Renove a assinatura da instância
TRIAL_EXPIRED402Trial ExpiradoPOST /instances/poweron/:instanceId em instância de teste com prazo vencido
Adicione um método de pagamento para converter o trial em assinatura
Insufficient Balance402Saldo de IA InsuficientePOST /aiagents/chat sem saldo de IA na empresa para reservar o custo do embedding ou da geração. É o ÚNICO 402 por saldo da API — os endpoints de envio de mensagem nunca retornam 402
Recarregue o saldo de IA da empresa no painel
(sem code)403Permissão InsuficienteUsado pelos endpoints de campanhas, tickets e lookups. O corpo traz { message: "Insufficient permissions", requiredPermission: "<código>" }, que nomeia exatamente a permissão faltante. Exceção: POST /tickets/:ticketId/transfer com target "queue" aceita duas permissões e traz requiredPermissions — uma lista, da qual basta uma
Conceda ao grupo do membro a permissão indicada em requiredPermission, ou uma das listadas em requiredPermissions
(sem code)403Empresa InativaCabeçalhos member/token válidos, mas a empresa está desativada: { message: "Company inactive" }. Só é devolvido depois de o token ser aceito, então não revela o estado de uma empresa a quem não tem o token
Fale com o suporte para reativar a empresa
Forbidden403Widget DesabilitadoGET /aiagents/widget/config com o widget desativado para o token informado
Habilite o widget para esse token em AI Agents → [Agente] → Tokens
(sem code)404Não EncontradoRecurso inexistente ou pertencente a outra empresa — a API responde 404 nos dois casos, de propósito, para não confirmar a existência de IDs alheios
Confira o ID e se ele pertence à empresa do token
TEMPLATE_NOT_FOUND404Template Não EncontradoO template não existe para essa empresa/idioma (envio via template)
Confira templateName e language; eles precisam bater com um template aprovado
TICKET_NOT_FOUND404Ticket Não EncontradoPOST /tickets/:ticketId/transfer com um :ticketId malformado, inexistente ou de outra empresa. Os endpoints de leitura de tickets respondem o 404 genérico, sem code
Confira o ID do ticket e se ele pertence à empresa do token
TEMPLATE_NOT_SUPPORTED422Template Não SuportadoA instância é Baileys; templates só funcionam em instâncias Meta Cloud
Use uma instância meta_cloud para enviar via template
TEMPLATE_NOT_APPROVED422Template Não AprovadoO template não está com status APPROVED (veja rejectedReason)
Aguarde aprovação do template na Meta ou ajuste e reenvie para revisão
MISSING_VARIABLES422Variáveis AusentesFaltam variáveis exigidas pelo template (body, header ou buttons)
Use GET /templates/:id → expectedVariables para montar o payload completo
DEPARTMENT_NOT_FOUND422Departamento Não EncontradoTransferência de ticket com um departmentId inexistente, inativo ou de outra empresa — sempre 422, nunca 404, para não confirmar IDs alheios
Liste os departamentos ativos em GET /departments e use um id de lá
ATTENDANT_NOT_ELIGIBLE422Atendente InelegívelTransferência de ticket com um attendantId inexistente, bloqueado, fora da empresa ou sem vínculo ativo com o canal do ticket. Presença e capacidade não contam
Use o id de GET /members (User.id, não o id da associação) e confira se o usuário é membro do canal do ticket
ATTENDANT_NOT_IN_DEPARTMENT422Atendente Fora do DepartamentoTransferência com target "attendant" e um departmentId de departamento do qual o atendente não é membro ativo
Omita departmentId para o ticket seguir o departamento do atendente, ou informe um departamento ao qual ele pertence
(sem code)422Entidade Não ProcessávelRegra de negócio violada em campanhas, ex.: "One or more instances are invalid" ao associar instâncias que não pertencem à empresa
Verifique os IDs enviados e o estado atual da campanha
(sem code)409ConflitoAção de campanha incompatível com o estado atual ("Campaign already sending") ou reenvio detectado pelo header Idempotency-Key ("Duplicate request")
Consulte o status atual em GET /campaigns/:campaignId antes de repetir a ação
TICKET_CLOSED409Ticket FechadoPOST /tickets/:ticketId/transfer em um ticket CLOSED. Só tickets PENDING, OPEN ou AUTOMATION são transferidos
Reabra o ticket pelo painel antes de transferir
CONCURRENT_UPDATE409Atualização ConcorrenteO ticket mudou de status enquanto a transferência era aplicada (ex.: um atendente aceitou o ticket no mesmo instante). Nada foi gravado
Consulte GET /tickets/:ticketId e repita a transferência se ela ainda fizer sentido
DEPARTMENTS_DISABLED409Departamentos DesabilitadosTransferência que envolve um departamento numa empresa sem departamentos habilitados: target "department", ou target "attendant" com departmentId
Use target "queue", ou "attendant" sem departmentId, ou habilite departamentos para a empresa
RATE_LIMIT_EXCEEDED429Limite de Taxa ExcedidoMuitas requisições na janela. O corpo traz { error, retryAfter } e, em alguns buckets, message. Os cabeçalhos padrão RateLimit-* acompanham a resposta
Aguarde os segundos indicados em retryAfter e implemente backoff exponencial
(sem code)500Erro Interno do ServidorErro inesperado. A mensagem crua fica no log do servidor, nunca na resposta
Tente novamente mais tarde ou contate o suporte
(sem code)503Instância IndisponívelEndpoints de envio: a instância existe mas o socket não está autenticado no momento (Baileys) ou a instância Meta Cloud não está ativa
Confirme o estado da conexão e, se necessário, desligue e ligue a instância
(sem code)503Autenticação IndisponívelEndpoints autenticados por member + token: o estado de autenticação do membro não pôde ser lido no momento ({ message: "Authentication unavailable" }). Não é falha de credencial
Tente novamente em alguns segundos, com backoff
(sem code)504Timeout de EnvioSem resposta do envio em 30s. Ocorre apenas em POST /instances/send/template/:instanceId
A mensagem pode ter sido enviada mesmo assim — confirme antes de repetir, para não duplicar o disparo

Limites de Taxa

Limites de Requisição

Para garantir a qualidade do serviço para todos os usuários, a API do ZapYou implementa limites de taxa nas requisições. Se você exceder o limite, receberá um erro 429 Too Many Requests.

POST /instances/send/* (texto, imagem, vídeo, áudio, documento e template)

Janela fixa de 1 minuto, contada por instância. O token da instância é verificado antes da contagem: uma requisição com token inválido recebe 401 e não conta. Não existe header que dispense o limite

30
Requisições
por minuto
POST /aiagents/chat

Mesmo limitador dos envios (30/min, janela fixa), contado por token do AI Agent verificado

30
Requisições
por minuto
GET /aiagents/agent, /aiagents/sessions/:sessionId, /aiagents/widget/config e DELETE /aiagents/sessions/:sessionId

Bucket geral da API. Chave = o Bearer token do AI Agent

100
Requisições
por minuto
GET /instances, /instances/:instanceId, /templates, /templates/:id, /messages, /messages/:phone e POST /instances/poweron|poweroff|disconnect

Janela fixa de 1 minuto, contada por membro (o par member + token autenticado), compartilhada entre todas as integrações do mesmo membro. O header Authorization não altera a contagem

100
Requisições
por minuto
Endpoints de campanhas (/campaigns/*), GET /departments e GET /members

Chave = companyId do token. Bucket próprio, que não compete com o dos tickets

60
Requisições
por minuto
GET /tickets/* e POST /tickets/:ticketId/transfer

Chave = companyId do token. Bucket separado, dimensionado para dashboards com várias widgets; a transferência conta no mesmo bucket

120
Requisições
por minuto
PUT /instances/update/:instanceId, PUT /instances/webhooks/:instanceId e POST /messages/clear-cache

Estes três endpoints não têm rate limit aplicado; ainda assim, use-os com parcimônia

sem limite
Requisições
por —