Eventos de contato
Lista dos eventos de webhook que o Turbo Notify envia quando algo muda em um contato. Os eventos contact.* são entregues à URL de webhook configurada no dashboard, junto com os demais eventos: veja Webhook: configuração e envelope para o passo a passo, a validação de assinatura e o formato do envelope.
Tabela
| Evento | Quando |
|---|---|
contact.created | Primeira observação do trio (organização, número, telefone): a identidade do contato é por número, então o mesmo telefone alcançando o main e um número extra gera dois eventos contact.created, um por número (veja Contatos: visão geral) |
contact.name_changed | name mudou em relação ao último nome conhecido |
contact.picture_changed | O WhatsApp avisou que a foto mudou, ou uma interação com o contato detectou uma foto nova, e ela foi armazenada no seu BYO Storage |
contact.first_messaged | A primeira mensagem enviada por este número via Turbo Notify chegou ao contato no WhatsApp. Dispara uma vez por contato e número (sinal de aquisição de novo contato; a cota é consumida por contato contactado no ciclo de faturamento avaliado, não por este evento) |
contact.quota.warning | Cruzou o limiar de 80, 90 ou 95% da cota de contatos do ciclo |
contact.quota.exceeded | Tentativa rejeitada por estouro (HTTP 402) |
A estrutura do data: trio do contato + campos padrão de contexto
O data de um evento contact.* espelha o data de
message.*: o trio do contato {id, name, number} mais os campos padrão
de contexto (direction, channel, origin). O id é o id público do
próprio contato (ct_{hex32}), igual ao data.id de message.*.
{ "id": "ct_a1b2c3d4...", "name": "João Silva", "number": "5511988887777", "direction": "inbound", "channel": "whatsapp", "origin": "tn"}id: id público do contato (ct_{hex32}), o sujeito do evento.name: nome de exibição (push name) do contato no WhatsApp.number: telefone completo em dígitos, sem o+(ex.:5511988887777). Nos endpoints REST de contato, o mesmo número vem emphone_numbercom o+.direction: sempre"inbound"(o contato é conhecido pelo canal do WhatsApp). Ausente nos eventos de cota.channel: sempre"whatsapp".origin: sempre"tn"(quem registrou o contato foi o Turbo Notify).
O
idaqui é o sujeito do evento. Ocontact_idque aparece no objetofrom/tode uma mensagem é diferente: referencia o contato da contraparte e mantém a chavecontact_id.
O número aparece completo, como no from/to de toda mensagem. O name (push name)
é dado pessoal: o Turbo Notify não o grava em logs, e o que você fizer com ele depois
de recebê-lo pelo webhook (inclusive guardá-lo) é responsabilidade sua.
created_at vs at
created_at(envelope): momento de emissão do evento pelo Turbo Notify.at(emdata): momento da ação real no WhatsApp.
São valores distintos. Não carregam at (apenas o created_at do envelope)
os eventos gerados pelo próprio Turbo Notify (contact.created, contact.quota.warning,
contact.quota.exceeded) e também contact.picture_changed (a foto nova é percebida
pelo Turbo Notify, sem um momento único de ação no WhatsApp). Carregam at os eventos
de ação real contact.name_changed e contact.first_messaged.
Todo evento contact.* carrega channel: "whatsapp" e origin: "tn". Os
eventos de ciclo de vida do contato carregam direction: "inbound"; os eventos
de cota não carregam direction.
number_alias no envelope
Todos os eventos contact.*, tanto os de ciclo de vida (contact.created,
contact.name_changed, contact.picture_changed, contact.first_messaged)
quanto os de cota (contact.quota.warning, contact.quota.exceeded), carregam,
logo abaixo de organization_id, o campo number_alias: o identificador do número do
Turbo Notify ao qual o contato (ou a cota de contatos) pertence, "main" para o
número principal ou o alias de um número extra. Use-o para levar o evento à
caixa de entrada certa.
A cota de contatos é por número: cada número tem
sua própria contagem de contatos distintos, então os eventos
contact.quota.warning/contact.quota.exceeded identificam o número afetado via
number_alias no envelope. O data do evento de cota carrega os campos
threshold_percent (só em warning), used, quota, cycle_start, cycle_end,
resets_at, tier, channel, origin.
used/quotasão medidos contra o ciclo de faturamento avaliado da organização, representado porcycle_start/cycle_end(ISO 8601 UTC).resets_até igual acycle_end.
O
number_aliasfica no envelope, nunca dentro dedata. Entre os eventos de conta, o campo é omitido emstorage.*e emmedia.unavailable(os eventos sem um número único). Toda família*.quota.*(tantocontact.quota.*quantomessage.quota.*, por número) carreganumber_alias.
Exemplo: contact.created
Gerado pelo Turbo Notify: sem at e sem source.
{ "id": "evt_36cd28f516fd65fc58d84081cfa17dcb", "type": "contact.created", "organization_id": "org_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "number_alias": "main", "number_id": "num_0a1b2c3d4e5f60718293a4b5c6d7e8f9", "created_at": "2026-05-15T12:00:00Z", "data": { "id": "ct_a1b2...", "name": "João Silva", "number": "5511988887777", "direction": "inbound", "channel": "whatsapp", "origin": "tn" }}Exemplo: contact.name_changed
name é o novo push name; old_name é o anterior.
{ "id": "evt_...", "type": "contact.name_changed", "organization_id": "org_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "number_alias": "main", "number_id": "num_0a1b2c3d4e5f60718293a4b5c6d7e8f9", "created_at": "2026-05-15T12:00:03Z", "data": { "id": "ct_a1b2...", "name": "João S.", "number": "5511988887777", "old_name": "João Silva", "direction": "inbound", "channel": "whatsapp", "origin": "tn", "at": "2026-05-15T12:00:00Z" }}created_at (emissão pelo Turbo Notify) é distinto de data.at (momento da ação no
WhatsApp): aqui o Turbo Notify emitiu o evento 3 s após a alteração do nome.
Exemplo: contact.picture_changed
{ "id": "evt_...", "type": "contact.picture_changed", "organization_id": "org_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "number_alias": "main", "number_id": "num_0a1b2c3d4e5f60718293a4b5c6d7e8f9", "created_at": "2026-05-15T12:00:02Z", "data": { "id": "ct_a1b2...", "name": "João Silva", "number": "5511988887777", "picture": { "kind": "available", "url": "https://<bucket>.r2.cloudflarestorage.com/contacts/.../picture.jpg?...", "url_expires_at": "2026-05-15T12:15:00Z", "hash": "abc123..." }, "direction": "inbound", "channel": "whatsapp", "origin": "tn" }}Este evento não carrega data.at (use o created_at do envelope para saber
quando ele foi emitido) nem picture.fetched_at: a validade da URL já é
informada por picture.url_expires_at. O fetched_at existe apenas na leitura do contato
(GET /v1/numbers/{ref}/contacts/{contact_id}), nunca
neste webhook.
O bloco picture segue o mesmo ciclo de vida da mídia de mensagem: quando a
foto está armazenada no seu bucket ela vem como kind: "available" com
uma URL pré-assinada e com validade limitada (url_expires_at, ~15 min),
nunca uma URL do WhatsApp. Quando a URL expirar, busque a foto de novo pelo endpoint GET. O evento dispara apenas quando a foto chega ao seu bucket, inclusive
quando o contato foi observado sem foto e ela fica disponível depois (após
habilitar o BYO Storage, na próxima interação com o contato). Para ler a foto sob demanda use
GET /v1/numbers/{ref}/contacts/{contact_id}/profile-picture,
onde {ref} é o number_id recebido no envelope deste evento (não muda) ou o alias atual do número.
Exemplo: contact.first_messaged
{ "id": "evt_...", "type": "contact.first_messaged", "organization_id": "org_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "number_alias": "main", "number_id": "num_0a1b2c3d4e5f60718293a4b5c6d7e8f9", "created_at": "2026-05-15T12:00:01Z", "data": { "id": "ct_...", "name": "João Silva", "number": "5511988887777", "tier": "unknown", "direction": "inbound", "channel": "whatsapp", "origin": "tn", "at": "2026-05-15T12:00:00Z" }}O evento só é enviado quando a mensagem chega ao WhatsApp: um envio aceito que
falha antes disso (por exemplo com number_not_connected) não dispara
contact.first_messaged, e o próximo envio que chegar dispara. data.at é o
momento em que essa primeira mensagem chegou ao WhatsApp e corresponde ao
first_messaged_at do contato.
O campo tier não é populado neste evento: contact.first_messaged sempre
emite "unknown". Não use este campo para inferir o plano da organização: o
tier real é entregue nos eventos contact.quota.warning/contact.quota.exceeded.
Exemplo: contact.quota.warning
{ "id": "evt_...", "type": "contact.quota.warning", "organization_id": "org_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "number_alias": "main", "number_id": "num_0a1b2c3d4e5f60718293a4b5c6d7e8f9", "created_at": "2026-06-20T12:00:00Z", "data": { "threshold_percent": 90, "used": 90, "quota": 100, "cycle_start": "2026-06-01T00:00:00Z", "cycle_end": "2026-07-01T00:00:00Z", "resets_at": "2026-07-01T00:00:00Z", "tier": "SOLO", "channel": "whatsapp", "origin": "tn" }}threshold_percent é um inteiro com o percentual atingido: 80, 90 ou 95 (80 quer dizer 80%). O esgotamento da cota
não vem com threshold_percent: 100: ele é sinalizado pelo evento separado
contact.quota.exceeded, que não possui o campo threshold_percent. Código
que ramifica em threshold_percent === 100 nunca roda e deixa passar o sinal
real de esgotamento.
Este evento é gerado pelo Turbo Notify: carrega channel e origin, sem
direction (não é uma troca de mensagens) e sem at. cycle_start e
cycle_end são o ciclo de faturamento avaliado contra o qual used e quota
foram medidos. resets_at é o momento em que a contagem da cota recomeça (igual
a cycle_end), não o momento de uma ação.
Exemplo: contact.quota.exceeded
{ "id": "evt_...", "type": "contact.quota.exceeded", "organization_id": "org_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "number_alias": "main", "number_id": "num_0a1b2c3d4e5f60718293a4b5c6d7e8f9", "created_at": "2026-06-20T12:00:00Z", "data": { "used": 100, "quota": 100, "cycle_start": "2026-06-01T00:00:00Z", "cycle_end": "2026-07-01T00:00:00Z", "resets_at": "2026-07-01T00:00:00Z", "tier": "SOLO", "channel": "whatsapp", "origin": "tn" }}Gerado pelo Turbo Notify: carrega channel/origin mas sem direction, sem at e
sem request_id em data; resets_at é mantido.
Idempotência
Cada evento carrega id único (evt_…). Use id como chave de deduplicação no seu lado.