Formular zur Lead-Erfassung6 Min. Lesezeit
Leads aus Formularen an HubSpot oder Pipedrive senden
Bilde jeden signierten Ikiform-Webhook auf HubSpots Kontakt-Upsert oder die Personen- und Lead-Aufrufe von Pipedrive ab, ohne doppelte Kontakte bei einer wiederholten Zustellung.
Richte einen Ikiform-Webhook auf ein kleines Relay aus, das die Signatur verifiziert und den Lead auf die API deines CRM abbildet: HubSpots Batch-Upsert mit der E-Mail als Schlüssel oder Pipedrives Personensuche, Personenanlage und Lead-Anlage. Der Webhook-Body von Ikiform ist fest, also übernimmt das Relay die Zuordnung. Mach jeden Schritt wiederholbar, denn eine fehlgeschlagene Zustellung wird mit derselben webhook-id erneut versucht.
Was beim Senden von Leads an ein CRM anders ist
Es gibt keine Ein-Klick-Integrationen. Jedes Tool wird über einen signierten Webhook erreicht, direkt oder über ein Automatisierungstool wie Zapier oder n8n. Der Webhook-Body ist für jedes Ziel derselbe: data.fields enthält die Werte des Formulars, und data.meta enthält Origin, Referrer, Land und Reply-To-Adresse. Ikiform lässt dich ihn nicht umformen, also macht etwas dazwischen daraus den Aufruf, den dein CRM erwartet.
Zwei Dinge machen ein CRM schwieriger als einen Chat-Channel:
- Ein CRM führt Datensätze, keine Nachrichten. Denselben Lead zweimal in Slack zu posten, ist Rauschen. Denselben Kontakt zweimal anzulegen, ist ein Durcheinander, das jemand von Hand zusammenführt. Das Relay muss nachschlagen, bevor es anlegt.
- Wiederholungen gehören zum Vertrag. Scheitert dein Relay oder das CRM, versucht Ikiform die Zustellung etwa 3 Tage lang mit derselben
webhook-iderneut. Ein Relay, das beim ersten Mal halb erfolgreich war, darf beim zweiten Mal nichts doppelt anlegen.
Relay oder ein Automatisierungstool
| Option | Stark bei | Nachteile |
|---|---|---|
| Zapier, Make oder n8n | Fertige Schritte für HubSpot und Pipedrive, mit Find-or-Create-Aktionen und ohne Code | Preise pro Task und ein weiteres Konto, das deine CRM-Zugangsdaten hält |
| Dein eigenes Relay | Exakte Feldzuordnung, deine eigenen Regeln für Duplikate, keine Abrechnung pro Lead | Du deployst und überwachst es. Etwa 60 Zeilen pro CRM, unten gezeigt |
So oder so ist das Webhook-Setup dasselbe: Füge den Endpunkt unter Webhooks hinzu, kopiere sein whsec_-Secret und sende einen Test. Die Webhook-Doku behandelt Payload und Wiederholungen, und dein eigener Server behandelt das Verifizieren von Zustellungen ausführlicher.
HubSpot: den Kontakt per E-Mail upserten
HubSpot behandelt die E-Mail als eindeutige Kennung eines Kontakts, und sein Batch-Upsert-Endpunkt nimmt idProperty: "email". Ein Aufruf legt den Kontakt an, wenn die Adresse neu ist, und aktualisiert ihn sonst, sodass eine Wiederholung kein Duplikat anlegen kann.
- fields.email, kleingeschrieben, wird zu email und zum Upsert-Schlüssel.
- fields.name, erstes Wort, wird zu firstname.
- fields.name, der Rest, wird zu lastname.
- fields.company wird zu company.
- fields.interest wird zu ikiform_interest, einer Eigenschaft, die du anlegst.
- HubSpot setzt Owner und Lifecycle-Phase nach seinen eigenen Regeln; das Relay lässt beide unangetastet.
Einrichtung
- Erstelle in HubSpot eine Private App (HubSpot führt sie in manchen Konten inzwischen als Legacy Apps) mit dem Scope
crm.objects.contacts.writeund kopiere ihr Access Token inHUBSPOT_TOKEN. - Lege eine Kontakteigenschaft mit dem internen Namen
ikiform_interestan oder streiche diese Zeile. HubSpot lehnt Eigenschaften ab, die nicht existieren. - Deploye die Route unten und füge ihre URL in Ikiform als Webhook-Endpunkt hinzu.
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 });
}- Ein
429von HubSpot wird zu einem429mitRetry-After, den Ikiform vor dem nächsten Versuch respektiert. - Jeder andere Fehler, auch ein
207mit Fehlern pro Datensatz, wird zu einem502mit HubSpots Antwort als Body. Die ersten 4 KB landen im Zustellungsprotokoll von Ikiform, sodass du HubSpots Meldung ohne eigene Logs lesen kannst. - Der Upsert sendet jedes Mal jede Eigenschaft, weil HubSpot keine partiellen Upserts mit der E-Mail als Schlüssel unterstützt. Ein leeres
companyaus einem zweiten Formular überschreibt ein bekanntes; entferne leere Werte ausproperties, wenn dir das wichtig ist.
v3-Pfad im Legacy-Abschnitt. Prüf beim Bauen die aktuelle Kontakt-Referenz und lass den Request-Body gleich.Pipedrive: die Person finden, dann einen Lead hinzufügen
Pipedrive trennt Personen von Leads. Ein Lead muss mit einer Person oder Organisation verknüpft sein, also findet oder erstellt das Relay die Person und fügt dann einen Lead mit Titel hinzu. Personen und die Personensuche nutzen API v2; Leads und Notizen sind v1-Endpunkte. Beide nehmen dein API-Token im Header x-api-token.
- Eine Zustellung kommt mit ihrer webhook-id an.
- Das Relay prüft einen Key-Value-Store auf diese ID.
- Wurde sie schon verarbeitet, antwortet das Relay mit 200 und hört auf.
- Sonst sucht es Personen per exakter E-Mail und erstellt nur dann eine, wenn keine passt.
- Es fügt einen Lead hinzu, der mit dieser Person verknüpft ist, dann eine Notiz mit der Nachricht, und speichert die 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_DOMAINist die Subdomain deines Unternehmens, dasacmeinacme.pipedrive.com. Das Token findest du in deinen persönlichen Einstellungen im Tab API.SEENist ein Workers-KV-Namespace, der an den Worker gebunden ist. Die ID wird erst gespeichert, nachdem der Lead angelegt ist, sodass ein Fehler auf halbem Weg nichts hinterlässt, was die Wiederholung blockiert.origin_identhält die ID der Ikiform-Einsendung, sodass du einen Lead zur gespeicherten Einsendung zurückverfolgen kannst.- Über die API angelegte Leads bekommen in Pipedrive
APIals Quelle.
Prüfen, ob es funktioniert
- Wähle Send test am Endpunkt. Das Relay antwortet mit
204und ignoriert das Eventendpoint.test. - Sende das Formular mit einer neuen Adresse ab. Ein HubSpot-Kontakt oder eine Pipedrive-Person mit Lead erscheint innerhalb von Sekunden.
- Sende erneut mit derselben Adresse. HubSpot aktualisiert den einen Kontakt; Pipedrive fügt der bestehenden Person einen zweiten Lead hinzu.
- Öffne die Zustellung unter Webhooks und wähle Retry. Nichts Neues wird angelegt.
- Mach das Token kaputt und sende ab. Die Zustellung zeigt den Fehler des CRM im Protokoll; korrigiere das Token und versuch es erneut.
Stolperfallen
- Ein Lead pro Einsendung, nicht pro Person. In Pipedrive ist das meist richtig, wo jede Anfrage ihr eigener Lead ist. Willst du Wiederholungen lieber an einen offenen Lead hängen, suche zuerst Leads nach Person.
- 3 Endpunkte pro Workspace. Ein Relay kann beide CRMs und den Chat aufrufen, was Endpunkte für andere Formulare freihält.
- Spam erreicht nie das CRM. Spam löst keinen Webhook aus. Markierst du eine Einsendung als kein Spam, geht ihr Webhook dann raus, und der Lead erscheint verspätet.
- Werte sind nicht vertrauenswürdig. Feldwerte kommen von der Öffentlichkeit. Sende sie als JSON-Werte und bau nie Query-Strings oder Formeln daraus, ohne sie zu kodieren.
Fragen
Keine mit einem Klick. Ikiform sendet jede Einsendung als signierten Webhook, und ein Relay oder ein Automatisierungstool wie Zapier, Make oder n8n legt den Kontakt oder Lead über die API des CRM an.
Nein. Der Webhook-Body ist für jedes Ziel fest, mit den Werten des Formulars unter data.fields. Ein Relay oder ein Automatisierungsschritt bildet ihn auf den Aufruf ab, den dein CRM erwartet.
Mach jeden Schritt wiederholbar. In HubSpot upsertest du per E-Mail. In Pipedrive suchst du die Person per exakter E-Mail, bevor du eine anlegst, und speicherst die webhook-id nach dem Anlegen des Leads, damit eine Wiederholung ihn überspringt.
Öffne den Endpunkt unter Webhooks im Dashboard. Jede Zustellung listet ihre Versuche mit dem Statuscode deines Relays und den ersten 4 KB seiner Antwort auf, also gib den Fehlertext des CRM im Body zurück.
Hängst du bei einem Schritt fest oder willst eine Anleitung für deinen Fall? Schreib an hello@ikiform.com.