Anleitungen15 Min. Lesezeit
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.
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.
- Ein Besucher füllt das Lead-Formular auf deiner Site aus, und es sendet an deinen Ikiform-Endpunkt.
- Ikiform prüft Honeypot, Spam-Score und Rate-Limit und speichert dann den Lead.
- Sekunden später bekommt jeder Empfänger der Benachrichtigung eine E-Mail, und ein Klick auf Antworten schreibt an den Lead.
- Der Lead erscheint im Dashboard, wo du ihn suchen, filtern und exportieren kannst.
- Ikiform sendet einen signierten submission.created-Webhook.
- Ein Relay oder ein Automatisierungstool postet den Lead in den Team-Chat und legt den Kontakt in deinem CRM an.
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 oder 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.
Vorschau
Führt den Validator von Ikiform in deinem Browser aus. Es wird nichts gesendet.
| 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 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.
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.
<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.
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";
});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.
"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 };
}"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.
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.
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] };
},
}),
};---
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.
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.
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.
Die Route-Action leitet den Post weiter und gibt Feldfehler mit Ikiforms Statuscode über data() zurück. Die Komponente liest sie aus actionData.
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.
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.
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;<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.
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.
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] };
});export default defineNuxtConfig({
runtimeConfig: {
ikiformSecretKey: "",
},
});<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.
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.
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>
);
}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.
<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_xxxxxxxxxxxxxxxxdurch die ID deines Formulars aus dem Dashboard. - Das versteckte Eingabefeld
_gotchaist der Honeypot. Menschen sehen es nie; Bots, die jedes Eingabefeld ausfüllen, markieren sich selbst als Spam. - Das versteckte Input
pagehält fest, von welcher Seite der Lead kam: Preise, eine Feature-Seite, eine Kampagnen-Landingpage. Wenn du Felder definierst, fügepageals optionales Textfeld hinzu, sonst wird der Post mitUnknown fieldabgelehnt. - Die Serverversionen leiten den
Origin-Header des Besuchers weiter, weil ein serverseitiger Fetch keinen sendet und Ikiform daran kostenloselocalhost-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.
IKIFORM_SECRET_KEY in reinen Server-Umgebungsvariablen.Die Doku behandelt das Setup jedes Frameworks ausführlich: Next.js, Astro, SvelteKit, Nuxt, React, Vue, Senden von einem Server und reines HTML.
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:
- Der Lead sendet das Formular ab.
- Ikiform speichert ihn und antwortet dem Browser in deutlich unter einer Sekunde.
- Benachrichtigungs-E-Mail und Webhook gehen Sekunden später aus einer Warteschlange raus.
- Jemand im Team klickt in seinem Posteingang auf Antworten oder auf den mailto-Link im Chat, und die Antwort geht an den Lead.
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.
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, Microsoft Teams oder Discord. 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:
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 });
}verifyWebhookprü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 derselbenwebhook-idetwa 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 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:
<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. 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 erreicht, direkt oder über ein Automatisierungstool wie Zapier oder n8n. 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 geht beides durch, inklusive Duplikaten und Wiederholungen. Bis du ein CRM brauchst, sind das Dashboard und ein wöchentlicher CSV-Export 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-AnfragenEin 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 AngebotsanfragenEin 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 BeratungsanfragenEin 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 ProjektanfragenEin 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 PipedriveBilde 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.jsEin 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 AstroEin 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 StartEin 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 RouterEin 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 SvelteKitEin 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 NuxtEin 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.
- 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. - Füll das Formular auf deiner Seite aus und sende es ab. Du landest auf deiner Dankeseite oder der gehosteten von Ikiform.
- Öffne Submissions im Dashboard. Die neue Einsendung ist da, mit jedem Feld, das du gesendet hast.
- 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. - Wenn du einen Webhook hinzugefügt hast, öffne den Endpunkt unter Webhooks. Die Zustellung zeigt
succeededmit der Antwort deines Servers. - 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. - Nichts angekommen? Stell den Statusfilter im Dashboard auf Spam. Spam bekommt dieselbe Erfolgsantwort wie eine echte Einsendung.
- 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
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.
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.
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.
Ü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.
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.
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.
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.
Hängst du bei einem Schritt fest oder willst eine Anleitung für deinen Fall? Schreib an hello@ikiform.com.