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.
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ção | Bom em | Contras |
|---|---|---|
| Zapier, Make ou n8n | Passos prontos para HubSpot e Pipedrive, com ações de buscar ou criar e sem código | Preço por tarefa, e mais uma conta que guarda as credenciais do seu CRM |
| Seu próprio relay | Mapeamento exato dos campos, suas próprias regras de duplicados, nada cobrado por lead | Você 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.
- fields.email, em minúsculas, vira email, e a chave do upsert.
- fields.name, a primeira palavra, vira firstname.
- fields.name, o resto, vira lastname.
- fields.company vira company.
- fields.interest vira ikiform_interest, uma propriedade que você cria.
- 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.
Configuração
- No HubSpot, crie um app privado (em algumas contas o HubSpot agora os classifica como apps legados) com o escopo
crm.objects.contacts.writee copie o token de acesso dele paraHUBSPOT_TOKEN. - Crie uma propriedade de contato com o nome interno
ikiform_interest, ou remova essa linha. O HubSpot recusa propriedades que não existem. - Faça o deploy da rota abaixo e adicione a URL dela como endpoint de webhook no Ikiform.
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
429do HubSpot vira um429comRetry-After, que o Ikiform respeita antes de tentar de novo. - Qualquer outra falha, inclusive um
207com erros por registro, vira um502com 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
companyvazio vindo de um segundo formulário sobrescreve um valor já conhecido; remova os valores vazios depropertiesse isso importar para você.
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.
- Uma entrega chega com o seu webhook-id.
- O relay procura esse id num armazenamento chave-valor.
- Se já foi tratada antes, o relay responde 200 e para.
- Caso contrário, ele busca pessoas pelo e-mail exato e só cria uma quando nenhuma corresponde.
- Ele adiciona um lead ligado a essa pessoa, depois uma nota com a mensagem, e registra o webhook-id.
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, oacmeemacme.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_idguarda o id do envio no Ikiform, para você rastrear um lead até o envio armazenado.- Leads criados pela API recebem
APIcomo origem no Pipedrive.
Confira se funciona
- Escolha Send test no endpoint. O relay responde
204e ignora o eventoendpoint.test. - Envie o formulário com um endereço novo. Um contato no HubSpot, ou uma pessoa e um lead no Pipedrive, aparecem em segundos.
- Envie de novo com o mesmo endereço. O HubSpot atualiza o único contato; o Pipedrive adiciona um segundo lead à pessoa existente.
- Abra a entrega em Webhooks e escolha Retry. Nada novo é criado.
- 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
Não de um clique. O Ikiform envia cada envio como um webhook assinado, e um relay ou uma ferramenta de automação como Zapier, Make ou n8n cria o contato ou o lead pela API do CRM.
Não. O corpo do webhook é fixo para todo destino, com os valores do formulário em data.fields. Um relay ou um passo de automação o converte na chamada que o seu CRM espera.
Torne cada passo seguro para repetir. No HubSpot, faça upsert por e-mail. No Pipedrive, busque a pessoa pelo e-mail exato antes de criar uma, e armazene o webhook-id depois de criar o lead para que uma nova tentativa o pule.
Abra o endpoint em Webhooks no painel. Cada entrega lista as tentativas com o status code do seu relay e os primeiros 4 KB da resposta dele, então devolva o texto de erro do CRM no corpo.
Travou em algum passo ou quer um guia para o seu caso? Envie um e-mail para hello@ikiform.com.