Desenvolvedores
Webhooks Beta
Receba notificações assinadas e confiáveis quando contatos, conversas, mensagens, tags ou membros da equipe mudarem.
Abrir a referência da APICrie um endpoint
- 01
Abra Configurações, selecione Desenvolvedor, encontre Webhooks e selecione Adicionar endpoint.
- 02
Informe um Nome e uma URL do endpoint pública em HTTPS e escolha pelo menos um item em Eventos.
- 03
Selecione Adicionar endpoint. Você também pode criar um com
POST /api/v1/webhooks.
O Sonny rejeita credenciais em URLs, localhost, faixas de IP privadas/link-local e nomes DNS que apontam para um endereço não público.
Salve o segredo de assinatura na hora
Ele começa com whsec_ e é exibido uma única vez. Copie-o para o seu gerenciador de segredos antes de selecionar Já salvei.
Limite um endpoint a canais selecionados
Em Configurações → Desenvolvedor, cada endpoint pode ouvir todos os canais ou só os que você escolher, tanto ao adicioná-lo quanto ao editá-lo depois. Endpoints criados antes de existir a limitação por canal continuam em todos os canais até você alterá-los.
Clientes de API e MCP definem o mesmo filtro com sourceIds ao criar ou atualizar um endpoint. Uma lista vazia significa todas as origens do espaço de trabalho. Quando há IDs, eventos de conversas e mensagens só são entregues se a conversa pertencer a uma das origens selecionadas. Eventos do espaço de trabalho, como mudanças em contatos ou em membros, não são enviados a um endpoint filtrado por origem.
{
"name": "Product A agent",
"url": "https://agent.example.com/sonny",
"events": ["conversation.created", "message.created"],
"sourceIds": ["cm_source_id"]
}Pare de entregar o que sua integração descarta
Um agente que responde pela API é acordado pela própria resposta, a menos que você diga o contrário. Três filtros opcionais restringem o que um endpoint recebe; todos vêm desligados, então um endpoint existente não muda.
agentGroupIds- Apenas conversas dos grupos de agentes listados. Isso segue a quem a conversa pertence, não o canal por onde ela chegou, então uma conversa de uma caixa de entrada compartilhada chega ao grupo dono dela. Conversas sem grupo não são entregues e, como os grupos costumam ser atribuídos depois que a conversa começa,
conversation.createdmuitas vezes dispara antes de existir um grupo correspondente. customerMessagesOnly- Apenas mensagens escritas pelo cliente. Ignora respostas da sua equipe, notas internas e tudo o que for enviado pela API, pelo MCP ou pela resposta automática de IA — incluindo as respostas do próprio endpoint.
excludedSenderIds- Ignora mensagens enviadas pelos colegas listados. Dê à integração uma conta de colega própria e exclua-a para que ela não seja acordada pelas próprias respostas, mas continue sabendo quando um humano assume a conversa — o sinal de que um agente de IA precisa para se retirar.
Os filtros de mensagens valem apenas para eventos message.*; para todo o resto, o controle continua sendo os tipos de evento. Um evento filtrado é descartado antes de virar uma entrega, então não custa nada e nunca aparece como falha. O payload e a apiVersion não mudam em nenhum dos casos.
Agrupe uma rajada de mensagens em uma só entrega
Um consumidor que lê a conversa inteira ao acordar não ganha nada com quatro entregas separadas em dez segundos. Defina coalesceSeconds e as mensagens de uma conversa são reunidas durante esse tempo e depois enviadas em uma única entrega. Vem desligado por padrão; endpoints sem essa opção continuam recebendo cada mensagem separadamente.
Uma entrega agrupada chega como conversation.activity, trazendo a conversa e todos os ids de mensagens reunidos; então leia a conversa uma vez, e não a cada mensagem. Esta é a única configuração que muda o formato do que você recebe, e por isso precisa ser ativada. Seus outros filtros continuam valendo — uma mensagem excluída por eles nunca entra em um grupo. Cada conversa tem a própria janela, e as novas tentativas tratam o grupo como uma única entrega.
{
"type": "conversation.activity",
"data": {
"conversationId": "cm_conversation_id",
"messageIds": ["cm_first_message", "cm_second_message"]
}
}Endpoints protegidos por autenticação bearer ou chave de API
Se o seu receptor fica atrás de um gateway que exige um cabeçalho, adicione-o em Cabeçalhos personalizados ao criar ou editar o endpoint, ou envie headers pela API. Os valores são criptografados no armazenamento e nunca retornados — um cabeçalho salvo volta só com o nome, e reenviar apenas esse nome mantém o valor guardado. Envie uma lista vazia para remover todos os cabeçalhos.
Mudar um endpoint para outro host desfaz esse reaproveitamento: os valores salvos precisam ser informados de novo, para que uma credencial nunca seja encaminhada a um destino para o qual não foi emitida. Mudar só o caminho os mantém.
Os cabeçalhos do próprio Sonny têm prioridade sobre os seus, então um cabeçalho personalizado nunca substitui sonny-signature, content-type nem os cabeçalhos de identificação da entrega. Sempre que possível, prefira verificar a assinatura: ela autentica cada payload, enquanto um token fixo só identifica quem chama.
{
"name": "Gateway",
"url": "https://api.example.com/hooks/sonny",
"events": ["conversation.created"],
"headers": [{ "name": "Authorization", "value": "Bearer …" }]
}Segurança e confiabilidade
- Corpo bruto assinado
- O HMAC-SHA256 cobre o timestamp Unix, um ponto e o corpo da requisição em UTF-8, sem alterações.
- Proteção contra reprodução
- Rejeite timestamps com mais de cinco minutos no passado ou no futuro, mesmo quando o HMAC for válido.
- Novas tentativas confiáveis
- Respostas fora da faixa 2xx são tentadas de novo após 1m, 5m, 30m, 2h, 6h. A tentativa 6 é a última.
Contrato da requisição
sonny-signature- t=<unix-seconds>,v1=<sha256-hex>
sonny-event- O tipo de evento, para um roteamento rápido.
sonny-delivery-id- Um ID estável para idempotência e suporte.
user-agent- Sonny-Webhooks/1.0
{
"id": "cm_event_id",
"type": "message.created",
"apiVersion": "2026-07-15",
"createdAt": "2026-07-15T12:00:00.000Z",
"data": {
"conversationId": "cm_conversation_id",
"messageId": "cm_message_id"
}
}Verifique a assinatura
Leia primeiro o corpo bruto. Fazer o parse do JSON e serializá-lo de novo muda os espaços em branco e faz uma assinatura válida falhar.
import { createHmac, timingSafeEqual } from "node:crypto";
const rawBody = await request.text(); // do not parse/re-serialize first
const signature = request.headers.get("sonny-signature");
const secret = process.env.SONNY_WEBHOOK_SECRET;
if (!signature || !secret) throw new Error("Missing webhook signature or secret");
const { t, v1 } = Object.fromEntries(signature.split(",").map(p => p.split("=")));
if (!/^\d+$/.test(t) || !/^[a-f0-9]{64}$/i.test(v1)) {
throw new Error("Malformed signature");
}
if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > 300) {
throw new Error("Stale webhook");
}
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`).digest("hex");
if (!timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(expected, "hex"))) {
throw new Error("Invalid signature");
}Catálogo de eventos
webhook.heartbeatEnviado a cada cinco minutos para os inscritos ativos (incluindo *), mesmo sem novas mensagens. Usa o caminho normal de entrega assinada e novas tentativas e ignora os filtros de conversa. Crie alertas para heartbeats ausentes ou atrasados; ele não procura conversas aguardando resposta.
{"intervalSeconds":300,"nextExpectedAt":"2026-09-29T16:05:00.000Z"}contact.createdUm contato foi criado.
{"contactId":"cm_contact_id"}contact.updatedUm contato foi atualizado ou mesclado.
{"contactId":"cm_contact_id"}contact.deletedUm contato foi arquivado ou mesclado com outro contato. Ele ainda pode ser lido com archived=true.
{"contactId":"cm_contact_id"}contact.erasedUm contato foi apagado permanentemente (por exemplo, em um pedido de exclusão pela LGPD/GDPR) com todas as suas conversas, mensagens e anexos. Ele não pode mais ser lido; exclua as cópias que você guardar.
{"contactId":"cm_contact_id"}conversation.createdUma conversa foi criada.
{"conversationId":"cm_conversation_id"}conversation.updatedUma conversa mudou.
{"conversationId":"cm_conversation_id"}conversation.closedUma conversa foi fechada.
{"conversationId":"cm_conversation_id"}conversation.deletedUma conversa foi movida para a lixeira e saiu da API pública.
{"conversationId":"cm_conversation_id"}message.createdUma mensagem ou nota interna foi criada. O contexto e uma breve prévia da mensagem são incluídos quando disponíveis; o texto das notas internas nunca é incluído.
{"conversationId":"cm_conversation_id","messageId":"cm_message_id","context":{"sourceId":"cm_source_id","sourceName":"Shopstar Go","storeName":"Shiney Store","inboxId":"cm_inbox_id","inboxName":"Go support","agentGroupId":"cm_group_id","agentGroupName":"Support","identityVerified":true,"status":"open","lastRepliedAt":"2026-09-25T09:00:00.000Z"},"message":{"id":"cm_message_id","type":"incoming","senderType":"human","senderId":null,"senderName":"Shiney","textPreview":"Could you check my bill?","textTruncated":false}}message.updatedUma resposta enviada foi editada na transcrição. Os e-mails já entregues não mudam.
{"conversationId":"cm_conversation_id","messageId":"cm_message_id"}tag.createdUma tag foi criada.
{"tagId":"cm_tag_id"}tag.updatedUma tag foi atualizada.
{"tagId":"cm_tag_id"}tag.deletedUma tag foi excluída.
{"tagId":"cm_tag_id"}member.invitedUm membro do espaço de trabalho foi convidado.
{"invitationId":"cm_invitation_id"}member.updatedA função ou o status de um membro mudou.
{"memberId":"cm_membership_id"}member.removedUm membro foi removido.
{"memberId":"cm_membership_id"}invitation.acceptedUm convite para o espaço de trabalho foi aceito.
{"invitationId":"cm_invitation_id","memberId":"cm_membership_id"}invitation.cancelledUm convite pendente para o espaço de trabalho foi cancelado.
{"invitationId":"cm_invitation_id"}conversation.activityMensagens de uma conversa, agrupadas em uma única entrega. Inclui um retrato de cada mensagem quando disponível. Enviado no lugar de message.created para endpoints com janela de agrupamento; não é possível se inscrever nele diretamente.
{"conversationId":"cm_conversation_id","messageIds":["cm_message_id","cm_other_message_id"]}webhook.testUm evento de teste solicitado por um administrador.
{"message":"This is a test webhook from Sonny."}
Comportamento das entregas
- Retorne qualquer status 2xx em até 10 segundos para marcar a entrega como bem-sucedida.
- Mantenha os handlers idempotentes. Use o
iddo evento ousonny-delivery-idpara ignorar duplicatas. - Redirecionamentos não são seguidos. Em vez disso, atualize a URL do endpoint no Sonny.
- Os corpos de resposta têm limite, e só os primeiros 4 KiB são guardados para diagnóstico.
- Selecione Testar para enviar um evento
webhook.test. Selecione Entregas para ver os 50 eventos mais recentes. Quando uma entrega falha de vez, selecione Tentar novamente para enviá-la de novo.
Detecte um feed silencioso
Adicione webhook.heartbeat aos eventos do seu endpoint nas configurações de Desenvolvedor ou com update_webhook. Assinantes ativos (incluindo *) recebem um heartbeat assinado a cada cinco minutos, mesmo quando não chegam mensagens novas. Os heartbeats ignoram filtros de canal, equipe e mensagem e não contêm dados de conversas. Eles usam o mesmo caminho de entrega e as mesmas novas tentativas que as mensagens. Confira createdAt e nextExpectedAt para que uma nova tentativa antiga não pareça um heartbeat recente; considere atrasos de consulta e de rede antes de disparar um alerta. Um acúmulo de entregas pode atrasar ou suprimir heartbeats.
Use list_webhook_deliveries com webhooks:read para ver tentativas e falhas. get_status verifica a conexão com o banco de dados, não a entrega de webhooks. Agende, de forma independente, list_conversations com status=open, awaitingReply=true, sort=waitingSince e direction=asc para encontrar conversas antigas sem resposta mesmo enquanto o feed está quieto.
Ative Payloads compactos nas configurações de Desenvolvedor ou defina compact=true no endpoint para omitir os rótulos de contexto, mantendo os IDs de roteamento, o remetente, o horário e prévias de mensagens de 200 caracteres. Isso também vale para entregas agrupadas; o texto das notas internas continua excluído. Os payloads padrão não mudam, exceto pelo horário da mensagem adicionado. Edite os escopos da sua chave de API existente para conceder webhooks:read e contacts:read, para os logs de entrega e as consultas diretas de contatos. Os dois escopos exigem uma chave com todos os canais e acesso de membro a todos os canais; não é possível ampliar uma chave restrita apenas adicionando permissões.
Documentos relacionados
- API públicaBeta
Faça a triagem e sincronize conversas, compartilhe o contexto dos clientes, leia relatórios e envie arquivos com chaves de API de escopo limitado.
- Caixa de entrada
Entenda status, prioridades, atribuição, adiamento, ações em massa e atalhos de teclado.
- Contatos
Saiba como os contatos são criados, gerenciados, marcados com tags e mesclados no Sonny.
- Equipe e funções
Convide usuários, crie equipes e entenda os acessos de proprietário, administrador, agente e leitor.