Formular zur Lead-Erfassung

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.

Preet SutharGründer, Ikiform

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-id erneut. Ein Relay, das beim ersten Mal halb erfolgreich war, darf beim zweiten Mal nichts doppelt anlegen.

Relay oder ein Automatisierungstool

OptionStark beiNachteile
Zapier, Make oder n8nFertige Schritte für HubSpot und Pipedrive, mit Find-or-Create-Aktionen und ohne CodePreise pro Task und ein weiteres Konto, das deine CRM-Zugangsdaten hält
Dein eigenes RelayExakte Feldzuordnung, deine eigenen Regeln für Duplikate, keine Abrechnung pro LeadDu 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.

Wie das Relay eine Ikiform-Einsendung auf einen HubSpot-Kontakt abbildetIkiform-EventKontakt in HubSpotfields.email, kleingeschriebenemail und der Upsert-Schlüsselfields.name, erstes Wortfirstnamefields.name, der Restlastnamefields.companycompanyfields.interestikiform_interest, eineEigenschaft, die du anlegstOwner und Lifecycle-Phase, ausHubSpots eigenen RegelnWie das Relay eine Ikiform-Einsendung auf einen HubSpot-Kontakt abbildetIkiform-EventKontakt in HubSpotfields.email,kleingeschriebenemail und derUpsert-Schlüsselfields.name,erstes Wortfirstnamefields.name,der Restlastnamefields.companycompanyfields.interestikiform_interest,eineEigenschaft,die du anlegstOwner undLifecycle-Phase,aus HubSpotseigenenRegeln
  1. fields.email, kleingeschrieben, wird zu email und zum Upsert-Schlüssel.
  2. fields.name, erstes Wort, wird zu firstname.
  3. fields.name, der Rest, wird zu lastname.
  4. fields.company wird zu company.
  5. fields.interest wird zu ikiform_interest, einer Eigenschaft, die du anlegst.
  6. HubSpot setzt Owner und Lifecycle-Phase nach seinen eigenen Regeln; das Relay lässt beide unangetastet.
Alles links stammt aus dem festen Webhook-Body von Ikiform. Die graue Zeile bleibt HubSpot-Workflows überlassen, statt vom Relay gesetzt zu werden.

Einrichtung

  1. Erstelle in HubSpot eine Private App (HubSpot führt sie in manchen Konten inzwischen als Legacy Apps) mit dem Scope crm.objects.contacts.write und kopiere ihr Access Token in HUBSPOT_TOKEN.
  2. Lege eine Kontakteigenschaft mit dem internen Namen ikiform_interest an oder streiche diese Zeile. HubSpot lehnt Eigenschaften ab, die nicht existieren.
  3. Deploye die Route unten und füge ihre URL in Ikiform als Webhook-Endpunkt hinzu.
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 });
}
  • Ein 429 von HubSpot wird zu einem 429 mit Retry-After, den Ikiform vor dem nächsten Versuch respektiert.
  • Jeder andere Fehler, auch ein 207 mit Fehlern pro Datensatz, wird zu einem 502 mit 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 company aus einem zweiten Formular überschreibt ein bekanntes; entferne leere Werte aus properties, wenn dir das wichtig ist.
HubSpots Referenz veröffentlicht inzwischen auch API-Pfade mit Datumsversion und führt diesen 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.

Das Pipedrive-Relay, mit jedem Schritt wiederholbarWebhookwebhook-idschongesehen?KV-AbfragePersonper E-Mailsuchen oderanlegenLeadtitle, person_idmit 200antwortennichts zu tunneinjaDas Pipedrive-Relay, mit jedem Schritt wiederholbarWebhookwebhook-idschongesehen?KV-AbfragePersonper E-Mail suchenoder anlegenLeadtitle, person_idmit 200antwortennichts zutunneinja
  1. Eine Zustellung kommt mit ihrer webhook-id an.
  2. Das Relay prüft einen Key-Value-Store auf diese ID.
  3. Wurde sie schon verarbeitet, antwortet das Relay mit 200 und hört auf.
  4. Sonst sucht es Personen per exakter E-Mail und erstellt nur dann eine, wenn keine passt.
  5. Es fügt einen Lead hinzu, der mit dieser Person verknüpft ist, dann eine Notiz mit der Nachricht, und speichert die webhook-id.
Die Personensuche macht eine Wiederholung sicher, bevor der Lead existiert. Die gespeicherte webhook-id macht sie danach sicher.
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 ist die Subdomain deines Unternehmens, das acme in acme.pipedrive.com. Das Token findest du in deinen persönlichen Einstellungen im Tab API.
  • SEEN ist 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_id enthält die ID der Ikiform-Einsendung, sodass du einen Lead zur gespeicherten Einsendung zurückverfolgen kannst.
  • Über die API angelegte Leads bekommen in Pipedrive API als Quelle.

Prüfen, ob es funktioniert

  1. Wähle Send test am Endpunkt. Das Relay antwortet mit 204 und ignoriert das Event endpoint.test.
  2. Sende das Formular mit einer neuen Adresse ab. Ein HubSpot-Kontakt oder eine Pipedrive-Person mit Lead erscheint innerhalb von Sekunden.
  3. Sende erneut mit derselben Adresse. HubSpot aktualisiert den einen Kontakt; Pipedrive fügt der bestehenden Person einen zweiten Lead hinzu.
  4. Öffne die Zustellung unter Webhooks und wähle Retry. Nichts Neues wird angelegt.
  5. 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

Hängst du bei einem Schritt fest oder willst eine Anleitung für deinen Fall? Schreib an hello@ikiform.com.