# 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.

Publicado em 2026-10-25 · https://ikiform.com/pt-br/guides/lead-capture/leads-to-crm

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](https://ikiform.com/pt-br/docs/webhooks.md) assinado, diretamente ou por uma ferramenta de automação como [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) ou [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md). 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](https://ikiform.com/blog/form-submissions-to-zapier.md), [Make](https://ikiform.com/blog/form-submissions-to-make.md) ou [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md) | 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](https://ikiform.com/pt-br/docs/webhooks.md) cobre o payload e as novas tentativas, e [o seu próprio servidor](https://ikiform.com/blog/form-submissions-to-your-own-server.md) 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.

Diagrama: Como o relay mapeia um envio do Ikiform para um contato do 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`:

```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`.

Diagrama: O relay do Pipedrive, com cada passo seguro para repetir

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`:

```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

### O Ikiform tem integração com HubSpot ou Pipedrive?

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.

### Posso mudar o corpo do webhook para bater com a API do meu 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.

### Como evito contatos duplicados quando um webhook é reenviado?

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.

### Onde vejo por que um lead não chegou ao CRM?

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.

## Mais guias

- [Formulário de captação de leads](https://ikiform.com/pt-br/guides/lead-capture.md): Um formulário de leads no seu próprio site que envia e-mail com reply-to, avisa o chat da equipe e alimenta seu CRM, para você responder enquanto o lead ainda está lá.
- [Formulário de pedido de demonstração](https://ikiform.com/pt-br/guides/lead-capture/demo-request.md): Um formulário de agendar demonstração que armazena o lead e depois manda o visitante para o Cal.com ou o Calendly com nome e e-mail já preenchidos.
- [Formulário de consulta de projeto](https://ikiform.com/pt-br/guides/lead-capture/project-inquiry.md): Um formulário de "comece um projeto" para estúdios e freelancers que separa as consultas em quentes, mornas e sem fit por orçamento e prazo antes de alguém ler o briefing.
- [Formulário de pedido de orçamento](https://ikiform.com/pt-br/guides/lead-capture/quote-request.md): Um formulário de pedido de orçamento que recebe briefings como links compartilhados em vez de uploads, coloca um prazo em cada pedido e mantém leads cheios de links fora do spam.
- [Formulário de pedido de consulta](https://ikiform.com/pt-br/guides/lead-capture/consultation-request.md): Um formulário de pedido de consulta para contadores, advogados, coaches e clínicas que pergunta como e quando ligar, e deixa os detalhes sensíveis para a conversa.
- [Formulário de leads em Next.js](https://ikiform.com/pt-br/guides/lead-capture/nextjs.md): Um formulário de leads no App Router do Next.js que valida com Zod numa server action, guarda as UTMs do primeiro toque num cookie e manda leads qualificados para a sua agenda.
