Formulaire de capture de leads

Formulaire de capture de leads7 min de lecture

Envoyer les leads d'un formulaire vers HubSpot ou Pipedrive

Convertissez chaque webhook Ikiform signé en upsert de contact HubSpot ou en appels person et lead de Pipedrive, sans contacts en double quand une livraison est réessayée.

Preet SutharFondateur, Ikiform

Faites pointer un webhook Ikiform vers un petit relais qui vérifie la signature et convertit le lead pour l'API de votre CRM : l'upsert par lot de HubSpot avec l'e-mail comme clé, ou la recherche, la création de personne puis la création de lead de Pipedrive. Le corps du webhook d'Ikiform est fixe, c'est donc le relais qui fait la conversion. Rendez chaque étape rejouable sans risque, car une livraison échouée est réessayée avec le même webhook-id.

Ce qui change quand on envoie des leads vers un CRM

Il n'y a pas d'intégrations en un clic. Chaque outil est atteint par un webhook signé, directement ou via un outil d'automatisation comme Zapier ou n8n. Le corps du webhook est le même pour chaque destination : data.fields contient les valeurs du formulaire, et data.meta l'origine, le referrer, le pays et l'adresse de réponse. Ikiform ne vous laisse pas le remodeler : quelque chose entre les deux le transforme donc en l'appel qu'attend votre CRM.

Deux choses rendent un CRM plus délicat qu'un canal de chat :

  • Un CRM conserve des fiches, pas des messages. Publier deux fois le même lead dans Slack, c'est du bruit. Créer deux fois le même contact, c'est un désordre que quelqu'un fusionne à la main. Le relais doit chercher avant de créer.
  • Les nouvelles tentatives font partie du contrat. Si votre relais ou le CRM échoue, Ikiform réessaie la livraison pendant environ 3 jours avec le même webhook-id. Un relais qui a à moitié réussi la première fois ne doit rien dupliquer la deuxième.

Relais ou outil d'automatisation

OptionPoints fortsCompromis
Zapier, Make ou n8nÉtapes HubSpot et Pipedrive prêtes à l'emploi, avec des actions de recherche ou création, sans codePrix à la tâche, et un compte de plus qui détient vos identifiants CRM
Votre propre relaisCorrespondance exacte des champs, vos propres règles de doublons, rien de facturé par leadVous le déployez et le surveillez. Environ 60 lignes par CRM, montrées ci-dessous

Dans les deux cas, la configuration du webhook est la même : ajoutez l'endpoint sous Webhooks, copiez son secret whsec_ et envoyez un test. La documentation des webhooks détaille le payload et les nouvelles tentatives, et votre propre serveur approfondit la vérification des livraisons.

HubSpot : upsert du contact par e-mail

HubSpot considère l'e-mail comme l'identifiant unique d'un contact, et son endpoint d'upsert par lot accepte idProperty: "email". Un seul appel crée le contact si l'adresse est nouvelle et le met à jour sinon : une nouvelle tentative ne peut donc pas créer de doublon.

Comment le relais convertit une soumission Ikiform en contact HubSpotÉvénement IkiformContact HubSpotfields.email, en minusculesemail, la clé de l'upsertfields.name, premier motfirstnamefields.name, le restelastnamefields.companycompanyfields.interestikiform_interest, une propriétéà créerpropriétaire et cycle de vie,règles HubSpotComment le relais convertit une soumission Ikiform en contact HubSpotÉvénement IkiformContact HubSpotfields.email,en minusculesemail, la cléde l'upsertfields.name,premier motfirstnamefields.name,le restelastnamefields.companycompanyfields.interestikiform_interest,une propriétéà créerpropriétaireet cycle devie, règlesHubSpot
  1. fields.email, en minuscules, devient email, la clé de l'upsert.
  2. fields.name, premier mot, devient firstname.
  3. fields.name, le reste, devient lastname.
  4. fields.company devient company.
  5. fields.interest devient ikiform_interest, une propriété que vous créez.
  6. HubSpot définit le propriétaire et l'étape du cycle de vie selon ses propres règles ; le relais n'y touche pas.
Tout ce qui est à gauche vient du corps fixe du webhook d'Ikiform. La ligne grise est laissée aux workflows HubSpot plutôt que définie par le relais.

Configuration

  1. Dans HubSpot, créez une app privée (HubSpot les classe désormais comme legacy apps sur certains comptes) avec le scope crm.objects.contacts.write, et copiez son token d'accès dans HUBSPOT_TOKEN.
  2. Créez une propriété de contact avec le nom interne ikiform_interest, ou supprimez cette ligne. HubSpot refuse les propriétés qui n'existent pas.
  3. Déployez la route ci-dessous et ajoutez son URL comme endpoint de webhook dans 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 });
}
  • Un 429 de HubSpot devient un 429 avec Retry-After, qu'Ikiform respecte avant de réessayer.
  • Tout autre échec, y compris un 207 avec des erreurs par fiche, devient un 502 avec la réponse de HubSpot comme corps. Les 4 premiers Ko arrivent dans le journal de livraison d'Ikiform : vous pouvez donc lire le message de HubSpot sans vos propres logs.
  • L'upsert envoie toutes les propriétés à chaque fois, car HubSpot ne prend pas en charge les upserts partiels avec l'e-mail comme clé. Un company vide dans un deuxième formulaire écrase une valeur connue ; retirez les valeurs vides de properties si cela compte pour vous.
La référence de HubSpot publie désormais aussi des chemins d'API versionnés par date et classe ce chemin v3 dans sa section legacy. Consultez la référence actuelle des contacts au moment de construire, et gardez le même corps de requête.

Pipedrive : trouver la personne, puis ajouter un lead

Pipedrive sépare les personnes des leads. Un lead doit être lié à une personne ou à une organisation : le relais trouve ou crée donc la personne, puis ajoute un lead avec un titre. Les personnes et la recherche de personnes utilisent l'API v2 ; les leads et les notes sont des endpoints v1. Les deux prennent votre token d'API dans l'en-tête x-api-token.

Le relais Pipedrive, avec chaque étape rejouable sans risquewebhookwebhook-iddéjà vu ?lecture KVpersonnerecherche pare-mail, oucréationleadtitle, person_idrépondre 200rien à fairenonouiLe relais Pipedrive, avec chaque étape rejouable sans risquewebhookwebhook-iddéjà vu ?lecture KVpersonnerecherche par e-mail,ou créationleadtitle, person_idrépondre200rien à fairenonoui
  1. Une livraison arrive avec son webhook-id.
  2. Le relais cherche cet id dans un stockage clé-valeur.
  3. S'il a déjà été traité, le relais répond 200 et s'arrête.
  4. Sinon, il cherche les personnes par e-mail exact et n'en crée une que si aucune ne correspond.
  5. Il ajoute un lead lié à cette personne, puis une note avec le message, et enregistre le webhook-id.
La recherche de personne rend une nouvelle tentative sûre avant que le lead n'existe. Le webhook-id stocké la rend sûre après.
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 est le sous-domaine de votre entreprise, le acme de acme.pipedrive.com. Le token se trouve dans vos préférences personnelles, onglet API.
  • SEEN est un namespace Workers KV lié au Worker. L'id n'est stocké qu'après la création du lead : un échec en cours de route ne laisse donc rien qui bloquerait la nouvelle tentative.
  • origin_id contient l'id de soumission Ikiform, pour remonter d'un lead à la soumission stockée.
  • Les leads créés via l'API ont API comme source dans Pipedrive.

Vérifiez que tout fonctionne

  1. Choisissez Send test sur l'endpoint. Le relais répond 204 et ignore l'événement endpoint.test.
  2. Envoyez le formulaire avec une nouvelle adresse. Un contact HubSpot ou une personne et un lead Pipedrive apparaissent en quelques secondes.
  3. Envoyez à nouveau avec la même adresse. HubSpot met à jour l'unique contact ; Pipedrive ajoute un second lead à la personne existante.
  4. Ouvrez la livraison sous Webhooks et choisissez Retry. Rien de nouveau n'est créé.
  5. Cassez le token et envoyez. La livraison affiche l'erreur du CRM dans le journal ; corrigez le token et réessayez-la.

Pièges

  • Un lead par soumission, pas par personne. C'est généralement ce qu'il faut dans Pipedrive, où chaque demande est un lead à part. Si vous préférez rattacher les répétitions à un lead ouvert, cherchez d'abord les leads de la personne.
  • 3 endpoints par espace de travail. Un seul relais peut appeler les deux CRM et le chat, ce qui laisse des endpoints libres pour d'autres formulaires.
  • Le spam n'atteint jamais le CRM. Le spam n'envoie aucun webhook. Quand vous marquez une soumission comme non-spam, son webhook part à ce moment-là, et le lead apparaît en retard.
  • Les valeurs ne sont pas fiables. Les valeurs des champs viennent du public. Envoyez-les comme valeurs JSON, et ne construisez jamais de paramètres d'URL ni de formules à partir d'elles sans les encoder.

Questions

Une étape vous bloque, ou vous voulez un guide pour votre cas ? Écrivez à hello@ikiform.com.