Guia · Webhooks

Webhooks assinados, com retry.

Receba eventos de play, retenção, CTA e intenção em qualquer endpoint HTTPS. Assinatura HMAC-SHA256 sobre {timestamp}.{rawBody}, retry com backoff e logs no painel.

Cadastrar e gerar secret

Em Integrações → Webhooks → Novo webhook, informe nome, URL HTTPS e (opcionalmente) os eventos que deseja receber. O LeadPlayer gera automaticamente um secret forte no formato whsec_… usando um RNG criptográfico do servidor. O valor completo é mostrado apenas no momento da criação ou regeneração — depois disso, o painel exibe somente um prefixo e os últimos caracteres.

Para integrações externas (LeadsPulse, n8n, Make, Zapier, WhatsApp), preencha o campo webhook_secret no painel da integração. Sem secret configurado, o LeadPlayer não envia eventos para aquele destino — isso é registrado como security_event (webhook_missing_secret ou integration_missing_webhook_secret) sem vazar URL completa nem secret.

Eventos disponíveis

EventoDescriçãoStatus
player_loadedPlayer montou no DOM e está pronto para tocar.ativo
video_viewSessão inicial registrada (visitante carregou o vídeo).ativo
video_playPrimeiro play da sessão.ativo
video_pausePausa manual.ativo
video_resumeRetomada após pause.ativo
video_unmuteVisitante destravou o áudio (sinal forte de intenção).ativo
watched_25_percentAtingiu 25% do tempo total assistido.ativo
watched_50_percentAtingiu 50%.ativo
watched_75_percentAtingiu 75%.ativo
watched_90_percentAtingiu 90% (considerado complete).ativo
cta_shownUm CTA com delay apareceu para o visitante.ativo
cta_clickedVisitante clicou no CTA.ativo
session_endedSessão encerrada (unload, navegação ou heartbeat expirado).ativo
reached_pitchVisitante chegou ao trecho de oferta configurado.em breve
high_intent_leadLead Intent Score cruzou o limiar configurado.em breve
conversionConversão server-side confirmada via /api/public/conversion.em breve
abandoned_before_pitchSaiu antes do pitch.em breve
abandoned_after_offerSaiu logo após a oferta.em breve

Headers e formato da requisição

  • X-Player-Event — nome do evento (ex.: cta_clicked).
  • X-Player-Signature — HMAC-SHA256 em hex (sem prefixo).
  • X-Player-Timestamp — Unix epoch em segundos no momento da assinatura.
  • X-Player-Delivery-Id — UUID estável da entrega; use para idempotência.
  • Content-Type: application/json.
POST /seu-endpoint HTTP/1.1
Host: api.exemplo.com
Content-Type: application/json
X-Player-Event: cta_clicked
X-Player-Signature: 4a9f3c... (hex)
X-Player-Timestamp: 1751155200
X-Player-Delivery-Id: 9b1c8d3e-4f5a-4b2d-9c10-3a1b2c3d4e5f

{
  "tenant_id": "uuid",
  "video_id": "uuid",
  "session_id": "ses_xxx",
  "visitor_id": "vis_xxx",
  "event_name": "cta_clicked",
  "current_time": 252,
  "duration": 480,
  "watched_percentage": 52.5,
  "utm_source": "meta",
  "utm_campaign": "vsl-01",
  "ctwa_clid": "...",
  "external_lead_id": "...",
  "created_at": "2026-06-28T00:00:00.000Z"
}

Verificar a assinatura (Node/TypeScript)

A base da assinatura é a string ${timestamp}.${rawBody}. Use sempre o corpo bruto (string como veio na requisição); qualquer reformatação por JSON.parse + JSON.stringify muda a assinatura. Compare com timingSafeEqual e rejeite timestamps fora de uma janela de 5 minutos para mitigar replay attacks.

import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 5 * 60;

export function verifyLeadPlayerWebhook(opts: {
  rawBody: string;
  signatureHeader: string | null;
  timestampHeader: string | null;
  secret: string;
}): boolean {
  const { rawBody, signatureHeader, timestampHeader, secret } = opts;
  if (!signatureHeader || !timestampHeader) return false;

  const ts = Number(timestampHeader);
  if (!Number.isFinite(ts)) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - ts) > TOLERANCE_SECONDS) return false;

  const expected = createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(signatureHeader, "hex");
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

Política de retry

Considera-se sucesso qualquer resposta HTTP 2xx em até 10 s. Qualquer outro status, timeout ou erro de rede é tratado como falha.

  • Tentativa 1 — imediata, no momento do evento.
  • Tentativa 2 — após 1 minuto.
  • Tentativa 3 — após 5 minutos.
  • Tentativa 4 — após 30 minutos.
  • Após 4 falhas a entrega vai para status = failed e fica disponível para reprocesso manual em Integrações → Logs.

Boas práticas no receiver

  • Responder 2xx rápido (ack) e processar em fila assíncrona.
  • Persistir X-Player-Delivery-Id para garantir idempotência — eventos podem repetir após retry.
  • Validar a assinatura antes de qualquer side-effect.
  • Rejeitar requisições sem X-Player-Signature ou com timestamp fora da janela.
  • Não confiar em campos sensíveis (tenant_id, valores monetários) sem cruzar com seu próprio banco.
  • Manter o secret apenas em variável de ambiente do receiver — nunca no front-end.

Perguntas frequentes

Quais eventos são entregues hoje?

player_loaded, video_view, video_play, video_pause, video_resume, video_unmute, watched_25_percent, watched_50_percent, watched_75_percent, watched_90_percent, cta_shown, cta_clicked, session_ended. Eventos sinalizados como em breve (reached_pitch, high_intent_lead, conversion, abandoned_before_pitch, abandoned_after_offer) já existem como destinos válidos mas ainda não são disparados automaticamente.

Qual a política de retry?

Até 4 tentativas por entrega. Primeira é imediata, depois 1 min, 5 min e 30 min. Considera-se sucesso qualquer 2xx. Acima de 4 falhas a entrega entra em status failed e aparece em Integrações → Logs para reprocessamento manual.

Como o LeadPlayer assina o payload?

HMAC-SHA256 sobre a string `${timestamp}.${rawBody}`, usando o secret do endpoint (ou do external_integrations.config.webhook_secret). A assinatura vai em hex no header X-Player-Signature, e o timestamp Unix em X-Player-Timestamp. Recomendamos rejeitar requisições com timestamp fora de uma janela de 5 minutos para mitigar replay.

O que acontece se um destino estiver sem secret?

O LeadPlayer NÃO envia o evento. Não existe secret padrão compartilhado. A tentativa fica registrada como security_event (webhook_missing_secret ou integration_missing_webhook_secret) com tenant_id, endpoint_id e o host de destino — sem URL completa nem secret.

Posso testar webhook em desenvolvimento?

Sim. Use ngrok ou Cloudflare Tunnel para expor seu endpoint e cadastre a URL em Integrações → Webhooks. Existe um botão Testar que enfileira um evento player_loaded contra o destino.

Pronto para testar no seu vídeo?

Plano grátis sem cartão. Hospede um vídeo, embede no seu site e veja a retenção em minutos. Player sem marca a partir do Pro.