# Ein Lead-Formular bauen, das dich schnell erreicht

> Ein Lead-Formular auf deiner eigenen Site, das dich per E-Mail mit Reply-To benachrichtigt, den Team-Chat anpingt und dein CRM füttert, damit du antwortest, solange der Lead noch da ist.

Veröffentlicht am 2026-10-25 · https://ikiform.com/de/guides/lead-capture

Setze ein kurzes Formular auf deine eigene Seite, das Name, geschäftliche E-Mail und eine qualifizierende Frage an ein Formular-Backend sendet. Schalte Benachrichtigungs-E-Mails ein, damit jeder Lead in deinem Posteingang landet und **Reply** an den Lead geht, und sende einen signierten Webhook an deinen Team-Chat und dein CRM. Miss dann die Minuten vom Absenden bis zur ersten Antwort, denn diese Zahl entscheidet mehr Deals als das Design des Formulars.

Ein Lead-Formular ist ein Kontaktformular mit einer Aufgabe: ein Verkaufsgespräch zu beginnen. Wer es ausfüllt, vergleicht dich mit jemand anderem, oft in einem anderen Tab. Das Team, das zuerst antwortet, mit einer hilfreichen Antwort von einem echten Menschen, bekommt meist den Termin. Diese Anleitung baut das Formular auf deiner eigenen Site, bringt jeden Lead innerhalb von Sekunden zu dir und lässt das CRM optional.

## Der Weg eines Leads

Das Formular sendet an Ikiform. Ikiform prüft den Post auf Spam, speichert ihn und macht dann drei Dinge gleichzeitig: Es schickt deinem Team eine E-Mail mit dem Lead als Reply-To-Adresse, sendet einen signierten Webhook und zeigt den Lead im Dashboard. Über den Webhook erreicht der Lead den Team-Chat und dein CRM.

Diagramm: Wie ein Lead von deinem Formular zu der Person gelangt, die ihn beantwortet

1. Ein Besucher füllt das Lead-Formular auf deiner Site aus, und es sendet an deinen Ikiform-Endpunkt.
2. Ikiform prüft Honeypot, Spam-Score und Rate-Limit und speichert dann den Lead.
3. Sekunden später bekommt jeder Empfänger der Benachrichtigung eine E-Mail, und ein Klick auf Antworten schreibt an den Lead.
4. Der Lead erscheint im Dashboard, wo du ihn suchen, filtern und exportieren kannst.
5. Ikiform sendet einen signierten submission.created-Webhook.
6. Ein Relay oder ein Automatisierungstool postet den Lead in den Team-Chat und legt den Kontakt in deinem CRM an.

E-Mail ist der schnellste Weg ohne Einrichtung. Der Webhook ergänzt die Orte, die dein Team ohnehin im Blick hat.

## Vier Wege, Leads zu erfassen

Jedes übliche Setup ist in etwas gut. Entscheide danach, wo deine Leads liegen sollen und wer das Formular pflegt.

| Ansatz | Stark bei | Nachteile |
| --- | --- | --- |
| Das eigene Formular deines CRM, etwa HubSpot-Formulare oder die Webformulare von Pipedrive | Leads landen ohne Zwischenschritt als Kontakte im CRM. Tracking und Lead-Scoring liegen in einem Produkt. | Das Formular wird mit Skript und Styling des CRM eingebettet. Ein CRM-Wechsel heißt, jedes Formular neu zu bauen. Manche Funktionen hängen vom Tarif ab. |
| Eingebetteter Formular-Baukasten wie [Typeform](https://ikiform.com/alternatives/typeform) oder [Tally](https://ikiform.com/alternatives/tally) | Ausgefeilte mehrstufige Formulare mit Logik und ohne Code. | Ein Embed, das wie der Baukasten aussieht, außer du zahlst fürs Umgestalten. Webhooks und Antwortlimits hängen vom Tarif ab. |
| Deine eigene API-Route und Datenbank | Jede Logik, die du willst, etwa Lead-Scoring, bevor überhaupt etwas gespeichert wird. | Du baust Spamfilter, Rate-Limits, E-Mail-Benachrichtigungen, Wiederholungen und Export selbst und betreibst einen Server für ein einziges Formular. |
| Formular-Backend wie Ikiform | Dein eigenes HTML auf jeder Site. Spamfilter, E-Mail-Benachrichtigungen mit Reply-To, signierte Webhooks und CSV-Export ohne Server. | Kein Lead-Scoring, keine CRM-Datensätze und keine E-Mail an den Lead. Das kommt per Webhook aus deinem CRM oder E-Mail-Tool. |

Wenn dein ganzer Vertriebsprozess in einem CRM läuft und du mit dessen Formular zufrieden bist, nutze es. Ein Formular-Backend passt, wenn das Formular zu deiner Site passen muss, wenn du das CRM vielleicht wechselst oder wenn dasselbe Formular E-Mail, Chat und ein CRM erreichen soll, ohne dass du dich für eines entscheiden musst.

## Wenig fragen, einmal qualifizieren

Jedes zusätzliche Feld kostet Einsendungen. Die meisten Lead-Formulare brauchen vier Dinge: an wen die Antwort geht, wo die Person arbeitet, was sie will und Platz für Erklärungen. Eine Frage mit festen Optionen übernimmt das Qualifizieren, weil sie als einer von wenigen bekannten Werten ankommt, denen dein Routing vertrauen kann. Probier es unten aus; es führt Ikiforms Validator in deinem Browser aus und zeigt, was gespeichert würde, oder den `422`, den du zurückbekämst.

Formular: Talk to sales

- **Name** (`name`, text, Pflichtfeld)
- **Geschäftliche E-Mail** (`email`, email, Pflichtfeld)
- **Unternehmen** (`company`, text)
- **Wobei können wir helfen?** (`interest`, select, Pflichtfeld)
- **Sollten wir noch etwas wissen?** (`message`, textarea)

Füge diese Definition im Tab **Fields** des Formulars ein, und Ikiform setzt sie auf dem Server durch. Die [Vorlage für Kontaktformulare](https://ikiform.com/forms/contact-form) ist eine schlankere Version mit einem Thema statt eines Interesses.

| Feld | Warum | Beachte |
| --- | --- | --- |
| `name` | Eine Antwort, die mit dem Namen beginnt | Behalte die ID `name`. Ein Link in einem Namensfeld ist ein starkes Spamsignal |
| `email` | An wen **Reply** schreibt | Behalte die ID `email`. Die Beschriftung **Work email** bringt Leute dazu, eine geschäftliche Adresse zu nutzen, erzwingt es aber nicht |
| `company` | Recherchiere, bevor du antwortest | Optional. Als Pflichtfeld kostet es Einsendungen von Freelancern |
| `interest` | Routing und Priorität | Ein `select` speichert einen gelisteten Wert oder lehnt den Post ab, sodass Automatisierungen darauf verzweigen können |
| `message` | Kontext für die erste Antwort | Optional. An einem Pflicht-Aufsatzfeld brechen Leute ab |

Telefonnummern, Teamgröße und Budget gehören auf Formulare, bei denen sie ändern, was du als Nächstes tust. Die [Vorlage für Angebotsanfragen](https://ikiform.com/forms/quote-request) ist die vollständigere Version, mit Leistung, Budget und Zeitrahmen als feste Optionen. Wenn ein Vertriebler jeden Lead anruft, frag nach einer Telefonnummer. Wenn du nach Teamgröße routest, frag sie als `select` ab. Sonst heb sie dir für die erste Antwort auf.

## Das Formular

Wähle deinen Stack. Jede Version zeigt die Meldung des Servers unter dem Feld, zu dem sie gehört, und behält, was der Besucher eingegeben hat, denn eine halb geschriebene Nachricht wegen eines Tippfehlers zu verlieren ist der Moment, in dem Leads aufgeben. Der Erfolgszustand nennt, wer antwortet und wann, damit sich niemand fragt, ob es geklappt hat.

**HTML**

Das Formular sendet auf dem einfachen Weg und leitet auf deine Dankeseite weiter, es funktioniert also, bevor irgendein Skript lädt. Jedes Feld hat einen leeren Platz für seinen Fehler.

`contact-sales.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST" id="lead-form" novalidate>
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required />
  <p class="error" data-error-for="name" hidden></p>

  <label for="email">Work email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <p class="error" data-error-for="email" hidden></p>

  <label for="company">Company</label>
  <input id="company" name="company" autocomplete="organization" />

  <label for="interest">What can we help with?</label>
  <select id="interest" name="interest" required>
    <option value="pricing">Pricing for my team</option>
    <option value="demo">A live demo</option>
    <option value="custom">A custom plan or contract</option>
    <option value="other">Something else</option>
  </select>

  <label for="message">Anything we should know?</label>
  <textarea id="message" name="message" rows="4"></textarea>
  <p class="error" data-error-for="message" hidden></p>

  <input type="hidden" name="page" value="pricing" />
  <input type="hidden" name="_redirect" value="https://example.com/thanks" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Talk to sales</button>
</form>
```

Das Skript hält den Besucher auf der Seite. Es zeigt den Fehler des Servers unter dem jeweiligen Feld und ersetzt das Formular durch eine Zeile, die sagt, wer antwortet und wann.

`lead-form.js`:

```js
const form = document.getElementById("lead-form");

function showErrors(fields) {
  form.querySelectorAll("[data-error-for]").forEach((slot) => {
    const message = fields[slot.dataset.errorFor]?.[0];
    slot.textContent = message ?? "";
    slot.hidden = !message;
  });
}

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const button = form.querySelector("button");
  button.disabled = true;
  button.textContent = "Sending";
  try {
    const response = await fetch(form.action, {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new FormData(form),
    });
    const result = await response.json();
    if (result.ok) {
      const thanks = document.createElement("p");
      thanks.setAttribute("role", "status");
      thanks.textContent = `Thanks, ${form.elements.name.value.trim().split(" ")[0]}. A person on our team will reply to ${form.elements.email.value} within one working day.`;
      form.replaceWith(thanks);
      return;
    }
    showErrors(result.error.fields ?? {});
    if (!result.error.fields) alert(result.error.message);
  } catch {
    alert("Something went wrong. Email sales@example.com and we'll pick it up.");
  }
  button.disabled = false;
  button.textContent = "Talk to sales";
});
```

**Next.js**

Die Server Action leitet den Lead mit deinem Secret Key weiter und gibt den ersten Fehler pro Feld zurück, plus die Werte, die der Besucher eingegeben hat, sodass bei einem `422` nichts verloren geht.

`app/contact-sales/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";

export interface LeadState {
  sent: boolean;
  firstName?: string;
  errors: Record<string, string>;
  values: Record<string, string>;
}

const KEYS = ["name", "email", "company", "interest", "message", "page", "_gotcha"] as const;

export async function sendLead(_previous: LeadState, formData: FormData): Promise<LeadState> {
  const values = Object.fromEntries(KEYS.map((key) => [key, String(formData.get(key) ?? "").trim()]));
  const origin = (await headers()).get("origin");

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify(values),
  });
  const result = await response.json().catch(() => null);
  if (result?.ok) return { sent: true, firstName: values.name.split(" ")[0], errors: {}, values: {} };

  const fields: Record<string, string[]> = result?.error?.fields ?? {};
  const errors = Object.fromEntries(Object.entries(fields).map(([key, messages]) => [key, messages[0] ?? ""]));
  if (!Object.keys(errors).length) errors.form = result?.error?.message ?? "Something went wrong. Email sales@example.com and we'll pick it up.";
  return { sent: false, errors, values };
}
```

`app/contact-sales/lead-form.tsx`:

```tsx
"use client";

import { useActionState } from "react";
import { type LeadState, sendLead } from "./actions";

const initial: LeadState = { sent: false, errors: {}, values: {} };

export function LeadForm() {
  const [state, action, pending] = useActionState(sendLead, initial);
  const { errors, values } = state;

  if (state.sent) {
    return <p role="status">Thanks, {state.firstName}. A person on our team will reply within one working day.</p>;
  }

  return (
    <form action={action}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required defaultValue={values.name} />
      {errors.name ? <p role="alert">{errors.name}</p> : null}

      <label htmlFor="email">Work email</label>
      <input id="email" name="email" type="email" autoComplete="email" required defaultValue={values.email} />
      {errors.email ? <p role="alert">{errors.email}</p> : null}

      <label htmlFor="company">Company</label>
      <input id="company" name="company" autoComplete="organization" defaultValue={values.company} />

      <label htmlFor="interest">What can we help with?</label>
      <select id="interest" name="interest" defaultValue={values.interest ?? "pricing"}>
        <option value="pricing">Pricing for my team</option>
        <option value="demo">A live demo</option>
        <option value="custom">A custom plan or contract</option>
        <option value="other">Something else</option>
      </select>

      <label htmlFor="message">Anything we should know?</label>
      <textarea id="message" name="message" rows={4} defaultValue={values.message} />
      {errors.message ? <p role="alert">{errors.message}</p> : null}

      <input type="hidden" name="page" value="pricing" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form ? <p role="alert">{errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Talk to sales"}</button>
    </form>
  );
}
```

Mehr zu Next.js? Lies [Qualifizierendes Lead-Formular mit Next.js bauen](https://ikiform.com/de/guides/lead-capture/nextjs.md).

**Astro**

Zod prüft die Felder, bevor etwas deinen Server verlässt, und `isInputError` liefert dir die Meldungen pro Feld. Die Seite muss bei Bedarf gerendert werden, damit das Formular an die Action senden kann.

`src/actions/index.ts`:

```ts
import { ActionError, defineAction } from "astro:actions";
import { getSecret } from "astro:env/server";
import { z } from "astro/zod";

export const server = {
  sendLead: defineAction({
    accept: "form",
    input: z.object({
      name: z.string().trim().min(1, "Tell us who to reply to."),
      email: z.email("Enter a work email we can reply to."),
      company: z.string().trim().optional(),
      interest: z.enum(["pricing", "demo", "custom", "other"]),
      message: z.string().trim().max(5000).optional(),
      page: z.string().default("pricing"),
      _gotcha: z.string().nullish(),
    }),
    handler: async (input, context) => {
      const origin = context.request.headers.get("origin");
      const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          Authorization: `Bearer ${getSecret("IKIFORM_SECRET_KEY")}`,
          ...(origin ? { Origin: origin } : {}),
        },
        body: JSON.stringify(input),
      });
      const result = await response.json();
      if (!result.ok) throw new ActionError({ code: "BAD_REQUEST", message: result.error.message });
      return { firstName: input.name.split(" ")[0] };
    },
  }),
};
```

`src/pages/contact-sales.astro`:

```astro
---
import { actions, isInputError } from "astro:actions";

export const prerender = false;

const result = Astro.getActionResult(actions.sendLead);
const fields = result?.error && isInputError(result.error) ? result.error.fields : {};
---

{result?.data ? (
  <p role="status">Thanks, {result.data.firstName}. A person on our team will reply within one working day.</p>
) : (
  <form method="POST" action={actions.sendLead}>
    <label for="name">Name</label>
    <input id="name" name="name" autocomplete="name" required />
    {fields.name && <p role="alert">{fields.name[0]}</p>}

    <label for="email">Work email</label>
    <input id="email" name="email" type="email" autocomplete="email" required />
    {fields.email && <p role="alert">{fields.email[0]}</p>}

    <label for="company">Company</label>
    <input id="company" name="company" autocomplete="organization" />

    <label for="interest">What can we help with?</label>
    <select id="interest" name="interest">
      <option value="pricing">Pricing for my team</option>
      <option value="demo">A live demo</option>
      <option value="custom">A custom plan or contract</option>
      <option value="other">Something else</option>
    </select>

    <label for="message">Anything we should know?</label>
    <textarea id="message" name="message" rows="4"></textarea>

    <input type="hidden" name="page" value="pricing" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {result?.error && !isInputError(result.error) && <p role="alert">{result.error.message}</p>}
    <button>Talk to sales</button>
  </form>
)}
```

Mehr zu Astro? Lies [Qualifizierendes Lead-Formular mit Astro bauen](https://ikiform.com/de/guides/lead-capture/astro.md).

**TanStack Start**

Die Serverfunktion liest die `FormData` in `.validator()`, leitet sie weiter und gibt entweder den Vornamen oder eine Map mit Feldfehlern zurück, die die Route unter jedem Input rendert.

`src/routes/contact-sales.tsx`:

```tsx
import { createFileRoute } from "@tanstack/react-router";
import { createServerFn, useServerFn } from "@tanstack/react-start";
import { getRequestHeader } from "@tanstack/react-start/server";
import { type FormEvent, useState } from "react";

type Result = { ok: true; firstName: string } | { ok: false; errors: Record<string, string> };

const sendLead = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    const read = (key: string) => String(data.get(key) ?? "").trim();
    return {
      name: read("name"),
      email: read("email"),
      company: read("company"),
      interest: read("interest"),
      message: read("message"),
      page: read("page"),
      _gotcha: read("_gotcha"),
    };
  })
  .handler(async ({ data }): Promise<Result> => {
    const origin = getRequestHeader("origin");
    const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
        ...(origin ? { Origin: origin } : {}),
      },
      body: JSON.stringify(data),
    });
    const result = await response.json();
    if (result.ok) return { ok: true, firstName: data.name.split(" ")[0] ?? "" };
    const fields: Record<string, string[]> = result.error.fields ?? { form: [result.error.message] };
    return { ok: false, errors: Object.fromEntries(Object.entries(fields).map(([key, list]) => [key, list[0] ?? ""])) };
  });

export const Route = createFileRoute("/contact-sales")({ component: ContactSales });

function ContactSales() {
  const send = useServerFn(sendLead);
  const [result, setResult] = useState<Result | null>(null);
  const [pending, setPending] = useState(false);
  const errors = result && !result.ok ? result.errors : {};

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setPending(true);
    try {
      setResult(await send({ data: new FormData(event.currentTarget) }));
    } catch {
      setResult({ ok: false, errors: { form: "Something went wrong. Email sales@example.com and we'll pick it up." } });
    } finally {
      setPending(false);
    }
  }

  if (result?.ok) {
    return <p role="status">Thanks, {result.firstName}. A person on our team will reply within one working day.</p>;
  }

  return (
    <form method="post" onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      {errors.name ? <p role="alert">{errors.name}</p> : null}
      <label htmlFor="email">Work email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {errors.email ? <p role="alert">{errors.email}</p> : null}
      <label htmlFor="company">Company</label>
      <input id="company" name="company" autoComplete="organization" />
      <label htmlFor="interest">What can we help with?</label>
      <select id="interest" name="interest">
        <option value="pricing">Pricing for my team</option>
        <option value="demo">A live demo</option>
        <option value="custom">A custom plan or contract</option>
        <option value="other">Something else</option>
      </select>
      <label htmlFor="message">Anything we should know?</label>
      <textarea id="message" name="message" rows={4} />
      <input type="hidden" name="page" value="pricing" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form ? <p role="alert">{errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Talk to sales"}</button>
    </form>
  );
}
```

Mehr zu TanStack Start? Lies [Qualifizierendes Lead-Formular mit TanStack Start bauen](https://ikiform.com/de/guides/lead-capture/tanstack-start.md).

**React Router**

Die Route-Action leitet den Post weiter und gibt Feldfehler mit Ikiforms Statuscode über `data()` zurück. Die Komponente liest sie aus `actionData`.

`app/routes/contact-sales.tsx`:

```tsx
import { data, Form, useNavigation } from "react-router";
import type { Route } from "./+types/contact-sales";

const KEYS = ["name", "email", "company", "interest", "message", "page", "_gotcha"];

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const values = Object.fromEntries(KEYS.map((key) => [key, String(formData.get(key) ?? "").trim()]));
  const origin = request.headers.get("origin");

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify(values),
  });
  const result = await response.json();
  if (result.ok) return { sent: true as const, firstName: values.name.split(" ")[0] };
  const fields: Record<string, string[]> = result.error.fields ?? { form: [result.error.message] };
  return data(
    { sent: false as const, errors: Object.fromEntries(Object.entries(fields).map(([key, list]) => [key, list[0]])) },
    { status: response.status },
  );
}

export default function ContactSales({ actionData }: Route.ComponentProps) {
  const navigation = useNavigation();
  const pending = navigation.state === "submitting";

  if (actionData?.sent) {
    return <p role="status">Thanks, {actionData.firstName}. A person on our team will reply within one working day.</p>;
  }
  const errors: Record<string, string | undefined> = actionData?.errors ?? {};

  return (
    <Form method="post">
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      {errors.name ? <p role="alert">{errors.name}</p> : null}
      <label htmlFor="email">Work email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {errors.email ? <p role="alert">{errors.email}</p> : null}
      <label htmlFor="company">Company</label>
      <input id="company" name="company" autoComplete="organization" />
      <label htmlFor="interest">What can we help with?</label>
      <select id="interest" name="interest">
        <option value="pricing">Pricing for my team</option>
        <option value="demo">A live demo</option>
        <option value="custom">A custom plan or contract</option>
        <option value="other">Something else</option>
      </select>
      <label htmlFor="message">Anything we should know?</label>
      <textarea id="message" name="message" rows={4} />
      <input type="hidden" name="page" value="pricing" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form ? <p role="alert">{errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Talk to sales"}</button>
    </Form>
  );
}
```

Mehr zu React Router? Lies [Qualifizierendes Lead-Formular mit React Router bauen](https://ikiform.com/de/guides/lead-capture/react-router.md).

**SvelteKit**

Die Form Action gibt `fail()` mit den Werten und den Fehlern zurück, und `update({ reset: false })` lässt das Eingegebene auf dem Bildschirm, während der Besucher ein Feld korrigiert. Deklariere `IKIFORM_SECRET_KEY` in `src/env.ts` mit `defineEnvVars`, damit `$app/env/private` ihn lesen kann.

`src/routes/contact-sales/+page.server.ts`:

```ts
import { fail } from "@sveltejs/kit";
import { IKIFORM_SECRET_KEY } from "$app/env/private";
import type { Actions } from "./$types";

const KEYS = ["name", "email", "company", "interest", "message", "page", "_gotcha"];

export const actions = {
  default: async ({ request, fetch, url }) => {
    const data = await request.formData();
    const values = Object.fromEntries(KEYS.map((key) => [key, String(data.get(key) ?? "").trim()]));

    const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${IKIFORM_SECRET_KEY}`,
        Origin: request.headers.get("origin") ?? url.origin,
      },
      body: JSON.stringify(values),
    });
    const result = await response.json();
    if (!result.ok) {
      const fields: Record<string, string[]> = result.error.fields ?? { form: [result.error.message] };
      const errors = Object.fromEntries(Object.entries(fields).map(([key, list]) => [key, list[0]]));
      return fail(response.status, { values, errors });
    }
    return { firstName: values.name.split(" ")[0] };
  },
} satisfies Actions;
```

`src/routes/contact-sales/+page.svelte`:

```svelte
<script lang="ts">
  import { enhance } from "$app/forms";
  import type { PageProps } from "./$types";

  let { form }: PageProps = $props();
  let pending = $state(false);
  const errors = $derived<Record<string, string | undefined>>(form?.errors ?? {});
</script>

{#if form?.firstName}
  <p role="status">Thanks, {form.firstName}. A person on our team will reply within one working day.</p>
{:else}
  <form
    method="POST"
    use:enhance={() => {
      pending = true;
      return async ({ update }) => {
        await update({ reset: false });
        pending = false;
      };
    }}
  >
    <label for="name">Name</label>
    <input id="name" name="name" autocomplete="name" required value={form?.values?.name ?? ""} />
    {#if errors.name}<p role="alert">{errors.name}</p>{/if}

    <label for="email">Work email</label>
    <input id="email" name="email" type="email" autocomplete="email" required value={form?.values?.email ?? ""} />
    {#if errors.email}<p role="alert">{errors.email}</p>{/if}

    <label for="company">Company</label>
    <input id="company" name="company" autocomplete="organization" value={form?.values?.company ?? ""} />

    <label for="interest">What can we help with?</label>
    <select id="interest" name="interest" value={form?.values?.interest ?? "pricing"}>
      <option value="pricing">Pricing for my team</option>
      <option value="demo">A live demo</option>
      <option value="custom">A custom plan or contract</option>
      <option value="other">Something else</option>
    </select>

    <label for="message">Anything we should know?</label>
    <textarea id="message" name="message" rows="4">{form?.values?.message ?? ""}</textarea>

    <input type="hidden" name="page" value="pricing" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {#if errors.form}<p role="alert">{errors.form}</p>{/if}
    <button disabled={pending}>{pending ? "Sending" : "Talk to sales"}</button>
  </form>
{/if}
```

Mehr zu SvelteKit? Lies [Qualifizierendes Lead-Formular mit SvelteKit bauen](https://ikiform.com/de/guides/lead-capture/sveltekit.md).

**Nuxt**

Eine Server-Route hält den Key vom Client fern und gibt Ikiforms Feldfehler im `data` des Fehlers zurück. Setze `NUXT_IKIFORM_SECRET_KEY` in der Umgebung.

`server/api/lead.post.ts`:

```ts
export default defineEventHandler(async (event) => {
  const body = await readBody<Record<string, string | undefined>>(event);
  const config = useRuntimeConfig(event);
  const origin = getRequestHeader(event, "origin");
  const keys = ["name", "email", "company", "interest", "message", "page", "_gotcha"];
  const values = Object.fromEntries(keys.map((key) => [key, String(body[key] ?? "").trim()]));

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${config.ikiformSecretKey}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify(values),
  });
  const result = await response.json();
  if (!result.ok) {
    throw createError({
      status: response.status,
      message: result.error.message,
      data: { fields: result.error.fields ?? {} },
    });
  }
  return { firstName: values.name?.split(" ")[0] };
});
```

`nuxt.config.ts`:

```ts
export default defineNuxtConfig({
  runtimeConfig: {
    ikiformSecretKey: "",
  },
});
```

`app/components/LeadForm.vue`:

```vue
<script setup lang="ts">
const lead = reactive({ name: "", email: "", company: "", interest: "pricing", message: "", _gotcha: "" });
const errors = ref<Record<string, string[]>>({});
const failure = ref("");
const sending = ref(false);
const firstName = ref("");

async function send() {
  sending.value = true;
  errors.value = {};
  failure.value = "";
  try {
    const result = await $fetch("/api/lead", { method: "POST", body: { ...lead, page: "pricing" } });
    firstName.value = result.firstName ?? "there";
  } catch (error) {
    const data = (error as { data?: { message?: string; data?: { fields?: Record<string, string[]> } } }).data;
    errors.value = data?.data?.fields ?? {};
    if (!Object.keys(errors.value).length) failure.value = data?.message ?? "Something went wrong. Email sales@example.com and we'll pick it up.";
  } finally {
    sending.value = false;
  }
}
</script>

<template>
  <p v-if="firstName" role="status">Thanks, {{ firstName }}. A person on our team will reply within one working day.</p>
  <form v-else @submit.prevent="send">
    <label for="name">Name</label>
    <input id="name" v-model="lead.name" name="name" autocomplete="name" required />
    <p v-if="errors.name" role="alert">{{ errors.name[0] }}</p>
    <label for="email">Work email</label>
    <input id="email" v-model="lead.email" name="email" type="email" autocomplete="email" required />
    <p v-if="errors.email" role="alert">{{ errors.email[0] }}</p>
    <label for="company">Company</label>
    <input id="company" v-model="lead.company" name="company" autocomplete="organization" />
    <label for="interest">What can we help with?</label>
    <select id="interest" v-model="lead.interest" name="interest">
      <option value="pricing">Pricing for my team</option>
      <option value="demo">A live demo</option>
      <option value="custom">A custom plan or contract</option>
      <option value="other">Something else</option>
    </select>
    <label for="message">Anything we should know?</label>
    <textarea id="message" v-model="lead.message" name="message" rows="4" />
    <input v-model="lead._gotcha" name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="failure" role="alert">{{ failure }}</p>
    <button :disabled="sending">{{ sending ? "Sending" : "Talk to sales" }}</button>
  </form>
</template>
```

Mehr zu Nuxt? Lies [Qualifizierendes Lead-Formular mit Nuxt bauen](https://ikiform.com/de/guides/lead-capture/nuxt.md).

**React**

Ohne eigenen Server sendet das Formular direkt aus dem Browser an Ikiform. Halte Secret Keys aus diesem Code heraus; stattdessen schützt **Allowed domains** das Formular.

`src/LeadForm.tsx`:

```tsx
import { type FormEvent, useState } from "react";

const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";

export function LeadForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "sent">("idle");
  const [errors, setErrors] = useState<Record<string, string[]>>({});
  const [firstName, setFirstName] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const body = new FormData(event.currentTarget);
    setStatus("sending");
    setErrors({});
    try {
      const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
      const result = await response.json();
      if (result.ok) {
        setFirstName(String(body.get("name")).trim().split(" ")[0] ?? "");
        setStatus("sent");
        return;
      }
      setErrors(result.error.fields ?? { form: [result.error.message] });
    } catch {
      setErrors({ form: ["Something went wrong. Email sales@example.com and we'll pick it up."] });
    }
    setStatus("idle");
  }

  if (status === "sent") {
    return <p role="status">Thanks, {firstName}. A person on our team will reply within one working day.</p>;
  }

  return (
    <form action={ENDPOINT} method="post" onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      {errors.name ? <p role="alert">{errors.name[0]}</p> : null}
      <label htmlFor="email">Work email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {errors.email ? <p role="alert">{errors.email[0]}</p> : null}
      <label htmlFor="company">Company</label>
      <input id="company" name="company" autoComplete="organization" />
      <label htmlFor="interest">What can we help with?</label>
      <select id="interest" name="interest">
        <option value="pricing">Pricing for my team</option>
        <option value="demo">A live demo</option>
        <option value="custom">A custom plan or contract</option>
        <option value="other">Something else</option>
      </select>
      <label htmlFor="message">Anything we should know?</label>
      <textarea id="message" name="message" rows={4} />
      <input type="hidden" name="page" value="pricing" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form ? <p role="alert">{errors.form[0]}</p> : null}
      <button disabled={status === "sending"}>{status === "sending" ? "Sending" : "Talk to sales"}</button>
    </form>
  );
}
```

**Vue**

Derselbe Post aus dem Browser als Vue-Komponente. Das Formular behält seine `action`, sodass ein Besucher, bei dem das Skript nicht lädt, dich trotzdem erreicht.

`src/components/LeadForm.vue`:

```vue
<script setup lang="ts">
import { ref } from "vue";

const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";

const status = ref<"idle" | "sending" | "sent">("idle");
const errors = ref<Record<string, string[]>>({});
const firstName = ref("");

async function send(event: SubmitEvent) {
  const body = new FormData(event.target as HTMLFormElement);
  status.value = "sending";
  errors.value = {};
  try {
    const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
    const result = await response.json();
    if (result.ok) {
      firstName.value = String(body.get("name")).trim().split(" ")[0] ?? "";
      status.value = "sent";
      return;
    }
    errors.value = result.error.fields ?? { form: [result.error.message] };
  } catch {
    errors.value = { form: ["Something went wrong. Email sales@example.com and we'll pick it up."] };
  }
  status.value = "idle";
}
</script>

<template>
  <p v-if="status === 'sent'" role="status">Thanks, {{ firstName }}. A person on our team will reply within one working day.</p>
  <form v-else :action="ENDPOINT" method="post" @submit.prevent="send">
    <label for="name">Name</label>
    <input id="name" name="name" autocomplete="name" required />
    <p v-if="errors.name" role="alert">{{ errors.name[0] }}</p>
    <label for="email">Work email</label>
    <input id="email" name="email" type="email" autocomplete="email" required />
    <p v-if="errors.email" role="alert">{{ errors.email[0] }}</p>
    <label for="company">Company</label>
    <input id="company" name="company" autocomplete="organization" />
    <label for="interest">What can we help with?</label>
    <select id="interest" name="interest">
      <option value="pricing">Pricing for my team</option>
      <option value="demo">A live demo</option>
      <option value="custom">A custom plan or contract</option>
      <option value="other">Something else</option>
    </select>
    <label for="message">Anything we should know?</label>
    <textarea id="message" name="message" rows="4" />
    <input type="hidden" name="page" value="pricing" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="errors.form" role="alert">{{ errors.form[0] }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Sending" : "Talk to sales" }}</button>
  </form>
</template>
```

- Ersetze `frm_xxxxxxxxxxxxxxxx` durch die ID deines Formulars aus dem Dashboard.
- Das versteckte Eingabefeld `_gotcha` ist der Honeypot. Menschen sehen es nie; Bots, die jedes Eingabefeld ausfüllen, markieren sich selbst als Spam.
- Das versteckte Input `page` hält fest, von welcher Seite der Lead kam: Preise, eine Feature-Seite, eine Kampagnen-Landingpage. Wenn du Felder definierst, füge `page` als optionales Textfeld hinzu, sonst wird der Post mit `Unknown field` abgelehnt.
- Die Serverversionen leiten den `Origin`-Header des Besuchers weiter, weil ein serverseitiger Fetch keinen sendet und Ikiform daran kostenlose `localhost`-Tests erkennt.
- Das reine HTML-Formular braucht **Redirect after submit** oder ein `_redirect`-Input auf einer deiner **Allowed domains**, um auf deiner eigenen Dankeseite zu landen.

> Die serverseitigen Versionen senden mit einem Secret Key, daher überspringt Ikiform **Allowed domains** und seine Menschlichkeitsprüfung. Jeder Post kommt dann von der IP deines Servers, und das Standard-Limit von 5 pro Minute und Besucher zählt alle deine Besucher zusammen. Wähle für ein Formular, das du über einen Server sendest, das [Rate-Limit](https://ikiform.com/de/docs/rate-limits.md) Relaxed oder Custom, und halte `IKIFORM_SECRET_KEY` in reinen Server-Umgebungsvariablen.

Die Doku behandelt das Setup jedes Frameworks ausführlich: [Next.js](https://ikiform.com/de/docs/guides/nextjs.md), [Astro](https://ikiform.com/de/docs/guides/astro.md), [SvelteKit](https://ikiform.com/de/docs/guides/sveltekit.md), [Nuxt](https://ikiform.com/de/docs/guides/nuxt.md), [React](https://ikiform.com/de/docs/guides/react.md), [Vue](https://ikiform.com/de/docs/guides/vue.md), [Senden von einem Server](https://ikiform.com/de/docs/guides/server.md) und [reines HTML](https://ikiform.com/de/docs/html.md).

## Erreiche den Lead in Minuten

Das Formular ist der einfache Teil. Was ein Lead-Formular, das verkauft, von einem unterscheidet, das nur sammelt, ist die Zeit zwischen Absenden und der Antwort eines Menschen. So sieht dieser Weg aus, wenn er funktioniert:

Diagramm: Die Minuten zwischen dem Absenden eines Leads und deiner ersten Antwort

1. Der Lead sendet das Formular ab.
2. Ikiform speichert ihn und antwortet dem Browser in deutlich unter einer Sekunde.
3. Benachrichtigungs-E-Mail und Webhook gehen Sekunden später aus einer Warteschlange raus.
4. Jemand im Team klickt in seinem Posteingang auf Antworten oder auf den mailto-Link im Chat, und die Antwort geht an den Lead.

Alles bis zur E-Mail dauert Sekunden. Der letzte Pfeil ist der, den du in der Hand hast, und der, der Deals gewinnt.

### Aus deinem Postfach antworten

Jede Benachrichtigungs-E-Mail setzt ihre Reply-To-Adresse auf den Lead. Ikiform nimmt sie zuerst aus einem Feld `_replyto`, dann aus einem Feld namens `email`, `email_address`, `your_email`, `reply_to`, `contact_email`. Behalte die Feld-ID `email`, dann geht **Reply** an den Lead, nicht an Ikiform. Der Betreff ist `New submission:` plus der Name des Formulars, also benenne das Formular nach dem, was es ist, etwa **Talk to sales**, und filtere nach diesem Betreff, um Leads zu labeln oder zu markieren.

Füge in den Benachrichtigungseinstellungen des Formulars bis zu 10 Empfänger pro Formular hinzu: Workspace-Mitglieder oder externe Adressen, die zuerst per E-Mail bestätigen. Schick sie an die Leute, die antworten, nicht an einen geteilten Alias, der niemandem gehört: Ein Lead in fünf Posteingängen, den niemand übernimmt, wartet länger als ein Lead in einem.

> Benachrichtigungs-E-Mails sind auf 50 pro Formular und Stunde begrenzt, oder 10, solange ein Workspace jünger als 7 Tage ist, danach kommt eine Zusammenfassung für den Rest der Stunde. Ein normales Lead-Formular kommt nie in die Nähe. Wenn du eine Kampagne fährst, bei der das passieren könnte, leite Leads zusätzlich per Webhook in den Chat.

### Pinge das Team dort an, wo es ist

E-Mail funktioniert, wenn jemand für den Posteingang zuständig ist. Ein Team, das im Chat lebt, antwortet schneller, wenn der Lead dort auftaucht. Füge einen Webhook zu Slack, Teams oder Discord hinzu, nach [Slack](https://ikiform.com/blog/form-submissions-to-slack.md), [Microsoft Teams](https://ikiform.com/blog/form-submissions-to-microsoft-teams.md) oder [Discord](https://ikiform.com/blog/form-submissions-to-discord.md). Setze einen `mailto:`-Link in die Nachricht, damit die Person, die ihn übernimmt, mit einem Klick antworten kann.

### Route nach dem, was angefragt wurde

Weil `interest` nur gelistete Werte akzeptiert, kann ein kleines Relay zuverlässig darauf verzweigen. Diese Route schickt Demo-Anfragen und Vertragsfragen in eigene Channels und alles andere in den Haupt-Channel des Vertriebs:

`app/api/leads/route.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

const CHANNELS: Record<string, string | undefined> = {
  demo: process.env.SLACK_DEMO_WEBHOOK,
  custom: process.env.SLACK_ENTERPRISE_WEBHOOK,
};

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 lead = event.data.fields;
  const interest = String(lead.interest ?? "other");
  const target = CHANNELS[interest] ?? process.env.SLACK_SALES_WEBHOOK!;
  const text = [
    `*${lead.name}* from ${lead.company || "an unknown company"} wants: ${interest}`,
    `Reply: mailto:${lead.email}`,
    lead.message ? `> ${String(lead.message).slice(0, 300)}` : "",
  ]
    .filter(Boolean)
    .join("\n");

  const response = await fetch(target, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ text }),
  });
  if (!response.ok) return new Response(`Slack answered ${response.status}`, { status: 502 });
  return new Response(null, { status: 204 });
}
```

- `verifyWebhook` prüft Signatur und Zeitstempel, damit niemand sonst gefälschte Leads in deine Channels posten kann.
- Ein `502`, wenn Slack ausgefallen ist, lässt Ikiform dieselbe Zustellung mit derselben `webhook-id` etwa 3 Tage lang wiederholen.
- Ändere die Beschriftung einer Option nach Belieben. Halte ihren Wert stabil, sobald ein Relay oder eine Automatisierung davon abhängt.

## Halte Müll aus der Pipeline

Lead-Formulare ziehen eine besondere Art von Spam an: Leute, die dir SEO, Linkbuilding und ausgelagerte Entwicklung verkaufen wollen. Das verschwendet deine schnellste Antwort, also filtere es, bevor es jemanden erreicht.

| Abwehr | Was sie stoppt | Einrichtung |
| --- | --- | --- |
| Honeypot | Bots, die jedes Eingabefeld ausfüllen | Das versteckte Eingabefeld `_gotcha` |
| Spam-Bewertung | Bekannte Verkaufsphrasen wie `seo services` (+2), Links im Namen (+3), drei oder mehr Links (+2), Wiederholungen (+3). Ab 5 ist es Spam | Keine |
| Rate-Limit | Skripte, die in einer Schleife senden | Stell das Formular auf Strict: 2 alle 10 Minuten pro Besucher. Ein echter Lead sendet selten zweimal in zehn Minuten |
| Erlaubte Domains | Posts von anderen Sites | Füge deine Domain unter **Allowed domains** hinzu |
| Menschlichkeitsprüfung | Bots in einem echten Browser | Stell **Spam check** auf **Ikiform**. Die `fetch`-Versionen brauchen dann ein Token von `challenge()` |

Spam bekommt dieselbe Erfolgsantwort wie ein echter Lead, löst keine E-Mail und keinen Webhook aus und zählt nicht zum Verbrauch. Das heißt auch: Ein falsch eingestufter Lead bleibt stumm. Sieh einmal pro Woche in den Filter **Spam**; markierst du einen als kein Spam, gehen seine E-Mail und Webhooks dann raus. Die [Spam-Doku](https://ikiform.com/de/docs/spam.md#scoring) listet jedes Signal auf.

## Wisse, woher Leads kommen

Jede Einsendung speichert bereits Origin und Referrer der Seite, die im Webhook als `meta.origin` und `meta.referrer` auftauchen. Kampagnen-Tags brauchen etwas Hilfe, weil der Besucher oft auf einer Seite landet und auf einer anderen absendet. Dieses Snippet kopiert `utm_`-Parameter in versteckte Inputs und merkt sie sich für die Sitzung:

`attribution.html`:

```html
<input type="hidden" name="utm_source" />
<input type="hidden" name="utm_medium" />
<input type="hidden" name="utm_campaign" />

<script>
  const params = new URLSearchParams(location.search);
  for (const key of ["utm_source", "utm_medium", "utm_campaign"]) {
    let value = params.get(key);
    try {
      if (value) sessionStorage.setItem(key, value);
      else value = sessionStorage.getItem(key);
    } catch {}
    const input = document.querySelector(`#lead-form [name="${key}"]`);
    if (input && value) input.value = value.slice(0, 200);
  }
</script>
```

In einem Framework liest du dieselben Parameter auf dem Server aus der URL und renderst sie als versteckte Inputs. Wenn Felder definiert sind, füge jede `utm_`-ID als optionales Textfeld hinzu. Namen, die mit `_` beginnen, werden nie gespeichert, nutze sie also nicht für Daten, die du behalten willst.

## Was der Lead als Nächstes sieht

Ikiform schreibt der Person, die das Formular ausfüllt, keine E-Mail. Benachrichtigungs-E-Mails gehen nur an dich und dein Team, es gibt also keinen Autoresponder, keine Bestätigungs-E-Mail und kein Double-Opt-in. Schick diese aus deinem E-Mail-Tool, ausgelöst durch einen [Webhook](https://ikiform.com/de/docs/webhooks.md). Für ein Lead-Formular ist das oft in Ordnung: Eine schnelle persönliche Antwort schlägt eine automatische. Wenn du trotzdem eine sofortige Eingangsbestätigung willst, schick sie aus deinem CRM oder E-Mail-Tool, wenn der Webhook den Kontakt anlegt, damit Antworten und Abmeldungen an einem Ort erfasst werden.

Was auch immer du schickst, die Erfolgsmeldung auf der Seite ist das Versprechen. Sag, wer antwortet und bis wann, und halte es ein. Ein Formular, das einen Werktag verspricht und in zehn Minuten antwortet, gewinnt Vertrauen; eines, das zehn Minuten verspricht und morgen antwortet, verliert es.

## Leads in deinem CRM

Es gibt keine Ein-Klick-Integrationen. Jedes Tool wird über einen signierten [Webhook](https://ikiform.com/de/docs/webhooks.md) erreicht, direkt oder über ein Automatisierungstool wie [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) oder [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md). Für ein CRM heißt das: Ein Relay bildet Ikiforms festen Webhook-Body auf den Create-Contact-Aufruf des CRM ab. [Leads an HubSpot oder Pipedrive](https://ikiform.com/de/guides/lead-capture/leads-to-crm.md) geht beides durch, inklusive Duplikaten und Wiederholungen. Bis du ein CRM brauchst, sind das Dashboard und ein wöchentlicher [CSV-Export](https://ikiform.com/de/docs/guides/export.md) eine gute Pipeline.

## Wähle deinen Fall

Das Setup oben funktioniert für jeden Lead. Diese Seiten behandeln, was sich für eine bestimmte Art von Lead ändert und wie Leads in ein CRM kommen.

- [Formular für Demo-Anfragen](https://ikiform.com/de/guides/lead-capture/demo-request.md): Ein Formular zum Demo-Buchen, das den Lead speichert und den Besucher dann mit vorausgefülltem Namen und E-Mail zu Cal.com oder Calendly schickt.
- [Formular für Angebotsanfragen](https://ikiform.com/de/guides/lead-capture/quote-request.md): Ein Formular für Angebotsanfragen, das Briefings als geteilte Links statt als Uploads sammelt, jeder Anfrage ein Fälligkeitsdatum gibt und linklastige Leads aus dem Spam hält.
- [Formular für Beratungsanfragen](https://ikiform.com/de/guides/lead-capture/consultation-request.md): Ein Formular für Beratungsanfragen für Steuerberater, Anwälte, Coaches und Praxen, das fragt, wie und wann angerufen werden soll, und heikle Details für das Gespräch lässt.
- [Formular für Projektanfragen](https://ikiform.com/de/guides/lead-capture/project-inquiry.md): Ein Formular für neue Projekte für Studios und Freelancer, das Anfragen nach Budget und Zeitpunkt in heiß, warm und unpassend sortiert, bevor jemand das Briefing liest.
- [Leads an HubSpot oder Pipedrive](https://ikiform.com/de/guides/lead-capture/leads-to-crm.md): 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.
- [Lead-Formular mit Next.js](https://ikiform.com/de/guides/lead-capture/nextjs.md): Ein Lead-Formular für den Next.js App Router, das in einer Server Action mit Zod validiert, UTMs des ersten Kontakts in einem Cookie hält und qualifizierte Leads in deinen Kalender schickt.
- [Lead-Formular mit Astro](https://ikiform.com/de/guides/lead-capture/astro.md): Ein Lead-Formular auf Basis von Astro Actions: Validierung mit Zod, UTMs des ersten Kontakts auf einer statischen Site, eine On-Demand-Kontaktseite und eine Weiterleitung zu deinem Terminplaner.
- [Lead-Formular mit TanStack Start](https://ikiform.com/de/guides/lead-capture/tanstack-start.md): Ein Lead-Formular mit TanStack Start: globale Request-Middleware für UTMs des ersten Kontakts, eine Server-Funktion mit Zod und ein geworfener Redirect zu deinem Terminplaner.
- [Lead-Formular mit React Router](https://ikiform.com/de/guides/lead-capture/react-router.md): Ein Lead-Formular mit React Router im Framework-Modus: Root-Middleware mit signiertem Cookie für den ersten Kontakt, eine validierende Routen-Action und eine Weiterleitung zu deinem Terminplaner.
- [Lead-Formular mit SvelteKit](https://ikiform.com/de/guides/lead-capture/sveltekit.md): Ein Lead-Formular mit SvelteKit: Der handle-Hook hält UTMs des ersten Kontakts fest, eine Form Action validiert mit Valibot, und redirect() schickt qualifizierte Leads in einen Kalender.
- [Lead-Formular mit Nuxt](https://ikiform.com/de/guides/lead-capture/nuxt.md): Ein Lead-Formular mit Nuxt: Server-Middleware hält UTMs des ersten Kontakts fest, eine mit Zod validierte Server-Route bedient fetch und einfache Posts, und gute Leads buchen einen Termin.

## Prüfen, ob es funktioniert

Geh das durch, bevor du Traffic auf die Seite schickst. Ikiform hat keinen kostenlosen Tarif. Testen ist kostenlos: Ein Workspace ohne Tarif nimmt 50 Test-Einsendungen pro Monat von `localhost` an. Für den Livebetrieb wählst du Pro ab 6 $/Monat für 500 Einsendungen oder zahlst einmal: 10.000 Einsendungen für 49 $ oder 50.000 Einsendungen für 149 $, mit Guthaben, das nie verfällt. Siehe [Preise](https://ikiform.com/de/pricing).

1. Füge unter **Allowed domains** des Formulars die Domain deiner Site hinzu, plus `localhost`, während du entwickelst. Lass das Feld leer, um Posts von überall anzunehmen.
2. Füll das Formular auf deiner Seite aus und sende es ab. Du landest auf deiner Dankeseite oder der gehosteten von Ikiform.
3. Öffne **Submissions** im Dashboard. Die neue Einsendung ist da, mit jedem Feld, das du gesendet hast.
4. Prüfe den Posteingang jedes Empfängers. Der Betreff ist `New submission:` plus der Name des Formulars, und **Reply** öffnet eine Nachricht an die Adresse, die du eingegeben hast.
5. Wenn du einen Webhook hinzugefügt hast, öffne den Endpunkt unter **Webhooks**. Die Zustellung zeigt `succeeded` mit der Antwort deines Servers.
6. Sende einen Post mit ausgefülltem Honeypot, `_gotcha=bot`. Er bekommt trotzdem eine Erfolgsantwort, aber die Einsendung wird als Spam gespeichert und löst weder E-Mail noch Webhook aus.
7. Nichts angekommen? Stell den Statusfilter im Dashboard auf **Spam**. Spam bekommt dieselbe Erfolgsantwort wie eine echte Einsendung.
8. Stopp die Zeit: Sende vom Handy ab und notiere, wie lange es dauert, bis E-Mail und Chat-Nachricht ankommen.

## Fehlerbehebung

| Problem | Was zu tun ist |
| --- | --- |
| **Reply** geht an Ikiforms Adresse, nicht an den Lead | Das Formular hat kein Feld, das Ikiform als E-Mail des Leads erkennt. Benenne das Feld in `email` um oder sende ein Feld `_replyto` mit der Adresse. Akzeptierte Namen: `email`, `email_address`, `your_email`, `reply_to`, `contact_email`. |
| `422` mit `Unknown field` für `page` oder `utm_source` | Das Formular hat eine Felddefinition, und das versteckte Input steht nicht darin. Füge es als optionales Textfeld hinzu oder entferne das Input. |
| `422 validation_failed` | Der Post passt nicht zur Felddefinition des Formulars. `error.fields` nennt jedes Feld und den Grund. Gleiche den `name` des Eingabefelds an die Feld-ID an. |
| `403 origin_not_allowed`, oder jeder Post landet im Spam | Die Domain der Seite steht nicht unter **Allowed domains** des Formulars. Füge sie hinzu, plus `localhost` für lokale Tests. Subdomains einer eingetragenen Domain sind erlaubt. |
| `429 rate_limited` | Ein Besucher hat mehr gesendet, als das Rate-Limit des Formulars erlaubt. Standard erlaubt 5 pro Minute und Besucher. Warte `Retry-After` ab oder wähle Relaxed oder Custom für Formulare, die von einer gemeinsamen IP aus senden. |
| `403 challenge_required` | Die Spamprüfung des Formulars steht auf **Ikiform**, und ein `fetch`-Post hatte kein Token. Hol dir vor jedem Absenden eins mit `challenge(formId)` aus `@ikiform/sdk` oder schalte die Prüfung aus. |
| Die Einsendung ist im Dashboard, aber keine E-Mail und kein Webhook kam an | Sie wurde als Spam gespeichert. Spam löst nichts aus. Öffne sie unter dem Filter **Spam**, um die Gründe zu sehen, und markiere sie als kein Spam, um ihre E-Mail und Webhooks zu senden. |
| Ein Empfänger bekommt keine Benachrichtigungs-E-Mails | Eine Adresse außerhalb des Workspace muss zuerst bestätigen, über die E-Mail, die Ikiform ihr schickt. Prüfe, ob sie bestätigt wurde, und sieh dann im Spamordner des Empfängers nach. |

## Fragen

### Was ist ein Formular zur Lead-Erfassung?

Ein Formular auf deiner Website, das die Kontaktdaten eines potenziellen Kunden und sein Anliegen erfasst, damit dein Team nachfassen kann. Anders als ein allgemeines Kontaktformular qualifiziert es den Lead, meist mit einer Frage mit festen Optionen, und ist so verdrahtet, dass es den Vertrieb schnell erreicht.

### Wie bekomme ich Einsendungen eines Lead-Formulars per E-Mail?

Richte das Formular auf deinen Ikiform-Endpunkt aus und lass die Benachrichtigungs-E-Mails für das Formular an. Jeder Lead kommt innerhalb von Sekunden an, an bis zu 10 Empfänger, und Antworten schreibt an den Lead, solange das E-Mail-Feld email heißt oder das Formular _replyto sendet.

### Wie viele Felder sollte ein Lead-Formular haben?

So wenige, wie du für eine gute Antwort brauchst: meist Name, E-Mail, eine qualifizierende Frage und eine optionale Nachricht. Füge Telefon, Budget oder Teamgröße nur hinzu, wenn die Antwort ändert, was dein Team als Nächstes tut.

### Kann Ikiform Leads an HubSpot, Pipedrive oder Salesforce senden?

Über einen Webhook, ja. Es gibt keine Ein-Klick-Integrationen, also bildet ein kleines Relay oder ein Automatisierungstool wie Zapier, Make oder n8n jeden signierten Webhook auf die API des CRM ab und legt den Kontakt oder Lead an.

### Bekommt der Lead eine Bestätigungs-E-Mail?

Nicht von Ikiform. Ikiform schreibt deinem Team, nicht der Person, die das Formular ausgefüllt hat. Schick eine Eingangsbestätigung aus deinem CRM oder E-Mail-Tool, ausgelöst durch den Webhook, oder antworte persönlich aus der Benachrichtigungs-E-Mail.

### Kann ich kostenlose E-Mail-Adressen wie Gmail blockieren?

Nicht über eine eingebaute Einstellung. Die Option für erlaubte Domains des E-Mail-Felds ist eine Allowlist, die zu internen Formularen passt, nicht zu öffentlichen. Akzeptiere jede Adresse und sortiere Freemail-Leads stattdessen in deinem Relay oder CRM; viele echte Käufer nutzen eine private Adresse.

### Ist Ikiform kostenlos?

Nein, es gibt keinen kostenlosen Tarif. Testen ist kostenlos: Du kannst 50 Testeinsendungen pro Monat von localhost senden, bevor du zahlst. Für den Livebetrieb startet Pro bei $6/Monat für 500 Einsendungen, oder du zahlst einmalig 10.000 Einsendungen für $49 oder 50.000 Einsendungen für $149, mit Guthaben, das nie verfällt.

## Weitere Anleitungen

- [Formular für Demo-Anfragen](https://ikiform.com/de/guides/lead-capture/demo-request.md): Ein Formular zum Demo-Buchen, das den Lead speichert und den Besucher dann mit vorausgefülltem Namen und E-Mail zu Cal.com oder Calendly schickt.
- [Formular für Angebotsanfragen](https://ikiform.com/de/guides/lead-capture/quote-request.md): Ein Formular für Angebotsanfragen, das Briefings als geteilte Links statt als Uploads sammelt, jeder Anfrage ein Fälligkeitsdatum gibt und linklastige Leads aus dem Spam hält.
- [Formular für Beratungsanfragen](https://ikiform.com/de/guides/lead-capture/consultation-request.md): Ein Formular für Beratungsanfragen für Steuerberater, Anwälte, Coaches und Praxen, das fragt, wie und wann angerufen werden soll, und heikle Details für das Gespräch lässt.
- [Formular für Projektanfragen](https://ikiform.com/de/guides/lead-capture/project-inquiry.md): Ein Formular für neue Projekte für Studios und Freelancer, das Anfragen nach Budget und Zeitpunkt in heiß, warm und unpassend sortiert, bevor jemand das Briefing liest.
- [Leads an HubSpot oder Pipedrive](https://ikiform.com/de/guides/lead-capture/leads-to-crm.md): 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.
- [Lead-Formular mit Next.js](https://ikiform.com/de/guides/lead-capture/nextjs.md): Ein Lead-Formular für den Next.js App Router, das in einer Server Action mit Zod validiert, UTMs des ersten Kontakts in einem Cookie hält und qualifizierte Leads in deinen Kalender schickt.
- [Lead-Formular mit Astro](https://ikiform.com/de/guides/lead-capture/astro.md): Ein Lead-Formular auf Basis von Astro Actions: Validierung mit Zod, UTMs des ersten Kontakts auf einer statischen Site, eine On-Demand-Kontaktseite und eine Weiterleitung zu deinem Terminplaner.
- [Lead-Formular mit TanStack Start](https://ikiform.com/de/guides/lead-capture/tanstack-start.md): Ein Lead-Formular mit TanStack Start: globale Request-Middleware für UTMs des ersten Kontakts, eine Server-Funktion mit Zod und ein geworfener Redirect zu deinem Terminplaner.
- [Lead-Formular mit React Router](https://ikiform.com/de/guides/lead-capture/react-router.md): Ein Lead-Formular mit React Router im Framework-Modus: Root-Middleware mit signiertem Cookie für den ersten Kontakt, eine validierende Routen-Action und eine Weiterleitung zu deinem Terminplaner.
- [Lead-Formular mit SvelteKit](https://ikiform.com/de/guides/lead-capture/sveltekit.md): Ein Lead-Formular mit SvelteKit: Der handle-Hook hält UTMs des ersten Kontakts fest, eine Form Action validiert mit Valibot, und redirect() schickt qualifizierte Leads in einen Kalender.
- [Lead-Formular mit Nuxt](https://ikiform.com/de/guides/lead-capture/nuxt.md): Ein Lead-Formular mit Nuxt: Server-Middleware hält UTMs des ersten Kontakts fest, eine mit Zod validierte Server-Route bedient fetch und einfache Posts, und gute Leads buchen einen Termin.
- [Newsletter-Anmeldeformular](https://ikiform.com/de/guides/newsletter-signup.md): Baue ein Newsletter-Anmeldeformular mit eigenem Markup, speichere die Einwilligung, blocke Bot-Anmeldungen und gib jeden Abonnenten an Kit, MailerLite, beehiiv oder ein anderes Tool weiter.
- [Absenden von HTML-Formularen](https://ikiform.com/de/guides/html-form-submission.md): Was ein Browser beim Absenden eines Formulars tut: welche Felder er sendet, wie er sie kodiert, was der Server antworten sollte und wie Ikiform antwortet.
- [Eventanmeldung](https://ikiform.com/de/guides/event-registration.md): Nimm Zusagen auf deiner eigenen Eventseite entgegen: Formular, Begleitpersonen-Limit, ehrliche Teilnehmerzahl, Schließen bei voller Kapazität und Check-in-Liste, ohne Ticketing-App.
