Skip to content
Vai integrar o webhook da campanha (recebimento server-to-server) com ajuda de IA? Copie a instrução que aponta o modelo para esta documentação.
Ver a instrução
Você vai integrar o webhook da campanha (recebimento server-to-server). Use a documentação oficial como fonte da verdade.

- Página desta integração: https://developers.zarv.com/sdk/webhooks
- Guia do Smartflow (o lado browser): https://developers.zarv.com/sdk/smartflow
- Índice da documentação em markdown: https://developers.zarv.com/llms.txt
- Documentação completa em um arquivo: https://developers.zarv.com/llms-full.txt

Isto é um endpoint HTTP que EU exponho e a Zarv chama — não é uma API que eu consumo. Verifique a assinatura HMAC-SHA256 sobre o corpo CRU da requisição (antes de qualquer parse de JSON) e compare em tempo constante. A assinatura não tem timestamp, então use o header X-Zarv-Delivery como chave de idempotência. Responda 2xx rápido e processe de forma assíncrona: o timeout de entrega é 10s.

Antes de escrever qualquer código, leia a página desta integração. Siga exatamente os nomes de campos, endpoints, callbacks e formatos que estiverem documentados — não invente parâmetros nem endpoints, e não deduza comportamento a partir de outras APIs que você conhece. Se algo de que você precisa não estiver na documentação, diga que não está em vez de supor.

Minha tarefa: [ex.: receber os eventos da campanha e liberar o pedido quando o cadastro for aprovado]

Webhook da campanha

O webhook é o canal server-to-server da campanha: a Zarv entrega, por HTTPS assinado, cada etapa concluída e a decisão final de um cadastro.

Ele existe porque os callbacks do navegador não são confiáveis para decisão de negócio. O onDecision do SDK chega por postMessage de um iframe, sem assinatura — qualquer pessoa com o console aberto forja um approved. Use os callbacks para mover a interface; use o webhook para liberar acesso, aprovar uma locação, criar um contrato.

Há ainda um segundo motivo, mais prosaico: a decisão pode nunca chegar ao navegador. Ela vem de um polling que roda dentro do iframe, então se o visitante fecha a aba — ou se você desmonta o embed assim que a última etapa termina — o onDecision simplesmente não dispara. O webhook chega de qualquer forma.

Assinatura da campanha

Cada campanha tem suas próprias assinaturas de webhook, configuradas no admin Zarv. Uma assinatura define:

CampoDescrição
urlSeu endpoint. HTTPS obrigatório; endereços privados/internos são recusados.
eventsLista de eventos que você quer. Vazio = todos.
includeLeadDatafalse por padrão. Ligado, os eventos terminais carregam um bloco lead com PII.
includeFilesfalse por padrão. Ligado, os eventos terminais carregam files[] com URLs assinadas.

O signingSecret é exibido uma única vez, na criação e a cada rotação — depois disso só os últimos 4 caracteres são legíveis. Guarde-o no seu cofre de segredos na hora em que aparecer.

Ligue includeLeadData e includeFiles apenas se o seu endpoint estiver adequadamente protegido: eles transportam dados pessoais e links para os documentos capturados.

Eventos

Por etapa — emitidos conforme o visitante conclui cada tela:

EventoEtapa
lead.basic.completedDados básicos
lead.email.completedE-mail verificado
lead.phone.completedTelefone verificado
lead.address.completedEndereço
lead.custom.completedCampos personalizados
lead.cnh.completedCNH enviada
lead.face.completedFacial / liveness

Para quem monta as telas isoladas, esses eventos são o equivalente server-side do onComplete de cada tela — e, ao contrário dele, confiáveis.

Terminais — emitidos quando o cadastro chega a um estado final:

EventoSignificado
lead.submittedCadastro enviado
lead.approvedAprovado
lead.rejectedReprovado
lead.completedO cadastro chegou ao fim do funnel

Os blocos lead e files só aparecem em eventos terminais, e só com o respectivo toggle ligado.

A decisão pode chegar em duas fases

Nem todo cadastro é decidido na hora. Quando a análise automática não conclui — ou quando a campanha está com o autopilot desligado, caso em que nenhum cadastro é decidido automaticamente —, o lead vai para revisão humana, e a decisão chega depois, num segundo evento.

DesfechoEventos, em ordem
Aprovado automaticamentelead.approvedlead.completed
Reprovado automaticamentelead.rejectedlead.completed
Enviado para revisão humanalead.completed(mais tarde) lead.approved ou lead.rejected

Duas consequências práticas, e é aqui que integrações costumam errar:

  • lead.completed sozinho não quer dizer aprovado. Se ele chegar sem um lead.approved/lead.rejected antes, o cadastro está pendente de análise humana — trate como "em análise", não como desfecho. O campo approved vem ausente exatamente nesse caso.
  • A decisão humana não repete o lead.completed. Ela chega sozinha, minutos ou dias depois. Se o seu handler só reage a lead.completed, você nunca verá o resultado dessas análises.

Por isso o gatilho de negócio deve ser lead.approved / lead.rejected, e não lead.completed.

Correlacionar com o seu registro

O payload traz o externalId — a sua própria referência, anexada ao lead pelo identify do SDK:

js
Zarv("smartflow", {
  action: "identify",
  leadId,
  externalId: "PEDIDO-00042",
});

É por ele que o seu backend reconhece de quem é o webhook, sem precisar confiar em nada que veio do navegador. Anexe o externalId logo na primeira tela, dentro do onSession.

Reconciliação: consultar pelo seu identificador

O webhook é o canal de push. Para o pull — reprocessar uma entrega que o seu endpoint perdeu, conferir um lote, ou responder "e aquele pedido, deu no quê?" — consulte o lead pelo mesmo externalId:

sh
curl -G "https://services.zarv.com/api/v1/signup/leads" \
  --data-urlencode "externalId=PEDIDO-00042" \
  -H "Authorization: Bearer <accessToken>"

A autenticação é a mesma OAuth2 do restante da API, e o token precisa da role id-list.

A busca é uma igualdade exata: o externalId é uma string opaca sua, que a Zarv nunca normaliza — não há match por prefixo, por parte, nem case-insensitive. Mandar o valor vazio não filtra nada (devolve a lista inteira), em vez de devolver os leads sem referência.

Trate isso como rede de segurança, não como substituto do webhook: consultar em laço é mais lento, mais caro e sempre atrás do evento.

Cabeçalhos de entrega

User-Agent:       zarv-smartflow-webhook/2
X-Zarv-Event:     <nome-do-evento>
X-Zarv-Delivery:  <uuid-da-entrega>
X-Zarv-Attempt:   <número-da-tentativa, começando em 1>
X-Zarv-Signature: sha256=<hmac-hex>

Verificar a assinatura

O HMAC-SHA256 é calculado sobre o corpo cru da requisição, antes de qualquer parse de JSON. Se o seu framework já desserializou o corpo, você precisa do buffer original — reserializar não reproduz os mesmos bytes.

go
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)

func verifyWebhookSignature(secret, rawBody []byte, sigHeader string) bool {
    mac := hmac.New(sha256.New, secret)
    mac.Write(rawBody)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(sigHeader))
}
js
import crypto from "node:crypto";

function verifyWebhookSignature(secret, rawBody, sigHeader) {
  const mac = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const expected = `sha256=${mac}`;
  const a = Buffer.from(expected);
  const b = Buffer.from(sigHeader ?? "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express: capture o corpo cru antes do parser de JSON.
app.use("/webhooks/zarv", express.raw({ type: "application/json" }));

Compare sempre em tempo constante (hmac.Equal / timingSafeEqual), nunca com ==.

A assinatura não inclui timestamp, então não há janela de replay embutida. Trate o X-Zarv-Delivery como chave de idempotência: registre os ids já processados e ignore repetições.

Payloads

Fino (todos os eventos):

json
{
  "event": "lead.phone.completed",
  "leadId": "lead_abc",
  "campaignId": "camp_xyz",
  "workspaceId": "ws_123",
  "externalId": "PEDIDO-00042",
  "step": "phone",
  "status": "pending",
  "score": 0,
  "occurredAt": "2026-06-11T12:00:00Z"
}

Terminal enriquecido (includeLeadData e includeFiles ligados):

json
{
  "event": "lead.approved",
  "leadId": "lead_abc",
  "campaignId": "camp_xyz",
  "workspaceId": "ws_123",
  "externalId": "PEDIDO-00042",
  "status": "approved",
  "approved": true,
  "score": 80,
  "occurredAt": "2026-06-11T12:00:00Z",
  "lead": {
    "nationalId": "12345678901",
    "fullName": "Jane Doe",
    "birthDate": "1990-01-01",
    "email": "[email protected]",
    "phone": "+5511900000000",
    "phoneIsWhatsApp": true,
    "address": { "street": "Rua Exemplo", "number": "42", "zip": "01310-100", "city": "São Paulo", "state": "SP", "country": "BR" },
    "customFields": [{ "id": "field_1", "type": "text", "label": "Empresa", "value": "Acme" }],
    "metadata": { "utm_source": "google" }
  },
  "files": [
    { "type": "selfie", "fileName": "selfie.jpeg", "contentType": "image/jpeg", "url": "https://storage.googleapis.com/...", "expiresAt": "2026-06-12T12:00:00Z" },
    { "type": "cnh", "fileName": "cnh.jpeg", "contentType": "image/jpeg", "url": "https://storage.googleapis.com/...", "expiresAt": "2026-06-12T12:00:00Z" }
  ]
}

Sobre files[]:

  • Tipos: selfie, cnh, liveness-smile, liveness-blink, liveness-turn-left, liveness-turn-right, profile, e custom:<fieldId> para campos personalizados do tipo arquivo.
  • As URLs assinadas do GCS expiram em 24h (expiresAt) e são geradas a cada tentativa — uma retentativa sempre entrega links válidos. Baixe os arquivos ao receber; não guarde a URL.
  • O profile usa URL pública estável de CDN e não tem expiresAt.
  • score está sempre presente, inclusive quando é 0.

Retentativas

Resposta suaO que acontece
2xxEntrega confirmada; o contador de falhas zera.
408, 429, 5xxRetentativa, até 6 tentativas, com backoff crescente.
Outros 4xxFalha permanente, sem retentativa.
3xxRedirecionamentos são recusados (falha permanente).

Após 30 falhas consecutivas a assinatura é desativada automaticamente. Reative-a no admin depois de corrigir o endpoint.

Boas práticas

  • Dispare o seu efeito de negócio em lead.approved / lead.rejected, nunca em lead.completed — veja A decisão pode chegar em duas fases.
  • Responda 2xx rápido e processe de forma assíncrona: o timeout de entrega é de 10 segundos.
  • Implemente idempotência por X-Zarv-Delivery — retentativas e entregas duplicadas acontecem.
  • Verifique a assinatura antes de fazer qualquer parse ou side effect.
  • Guarde os payloads recebidos para auditoria.
  • Seu endpoint precisa ser público na internet: endereços privados são recusados no momento da entrega.