Formulário de captação de leads

Formulário de captação de leads7 min de leitura

Envie leads de formulário para o HubSpot ou o Pipedrive

Mapeie cada webhook assinado do Ikiform para o upsert de contatos do HubSpot ou para as chamadas de pessoa e lead do Pipedrive, sem contatos duplicados quando uma entrega é repetida.

Preet SutharFundador, Ikiform

Aponte um webhook do Ikiform para um pequeno relay que verifica a assinatura e mapeia o lead para a API do seu CRM: o batch upsert do HubSpot com chave no e-mail, ou a busca de pessoa, criação de pessoa e criação de lead do Pipedrive. O corpo do webhook do Ikiform é fixo, então o relay faz o mapeamento. Torne cada passo seguro para repetir, porque uma entrega que falhou é tentada de novo com o mesmo webhook-id.

O que muda ao enviar leads para um CRM

Não há integrações de um clique. Toda ferramenta é alcançada por um webhook assinado, diretamente ou por uma ferramenta de automação como Zapier ou n8n. O corpo do webhook é o mesmo para todo destino: data.fields guarda os valores do formulário, e data.meta guarda a origem, o referrer, o país e o endereço de reply-to. O Ikiform não deixa você mudar esse formato, então algo no meio o transforma na chamada que o seu CRM espera.

Duas coisas tornam um CRM mais difícil do que um canal de chat:

  • Um CRM guarda registros, não mensagens. Publicar o mesmo lead duas vezes no Slack é ruído. Criar o mesmo contato duas vezes é uma bagunça que alguém mescla à mão. O relay precisa buscar antes de criar.
  • Novas tentativas fazem parte do contrato. Se o seu relay ou o CRM falhar, o Ikiform tenta a entrega de novo por cerca de 3 dias com o mesmo webhook-id. Um relay que deu meio certo na primeira vez não pode duplicar na segunda.

Relay ou ferramenta de automação

OpçãoBom emContras
Zapier, Make ou n8nPassos prontos para HubSpot e Pipedrive, com ações de buscar ou criar e sem códigoPreço por tarefa, e mais uma conta que guarda as credenciais do seu CRM
Seu próprio relayMapeamento exato dos campos, suas próprias regras de duplicados, nada cobrado por leadVocê faz o deploy e monitora. Cerca de 60 linhas por CRM, mostradas abaixo

De qualquer forma, a configuração do webhook é a mesma: adicione o endpoint em Webhooks, copie o segredo whsec_ dele e envie um teste. A documentação de webhooks cobre o payload e as novas tentativas, e o seu próprio servidor explica com mais detalhes como verificar as entregas.

HubSpot: faça upsert do contato pelo e-mail

O HubSpot trata o e-mail como identificador único de um contato, e o endpoint de batch upsert dele aceita idProperty: "email". Uma chamada cria o contato se o endereço for novo e o atualiza se não for, então uma nova tentativa não consegue criar um duplicado.

Como o relay mapeia um envio do Ikiform para um contato do HubSpotEvento do IkiformContato no HubSpotfields.email, em minúsculasemail, e a chave do upsertfields.name, primeira palavrafirstnamefields.name, o restantelastnamefields.companycompanyfields.interestikiform_interest, umapropriedade que você criaproprietário e estágio do ciclode vida, pelas regras do HubSpotComo o relay mapeia um envio do Ikiform para um contato do HubSpotEvento do IkiformContato no HubSpotfields.email,em minúsculasemail, e achave doupsertfields.name,primeirapalavrafirstnamefields.name, orestantelastnamefields.companycompanyfields.interestikiform_interest,umapropriedadeque você criaproprietárioe estágio dociclo de vida,pelas regrasdo HubSpot
  1. fields.email, em minúsculas, vira email, e a chave do upsert.
  2. fields.name, a primeira palavra, vira firstname.
  3. fields.name, o resto, vira lastname.
  4. fields.company vira company.
  5. fields.interest vira ikiform_interest, uma propriedade que você cria.
  6. O HubSpot define o proprietário e o estágio do ciclo de vida com as próprias regras; o relay não mexe neles.
Tudo à esquerda vem do corpo fixo do webhook do Ikiform. A linha cinza fica a cargo dos workflows do HubSpot, em vez de ser definida pelo relay.

Configuração

  1. No HubSpot, crie um app privado (em algumas contas o HubSpot agora os classifica como apps legados) com o escopo crm.objects.contacts.write e copie o token de acesso dele para HUBSPOT_TOKEN.
  2. Crie uma propriedade de contato com o nome interno ikiform_interest, ou remova essa linha. O HubSpot recusa propriedades que não existem.
  3. Faça o deploy da rota abaixo e adicione a URL dela como endpoint de webhook no Ikiform.
app/api/hubspot/route.ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

const UPSERT = "https://api.hubapi.com/crm/v3/objects/contacts/batch/upsert";

export async function POST(request: Request) {
  const payload = await request.text();
  let event;
  try {
    event = await verifyWebhook({ payload, headers: request.headers, secret: process.env.IKIFORM_WEBHOOK_SECRET! });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
  if (event.type !== "submission.created") return new Response(null, { status: 204 });

  const fields = event.data.fields;
  const email = String(fields.email ?? "").trim().toLowerCase();
  if (!email) return new Response("No email, nothing to upsert", { status: 200 });
  const [firstname = "", ...rest] = String(fields.name ?? "").trim().split(/\s+/);

  const response = await fetch(UPSERT, {
    method: "POST",
    headers: {
      authorization: `Bearer ${process.env.HUBSPOT_TOKEN}`,
      "content-type": "application/json",
    },
    body: JSON.stringify({
      inputs: [
        {
          idProperty: "email",
          id: email,
          properties: {
            email,
            firstname,
            lastname: rest.join(" "),
            company: String(fields.company ?? ""),
            phone: String(fields.phone ?? ""),
            ikiform_interest: String(fields.interest ?? ""),
          },
        },
      ],
    }),
  });

  if (response.status === 429) {
    return new Response("HubSpot rate limit", { status: 429, headers: { "retry-after": "10" } });
  }
  if (response.status === 207 || !response.ok) {
    return new Response(await response.text(), { status: 502 });
  }
  return new Response(null, { status: 204 });
}
  • Um 429 do HubSpot vira um 429 com Retry-After, que o Ikiform respeita antes de tentar de novo.
  • Qualquer outra falha, inclusive um 207 com erros por registro, vira um 502 com a resposta do HubSpot no corpo. Os primeiros 4 KB vão para o log de entregas do Ikiform, então você lê a mensagem do HubSpot sem precisar dos seus próprios logs.
  • O upsert envia todas as propriedades toda vez, porque o HubSpot não aceita upserts parciais com o e-mail como chave. Um company vazio vindo de um segundo formulário sobrescreve um valor já conhecido; remova os valores vazios de properties se isso importar para você.
A referência do HubSpot agora também publica caminhos de API versionados por data e lista este caminho v3 na seção legada. Confira a referência atual de contatos quando for construir, e mantenha o mesmo corpo da requisição.

Pipedrive: encontre a pessoa e depois adicione um lead

O Pipedrive separa pessoas de leads. Um lead precisa estar ligado a uma pessoa ou a uma organização, então o relay encontra ou cria a pessoa e depois adiciona um lead com um título. Pessoas e a busca de pessoas usam a API v2; leads e notas são endpoints v1. Ambos recebem o seu token de API no header x-api-token.

O relay do Pipedrive, com cada passo seguro para repetirwebhookwebhook-idjá visto?consulta noKVpessoabusca pore-mail, ou crialeadtitle, person_idresponde 200nada a fazernãosimO relay do Pipedrive, com cada passo seguro para repetirwebhookwebhook-idjá visto?consulta no KVpessoabusca por e-mail, oucrialeadtitle, person_idresponde200nada a fazernãosim
  1. Uma entrega chega com o seu webhook-id.
  2. O relay procura esse id num armazenamento chave-valor.
  3. Se já foi tratada antes, o relay responde 200 e para.
  4. Caso contrário, ele busca pessoas pelo e-mail exato e só cria uma quando nenhuma corresponde.
  5. Ele adiciona um lead ligado a essa pessoa, depois uma nota com a mensagem, e registra o webhook-id.
A busca de pessoa torna uma nova tentativa segura antes de o lead existir. O webhook-id armazenado a torna segura depois.
src/index.ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

interface Env {
  IKIFORM_WEBHOOK_SECRET: string;
  PIPEDRIVE_TOKEN: string;
  PIPEDRIVE_DOMAIN: string;
  SEEN: KVNamespace;
}

export default {
  async fetch(request: Request, env: Env) {
    const payload = await request.text();
    let event;
    try {
      event = await verifyWebhook({ payload, headers: request.headers, secret: env.IKIFORM_WEBHOOK_SECRET });
    } catch (error) {
      if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
      throw error;
    }
    if (event.type !== "submission.created") return new Response(null, { status: 204 });
    if (await env.SEEN.get(event.id)) return new Response("Already handled", { status: 200 });

    const base = `https://${env.PIPEDRIVE_DOMAIN}.pipedrive.com/api`;
    const headers = { "x-api-token": env.PIPEDRIVE_TOKEN, "content-type": "application/json" };
    const call = async (path: string, init?: RequestInit) => {
      const response = await fetch(`${base}${path}`, { ...init, headers });
      if (!response.ok) throw new Error(`Pipedrive ${path} answered ${response.status}: ${await response.text()}`);
      return (await response.json()) as { data: any };
    };

    const fields = event.data.fields;
    const email = String(fields.email ?? "").trim().toLowerCase();
    const name = String(fields.name ?? email);

    try {
      const found = await call(`/v2/persons/search?term=${encodeURIComponent(email)}&fields=email&exact_match=true`);
      let personId: number | undefined = found.data?.items?.[0]?.item?.id;
      if (!personId) {
        const created = await call("/v2/persons", {
          method: "POST",
          body: JSON.stringify({ name, emails: [{ value: email, primary: true, label: "work" }] }),
        });
        personId = created.data.id;
      }

      const lead = await call("/v1/leads", {
        method: "POST",
        body: JSON.stringify({
          title: `${fields.company || name}: ${fields.interest ?? "website lead"}`,
          person_id: personId,
          origin_id: event.data.submission_id,
        }),
      });

      if (fields.message) {
        await call("/v1/notes", {
          method: "POST",
          body: JSON.stringify({ lead_id: lead.data.id, content: String(fields.message) }),
        });
      }
    } catch (error) {
      return new Response(String(error), { status: 502 });
    }

    await env.SEEN.put(event.id, "1", { expirationTtl: 60 * 60 * 24 * 5 });
    return new Response(null, { status: 204 });
  },
};
  • PIPEDRIVE_DOMAIN é o subdomínio da sua empresa, o acme em acme.pipedrive.com. O token fica nas suas preferências pessoais, na aba API.
  • SEEN é um namespace do Workers KV vinculado ao Worker. O id só é armazenado depois que o lead é criado, então uma falha no meio do caminho não deixa nada que bloqueie a nova tentativa.
  • origin_id guarda o id do envio no Ikiform, para você rastrear um lead até o envio armazenado.
  • Leads criados pela API recebem API como origem no Pipedrive.

Confira se funciona

  1. Escolha Send test no endpoint. O relay responde 204 e ignora o evento endpoint.test.
  2. Envie o formulário com um endereço novo. Um contato no HubSpot, ou uma pessoa e um lead no Pipedrive, aparecem em segundos.
  3. Envie de novo com o mesmo endereço. O HubSpot atualiza o único contato; o Pipedrive adiciona um segundo lead à pessoa existente.
  4. Abra a entrega em Webhooks e escolha Retry. Nada novo é criado.
  5. Quebre o token e envie. A entrega mostra o erro do CRM no log; corrija o token e tente de novo.

Pegadinhas

  • Um lead por envio, não por pessoa. Isso costuma ser o certo no Pipedrive, onde cada contato comercial é um lead próprio. Se preferir ligar as repetições a um lead aberto, busque primeiro os leads da pessoa.
  • 3 endpoints por workspace. Um único relay pode chamar os dois CRMs e o chat, o que deixa endpoints livres para outros formulários.
  • Spam nunca chega ao CRM. Spam não envia webhook. Quando você marca um envio como não spam, o webhook dele sai nesse momento, e o lead aparece com atraso.
  • Os valores não são confiáveis. Os valores dos campos vêm do público. Envie-os como valores JSON e nunca monte query strings ou fórmulas com eles sem codificar.

Perguntas

Travou em algum passo ou quer um guia para o seu caso? Envie um e-mail para hello@ikiform.com.