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.
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
| Option | Points forts | Compromis |
|---|---|---|
| Zapier, Make ou n8n | Étapes HubSpot et Pipedrive prêtes à l'emploi, avec des actions de recherche ou création, sans code | Prix à la tâche, et un compte de plus qui détient vos identifiants CRM |
| Votre propre relais | Correspondance exacte des champs, vos propres règles de doublons, rien de facturé par lead | Vous 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.
- fields.email, en minuscules, devient email, la clé de l'upsert.
- fields.name, premier mot, devient firstname.
- fields.name, le reste, devient lastname.
- fields.company devient company.
- fields.interest devient ikiform_interest, une propriété que vous créez.
- HubSpot définit le propriétaire et l'étape du cycle de vie selon ses propres règles ; le relais n'y touche pas.
Configuration
- 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 dansHUBSPOT_TOKEN. - 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. - Déployez la route ci-dessous et ajoutez son URL comme endpoint de webhook dans 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 });
}- Un
429de HubSpot devient un429avecRetry-After, qu'Ikiform respecte avant de réessayer. - Tout autre échec, y compris un
207avec des erreurs par fiche, devient un502avec 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
companyvide dans un deuxième formulaire écrase une valeur connue ; retirez les valeurs vides depropertiessi cela compte pour vous.
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.
- Une livraison arrive avec son webhook-id.
- Le relais cherche cet id dans un stockage clé-valeur.
- S'il a déjà été traité, le relais répond 200 et s'arrête.
- Sinon, il cherche les personnes par e-mail exact et n'en crée une que si aucune ne correspond.
- Il ajoute un lead lié à cette personne, puis une note avec le message, et enregistre le 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_DOMAINest le sous-domaine de votre entreprise, leacmedeacme.pipedrive.com. Le token se trouve dans vos préférences personnelles, onglet API.SEENest 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_idcontient l'id de soumission Ikiform, pour remonter d'un lead à la soumission stockée.- Les leads créés via l'API ont
APIcomme source dans Pipedrive.
Vérifiez que tout fonctionne
- Choisissez Send test sur l'endpoint. Le relais répond
204et ignore l'événementendpoint.test. - Envoyez le formulaire avec une nouvelle adresse. Un contact HubSpot ou une personne et un lead Pipedrive apparaissent en quelques secondes.
- Envoyez à nouveau avec la même adresse. HubSpot met à jour l'unique contact ; Pipedrive ajoute un second lead à la personne existante.
- Ouvrez la livraison sous Webhooks et choisissez Retry. Rien de nouveau n'est créé.
- 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
Pas en un clic. Ikiform envoie chaque soumission sous forme de webhook signé, et un relais ou un outil d'automatisation comme Zapier, Make ou n8n crée le contact ou le lead via l'API du CRM.
Non. Le corps du webhook est fixe pour chaque destination, avec les valeurs du formulaire sous data.fields. Un relais ou une étape d'automatisation le convertit en l'appel qu'attend votre CRM.
Rendez chaque étape rejouable sans risque. Dans HubSpot, faites un upsert par e-mail. Dans Pipedrive, cherchez la personne par e-mail exact avant d'en créer une, et stockez le webhook-id après avoir créé le lead pour qu'une nouvelle tentative le saute.
Ouvrez l'endpoint sous Webhooks dans le tableau de bord. Chaque livraison liste ses tentatives avec le code de statut de votre relais et les 4 premiers Ko de sa réponse : renvoyez donc le texte d'erreur du CRM dans le corps.
Une étape vous bloque, ou vous voulez un guide pour votre cas ? Écrivez à hello@ikiform.com.