Pular para o conteúdo

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

EventoQuando
contact.createdPrimeira 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_changedname mudou em relação ao último nome conhecido
contact.picture_changedO 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_messagedA 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.warningCruzou o limiar de 80, 90 ou 95% da cota de contatos do ciclo
contact.quota.exceededTentativa 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 em phone_number com 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 id aqui é o sujeito do evento. O contact_id que aparece no objeto from/to de uma mensagem é diferente: referencia o contato da contraparte e mantém a chave contact_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 (em data): 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/quota são medidos contra o ciclo de faturamento avaliado da organização, representado por cycle_start/cycle_end (ISO 8601 UTC). resets_at é igual a cycle_end.

O number_alias fica no envelope, nunca dentro de data. Entre os eventos de conta, o campo é omitido em storage.* e em media.unavailable (os eventos sem um número único). Toda família *.quota.* (tanto contact.quota.* quanto message.quota.*, por número) carrega number_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.