Conector para formulário próprio

Guia da integração implementada para enviar leads de um formulário hospedado em uma landing page externa à plataforma Gestão de Tráfego.

Resposta rápida: configurar apenas a URL não basta

Atualmente, as configurações de canal/campanha Ads não oferecem um campo de URL que conecte automaticamente uma landing page ao recebimento de leads. O conector também não visita nem lê a página indicada por uma URL.

É necessário configurar a integração no backend da landing page (ou em uma função serverless): ao receber o formulário, esse backend envia os dados para POST /integracoes/leads, assinando a requisição com o segredo HMAC. Um formulário estático com apenas HTML/JavaScript não pode fazer essa assinatura com segurança, pois o segredo ficaria exposto no navegador. A chamada server-to-server também evita depender de CORS entre os domínios.

O backend deve enviar o campanha_id de uma campanha já existente na plataforma. Não é o ID do canal Ads nem a URL da landing page.

Fluxo

Visitante envia formulário
    → backend da landing page valida os dados
    → backend monta e assina o JSON com FORMULARIO_SECRET
    → POST /integracoes/leads
    → plataforma valida JSON, timestamp e HMAC
    → webhook_eventos + fila "webhooks"
    → ProcessarWebhookJob resolve campanha/cliente/usuário e cria o lead

O endpoint responde 202 Accepted quando o evento for recebido e enfileirado.
O processamento do lead é assíncrono; consulte /admin/webhooks para acompanhar o resultado. Um 202 não garante que o job posterior terminou com sucesso.

Configuração

  1. Configure um segredo aleatório forte no .env da plataforma:

    FORMULARIO_SECRET=use-um-segredo-aleatorio-forte
    FORMULARIO_TIMESTAMP_TOLERANCE=300
    WEBHOOK_ALLOW_UNSIGNED=false
    
  2. Configure o mesmo valor de FORMULARIO_SECRET como variável de ambiente privada no backend da landing page. Não o coloque em HTML, JavaScript, repositório público ou parâmetros da URL.

  3. Descubra o campanha_id da campanha existente que receberá esses leads e configure-o no backend da landing page. A plataforma deriva da campanha o cliente e o usuário proprietários.

  4. Configure o backend para enviar o POST para a URL completa da aplicação, terminada em /integracoes/leads (por exemplo, https://app.exemplo.com.br/gestao-trafego/integracoes/leads, conforme a instalação e o URL_BASE).

  5. Garanta que um worker da fila webhooks esteja ativo em produção. Veja
    WEBHOOKS.md.

Não é necessário configurar a URL da landing page no canal Ads para que o endpoint receba os envios.

Contrato HTTP

POST /integracoes/leads
Content-Type: application/json
X-Formulario-Signature: sha256=<hmac-sha256-em-hex>

A assinatura é sha256= seguido do HMAC-SHA256 do corpo HTTP bruto usando FORMULARIO_SECRET. O corpo enviado deve ser exatamente o mesmo corpo usado para gerar a assinatura; não o reserialize entre a assinatura e o envio.

O JSON exige os campos abaixo:

Campo Tipo Descrição
event_type string Tipo do evento; use lead_form_submit
id string Identificador único para este envio, usado na deduplicação
timestamp inteiro Timestamp Unix atual; aceito dentro da tolerância configurada
campanha_id inteiro ID de uma campanha existente na plataforma
nome string Nome do lead

Campos opcionais: email, telefone, utm_source, utm_medium, utm_campaign, utm_content, utm_term, gclid, fbclid e mensagem.
UTMs, identificadores de anúncio e mensagem são preservados nas observações do lead. E-mail e telefone são usados como dados de contato.

Exemplo de payload:

{
  "event_type": "lead_form_submit",
  "id": "form-8a6c1d00-1f05-4cc2-a8d3-9f157f5d5a88",
  "timestamp": 1791484800,
  "campanha_id": 42,
  "nome": "João Silva",
  "email": "joao@example.com",
  "telefone": "11999990000",
  "utm_source": "google",
  "utm_medium": "cpc",
  "utm_campaign": "campanha-verao"
}

Gere um id novo para cada submissão legítima e reutilize-o apenas ao repetir a mesma tentativa por falha de rede. Se o primeiro evento já estiver processando ou processado, o endpoint retorna 200 already_accepted. Se ainda estiver pendente, o reenvio pode ser aceito com 202 e enfileirado novamente; o job também verifica se já existe um lead com aquele ID antes de criar outro.

Exemplo de encaminhamento server-side em PHP

Este exemplo representa o handler server-side do formulário. Adapte a leitura dos campos e a validação à landing page; não envie o segredo ao navegador.

<?php
declare(strict_types=1);

$secret = getenv('FORMULARIO_SECRET');
if ($secret === false || $secret === '') {
    throw new RuntimeException('FORMULARIO_SECRET não configurado.');
}

$dados = [
    'event_type'  => 'lead_form_submit',
    'id'          => bin2hex(random_bytes(16)),
    'timestamp'   => time(),
    'campanha_id' => 42, // ID de campanha existente na plataforma
    'nome'        => trim((string) ($_POST['nome'] ?? '')),
    'email'       => trim((string) ($_POST['email'] ?? '')),
    'telefone'    => trim((string) ($_POST['telefone'] ?? '')),
    'utm_source'  => trim((string) ($_POST['utm_source'] ?? '')),
];

$corpo = json_encode(
    $dados,
    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
);
$assinatura = 'sha256=' . hash_hmac('sha256', $corpo, $secret);

$ch = curl_init('https://app.exemplo.com.br/gestao-trafego/integracoes/leads');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $corpo,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'X-Formulario-Signature: ' . $assinatura,
    ],
    CURLOPT_TIMEOUT        => 10,
]);

$resposta = curl_exec($ch);
if ($resposta === false) {
    $erro = curl_error($ch);
    curl_close($ch);
    throw new RuntimeException('Falha ao enviar lead à plataforma: ' . $erro);
}

$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status !== 202 && $status !== 200) {
    throw new RuntimeException(
        'A plataforma recusou o lead (HTTP ' . $status . '): ' . $resposta
    );
}

Em produção, não mostre respostas internas ou erros técnicos aos visitantes. Registre-os no log privado do backend e apresente uma mensagem apropriada no formulário. Uma resposta 200 indica que o evento anterior já está processando ou foi processado; 202 indica que este evento foi enfileirado.

Respostas e diagnóstico

HTTP Resposta/causa
202 {"ok":true,"status":"accepted","evento_id":...} — evento enfileirado
200 {"ok":true,"status":"already_accepted"} — o evento anterior já está processando/processado
401 Assinatura ausente/inválida ou timestamp fora da tolerância
422 Corpo vazio, JSON inválido ou campo obrigatório ausente

Um reenvio com o mesmo id enquanto o evento original ainda está pendente pode receber 202 e gerar outro evento na fila. Use sempre o mesmo ID ao repetir a mesma submissão e não crie um ID novo para contornar erros.

Se a requisição receber 202, mas o lead não aparecer, verifique /admin/webhooks, confirme que o worker da fila webhooks está ativo e consulte o erro do evento. Uma campanha inexistente, por exemplo, só é detectada durante o processamento assíncrono e marca o evento como erro.

Segurança

Para detalhes das demais origens, filas e operação de webhooks, consulte WEBHOOKS.md e API-INTERNA.md.