Pular para o conteúdo

Webhooks

Hoje o Canverly entrega um evento por webhook: lead.created, disparado a cada envio aceito de um formulário. Os webhooks são configurados no painel do site, na área do aplicativo Formulários.

  1. Informe a URL de destino. Tem de ser https, sem usuário e senha na URL e sem porta de serviço. A URL é validada ao salvar e de novo a cada entrega.
  2. Escolha o escopo. Um formulário específico, ou todos os formulários do site.
  3. Guarde o segredo. O segredo de assinatura é gerado pela plataforma e aparece uma única vez, na criação. Para trocá-lo, apague o webhook e crie outro.

Se o seu destino exige autenticação própria, dá para cadastrar um cabeçalho (nome e valor). O valor é guardado cifrado e nunca é devolvido depois. Nomes com o prefixo x-canverly- são recusados, porque esse prefixo carrega a assinatura.

Um POST com corpo JSON e estes cabeçalhos:

Cabeçalho Conteúdo
x-canverly-signature t=<unix>,v1=<hmac>: a assinatura (veja abaixo).
x-canverly-timestamp O mesmo instante, em claro, só para diagnóstico.
x-canverly-event O evento, hoje sempre lead.created.
x-canverly-delivery Identificador da entrega, para deduplicar.

Sem mapeamento configurado, o corpo é o evento com os campos event, site_id, form_id, form_slug, lead_id, created_at (RFC 3339), data (os campos preenchidos) e meta (página de origem, UTM e dados técnicos do envio). Com mapeamento, o corpo tem a forma que o dono desenhou no painel, e a pré-visualização do painel mostra o JSON exato que vai sair.

A assinatura é um HMAC-SHA256, em hexadecimal (64 caracteres), calculado com o segredo sobre o texto <t>.<corpo>: o valor de t, um ponto e o corpo cru, byte a byte, como chegou.

import crypto from 'node:crypto';
function assinaturaValida(cabecalho, corpoCru, segredo, toleranciaSeg = 300) {
const partes = Object.fromEntries(cabecalho.split(',').map((p) => p.split('=')));
const t = Number(partes.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranciaSeg) return false;
const esperado = crypto.createHmac('sha256', segredo).update(`${t}.${corpoCru}`).digest('hex');
const recebido = String(partes.v1 ?? '');
return recebido.length === esperado.length &&
crypto.timingSafeEqual(Buffer.from(recebido), Buffer.from(esperado));
}

O prefixo v1= existe para uma futura troca de algoritmo: durante a transição, o destinatário poderá aceitar as duas versões.

  • A entrega é pelo menos uma vez: uma resposta perdida faz o mesmo lead chegar de novo. Use o x-canverly-delivery para descartar repetições.
  • Se o destino falhar, a plataforma tenta de novo com espera crescente (30 s, 1 min, 2 min, 4 min…, com teto de 6 horas entre tentativas), até 10 tentativas, cerca de 8,5 horas no total.
  • Depois da última, a entrega fica marcada como falha e continua visível no painel.