# So erstellst du eine Warteliste für ein neues SaaS

> Baue eine Pre-Launch-Warteliste auf deiner eigenen Landingpage: das Formular, Spamschutz, Duplikate, Bestätigungs-E-Mails und ein sauberer Export am Launch-Tag.

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

Setze ein einfaches HTML-Formular auf deine Landingpage, das die E-Mail des Besuchers an ein Formular-Backend sendet, damit du vor dem Launch keinen Server betreibst. Leite jede Anmeldung per Webhook an das E-Mail-Tool weiter, mit dem du launchst, und lass dieses Tool eine eventuelle Bestätigung senden. Exportiere am Launch-Tag die Liste als CSV, entferne Duplikate nach E-Mail und sende eine Launch-E-Mail.

Eine Warteliste ist der günstigste Test einer SaaS-Idee. Wenn Fremde ihre E-Mail für etwas hinterlassen, das es noch nicht gibt, hast du einen Grund, es zu bauen, und eine Liste von Leuten, denen du Bescheid gibst, wenn es erscheint. Diese Anleitung baut eine auf deiner eigenen Landingpage, ohne Datenbank oder Server, und macht sie bereit für den Launch-Tag.

## So funktioniert eine Warteliste

Jede Warteliste hat dieselben drei Teile: ein Formular auf deiner Seite, einen Ort, der jede Anmeldung speichert, und das Tool, mit dem du die Liste anschreibst. Hier sendet das Formular an Ikiform. Ikiform prüft den Post auf Spam, speichert ihn, benachrichtigt dich per E-Mail und sendet einen signierten Webhook. Ein kleines Relay oder ein Automatisierungstool übergibt die Adresse an dein E-Mail-Tool.

Diagramm: Wie eine Wartelisten-Anmeldung von deiner Landingpage zu deinem E-Mail-Tool gelangt

1. Ein Besucher gibt auf deiner Landingpage seine E-Mail ein, und das Formular sendet an deinen Ikiform-Endpunkt.
2. Ikiform prüft Honeypot, Spam-Score und Rate-Limit und speichert dann die Anmeldung.
3. Die Anmeldung erscheint im Dashboard, wo du sie durchsuchen und die Liste als CSV exportieren kannst.
4. Ikiform benachrichtigt dich und dein Team per E-Mail. Der Person, die sich angemeldet hat, schreibt es nie.
5. Ikiform sendet einen signierten submission.created-Webhook.
6. Ein Relay oder ein Automatisierungstool fügt die Adresse deinem E-Mail-Tool hinzu, das eine eventuelle Bestätigung und später die Launch-E-Mail sendet.

Das E-Mail-Tool ist am ersten Tag optional. Du kannst wochenlang sammeln und später eine CSV importieren; der Webhook hält beides von Anfang an synchron.

## Vier Wege, eine Warteliste zu betreiben

Es gibt nicht das eine richtige Tool. Die Tabelle vergleicht die vier üblichen Setups danach, worin sie gut sind und was sie dich später kosten.

| Ansatz | Stark bei | Nachteile |
| --- | --- | --- |
| Wartelisten-SaaS wie LaunchList oder Waitlister | Empfehlungslinks, Bestenlisten und Platz in der Warteschlange, sofort einsatzbereit. Eine gehostete Seite, falls du noch keine Site hast. | Deine Liste lebt in einem anderen Produkt. Das Widget sieht aus wie ihres, außer du zahlst oder gestaltest es um. Die Preise wachsen meist mit den Anmeldungen. |
| Eingebetteter Formular-Baukasten wie [Typeform](https://ikiform.com/alternatives/typeform) oder [Tally](https://ikiform.com/alternatives/tally) | Ausgefeilte Abläufe mit einer Frage pro Schritt, ganz ohne Code. | Ein iframe, das nicht zu deiner Seite passt und eigene Skripte lädt. Branding, Antwortlimits und Webhooks hängen vom Tarif ab. |
| Deine eigene Datenbank und API-Route | Volle Kontrolle über die Daten und jede Logik, etwa Empfehlungscodes. | Du baust Spamschutz, Rate-Limits, Benachrichtigungen und Export selbst und brauchst einen Server, auch wenn die Landingpage statisch ist. |
| Formular-Backend wie Ikiform | Dein eigenes HTML auf jeder Site. Spamfilter, Speicherung, E-Mail-Benachrichtigungen, signierte Webhooks und CSV-Export ohne Server. | Keine Empfehlungs-Bestenliste und keine E-Mails an die Person, die sich anmeldet. Die kommen von deinem E-Mail-Tool. |

Nimm ein Wartelisten-SaaS, wenn Empfehlungsmechaniken der ganze Plan sind. Nimm ein Formular-Backend, wenn das Formular aussehen soll wie der Rest deiner Site, die Liste portabel bleiben soll und du E-Mails aus dem Tool senden willst, das du nach dem Launch ohnehin nutzt. Der Rest dieser Anleitung nutzt das Formular-Backend.

## Das Formular

Frag zu Beginn nur nach einer E-Mail. Jedes zusätzliche Feld kostet Anmeldungen, und Fragen kannst du später per E-Mail an Leute stellen, die schon Ja gesagt haben. Wähle unten deinen Stack; die Wahl gilt für jedes Codebeispiel in diesen Anleitungen.

**HTML**

Füge das Formular dort ein, wo die Anmeldung erscheinen soll. Es sendet auf die einfache Art und leitet weiter, funktioniert also ganz ohne JavaScript.

`index.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" placeholder="you@company.com" autocomplete="email" required />
  <input type="hidden" name="ref" value="homepage" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Join the waitlist</button>
</form>
```

Damit Besucher auf der Seite bleiben, füge dieses Skript nach dem Formular ein. Es sendet mit `fetch`, ersetzt das Formular durch die Dankeszeile und zeigt die Feldfehler des Servers an.

`waitlist.js`:

```js
const form = document.querySelector('form[action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx"]');
const status = document.createElement("p");
status.setAttribute("role", "alert");
form.append(status);

const labels = {
  email: "Email",
};

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const button = form.querySelector("button");
  button.disabled = true;
  status.textContent = "";
  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) {
      form.replaceWith("You're on the list. We'll email you at launch.");
      return;
    }
    const fields = Object.entries(result.error.fields ?? {});
    status.textContent = fields.length
      ? fields.map(([id, messages]) => `${labels[id] ?? id}: ${messages[0]}`).join(" ")
      : result.error.message;
  } catch {
    status.textContent = "Couldn't reach the server. Try again.";
  }
  button.disabled = false;
});
```

Mehr zu HTML? Lies [So fügst du jeder Landingpage ein Wartelisten-Formular in reinem HTML hinzu](https://ikiform.com/de/guides/waitlist/html.md).

**Next.js**

Eine Server Action leitet die Anmeldung mit deinem Secret Key weiter, und `useActionState` zeigt das Ergebnis ohne Neuladen der Seite. Das Formular sendet trotzdem, wenn JavaScript noch nicht geladen ist.

`app/waitlist/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";

export interface JoinState {
  ok: boolean;
  message: string;
  email?: string;
}

export async function joinWaitlist(_previous: JoinState, formData: FormData): Promise<JoinState> {
  const email = String(formData.get("email") ?? "").trim();
  if (!email.includes("@")) return { ok: false, message: "Enter the email we should send your invite to.", email };

  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: (await headers()).get("origin") ?? "",
    },
    body: JSON.stringify({
      email,
      ref: String(formData.get("ref") ?? "homepage"),
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json().catch(() => null);
  if (result?.ok) return { ok: true, message: "You're on the list. We'll email you on launch day." };
  return {
    ok: false,
    message: result?.error?.fields?.email?.[0] ?? result?.error?.message ?? "Couldn't join the list. Try again.",
    email,
  };
}
```

`app/waitlist/waitlist-form.tsx`:

```tsx
"use client";

import { useActionState } from "react";
import { type JoinState, joinWaitlist } from "./actions";

const initial: JoinState = { ok: false, message: "" };

export function WaitlistForm() {
  const [state, action, pending] = useActionState(joinWaitlist, initial);

  if (state.ok) return <p role="status">{state.message}</p>;

  return (
    <form action={action}>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required defaultValue={state.email} placeholder="you@company.com" />
      <input type="hidden" name="ref" value="homepage" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state.message ? <p role="alert">{state.message}</p> : null}
      <button disabled={pending}>{pending ? "Joining" : "Join the waitlist"}</button>
    </form>
  );
}
```

Mehr zu Next.js? Lies [So baust du eine Next.js-Warteliste mit einer Server Action](https://ikiform.com/de/guides/waitlist/nextjs.md).

**Astro**

Eine Astro Action mit `accept: "form"` validiert die E-Mail mit Zod und leitet sie weiter. Das Formular sendet ohne Client-JavaScript, die Seite muss also bei Bedarf gerendert werden: Füge einen Adapter hinzu und behalte `prerender = false`.

`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 = {
  joinWaitlist: defineAction({
    accept: "form",
    input: z.object({
      email: z.email(),
      ref: z.string().default("homepage"),
      _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.fields?.email?.[0] ?? result.error.message,
        });
      }
      return { joined: true };
    },
  }),
};
```

`src/pages/index.astro`:

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

export const prerender = false;

const result = Astro.getActionResult(actions.joinWaitlist);
---

{result?.data?.joined ? (
  <p role="status">You're on the list. We'll email you on launch day.</p>
) : (
  <form method="POST" action={actions.joinWaitlist}>
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required placeholder="you@company.com" />
    <input type="hidden" name="ref" value="homepage" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {result?.error && (
      <p role="alert">{isInputError(result.error) ? "Enter a valid email address." : result.error.message}</p>
    )}
    <button>Join the waitlist</button>
  </form>
)}
```

Mehr zu Astro? Lies [Eine Warteliste in Astro bauen](https://ikiform.com/de/guides/waitlist/astro.md).

**TanStack Start**

Eine `createServerFn`-POST-Funktion empfängt die `FormData`, prüft sie in `.validator()` und leitet sie vom Server weiter. Die Route ruft sie über `useServerFn` auf.

`src/routes/index.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";

const joinWaitlist = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    const email = String(data.get("email") ?? "").trim();
    if (!email.includes("@")) throw new Error("Enter the email we should send your invite to.");
    return {
      email,
      ref: String(data.get("ref") ?? "homepage"),
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
  })
  .handler(async ({ data }) => {
    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, message: "You're on the list. We'll email you on launch day." };
    return { ok: false, message: String(result.error.fields?.email?.[0] ?? result.error.message) };
  });

export const Route = createFileRoute("/")({ component: Home });

function Home() {
  const join = useServerFn(joinWaitlist);
  const [state, setState] = useState<{ ok: boolean; message: string } | null>(null);
  const [pending, setPending] = useState(false);

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    setPending(true);
    try {
      setState(await join({ data }));
    } catch (error) {
      setState({ ok: false, message: error instanceof Error ? error.message : "Couldn't join the list. Try again." });
    } finally {
      setPending(false);
    }
  }

  if (state?.ok) return <p role="status">{state.message}</p>;

  return (
    <form method="post" onSubmit={onSubmit}>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required placeholder="you@company.com" />
      <input type="hidden" name="ref" value="homepage" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state?.message ? <p role="alert">{state.message}</p> : null}
      <button disabled={pending}>{pending ? "Joining" : "Join the waitlist"}</button>
    </form>
  );
}
```

Mehr zu TanStack Start? Lies [Eine Warteliste in TanStack Start bauen](https://ikiform.com/de/guides/waitlist/tanstack-start.md).

**React Router**

Die `action` der Route leitet den Post weiter, und `actionData` bringt das Ergebnis zurück zur Komponente. `data()` behält bei Fehlern den Statuscode von Ikiform.

`app/routes/home.tsx`:

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

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const email = String(formData.get("email") ?? "").trim();

  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: request.headers.get("origin") ?? "",
    },
    body: JSON.stringify({
      email,
      ref: String(formData.get("ref") ?? "homepage"),
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json();
  if (result.ok) return { ok: true, message: "You're on the list. We'll email you on launch day." };
  return data(
    { ok: false, message: String(result.error.fields?.email?.[0] ?? result.error.message) },
    { status: response.status },
  );
}

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

  if (actionData?.ok) return <p role="status">{actionData.message}</p>;

  return (
    <Form method="post">
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required placeholder="you@company.com" />
      <input type="hidden" name="ref" value="homepage" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {actionData?.message ? <p role="alert">{actionData.message}</p> : null}
      <button disabled={pending}>{pending ? "Joining" : "Join the waitlist"}</button>
    </Form>
  );
}
```

Mehr zu React Router? Lies [Eine Warteliste in React Router bauen](https://ikiform.com/de/guides/waitlist/react-router.md).

**SvelteKit**

Eine Standard-Form-Action leitet den Post weiter und gibt bei Fehlern `fail()` mit der Meldung zurück. `use:enhance` sendet ohne Neuladen und behält die eingegebene E-Mail im Eingabefeld. Deklariere in SvelteKit 3 `IKIFORM_SECRET_KEY` in `src/env.ts` mit `defineEnvVars`, damit `$app/env/private` ihn lesen kann.

`src/routes/+page.server.ts`:

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

export const actions = {
  default: async ({ request, fetch, url }) => {
    const data = await request.formData();
    const email = String(data.get("email") ?? "").trim();
    if (!email.includes("@")) return fail(400, { email, message: "Enter the email we should send your invite to." });

    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({
        email,
        ref: String(data.get("ref") ?? "homepage"),
        _gotcha: String(data.get("_gotcha") ?? ""),
      }),
    });
    const result = await response.json();
    if (!result.ok) {
      return fail(response.status, {
        email,
        message: String(result.error.fields?.email?.[0] ?? result.error.message),
      });
    }
    return { joined: true };
  },
} satisfies Actions;
```

`src/routes/+page.svelte`:

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

  let { form }: PageProps = $props();
  let pending = $state(false);
</script>

{#if form?.joined}
  <p role="status">You're on the list. We'll email you on launch day.</p>
{:else}
  <form
    method="POST"
    use:enhance={() => {
      pending = true;
      return async ({ update }) => {
        await update();
        pending = false;
      };
    }}
  >
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required value={form?.email ?? ""} placeholder="you@company.com" />
    <input type="hidden" name="ref" value="homepage" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {#if form?.message}
      <p role="alert">{form.message}</p>
    {/if}
    <button disabled={pending}>{pending ? "Joining" : "Join the waitlist"}</button>
  </form>
{/if}
```

Mehr zu SvelteKit? Lies [Eine Warteliste in SvelteKit bauen](https://ikiform.com/de/guides/waitlist/sveltekit.md).

**Nuxt**

Eine Server-Route hält den Secret Key vom Browser fern. Setze `NUXT_IKIFORM_SECRET_KEY` in der Umgebung, und Nuxt füllt daraus `runtimeConfig.ikiformSecretKey`.

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

```ts
export default defineEventHandler(async (event) => {
  const body = await readBody<{ email?: string; ref?: string; _gotcha?: string }>(event);
  const config = useRuntimeConfig(event);
  const origin = getRequestHeader(event, "origin");

  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({
      email: body.email?.trim() ?? "",
      ref: body.ref ?? "homepage",
      _gotcha: body._gotcha ?? "",
    }),
  });
  const result = await response.json();
  if (!result.ok) {
    throw createError({
      status: response.status,
      message: String(result.error.fields?.email?.[0] ?? result.error.message),
    });
  }
  return { joined: true };
});
```

`nuxt.config.ts`:

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

`app/components/WaitlistForm.vue`:

```vue
<script setup lang="ts">
const email = ref("");
const trap = ref("");
const status = ref<"idle" | "sending" | "joined">("idle");
const error = ref("");

async function join() {
  status.value = "sending";
  error.value = "";
  try {
    await $fetch("/api/waitlist", {
      method: "POST",
      body: { email: email.value, ref: "homepage", _gotcha: trap.value },
    });
    status.value = "joined";
  } catch (failure) {
    error.value = (failure as { data?: { message?: string } }).data?.message ?? "Couldn't join the list. Try again.";
    status.value = "idle";
  }
}
</script>

<template>
  <p v-if="status === 'joined'" role="status">You're on the list. We'll email you on launch day.</p>
  <form v-else @submit.prevent="join">
    <label for="email">Email</label>
    <input id="email" v-model="email" name="email" type="email" required placeholder="you@company.com" />
    <input v-model="trap" name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="error" role="alert">{{ error }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Joining" : "Join the waitlist" }}</button>
  </form>
</template>
```

Mehr zu Nuxt? Lies [Eine Warteliste in Nuxt bauen](https://ikiform.com/de/guides/waitlist/nuxt.md).

**React**

Eine Single-Page-App hat keinen eigenen Server, das Formular sendet also direkt aus dem Browser an Ikiform. Schreib nie einen Secret Key in diesen Code; stattdessen schützt **Allowed domains** das Formular.

`src/WaitlistForm.tsx`:

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

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

export function WaitlistForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "joined">("idle");
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const body = new FormData(event.currentTarget);
    setStatus("sending");
    setError("");
    try {
      const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
      const result = await response.json();
      if (result.ok) {
        setStatus("joined");
        return;
      }
      setError(result.error.fields?.email?.[0] ?? result.error.message);
    } catch {
      setError("Couldn't reach the server. Try again.");
    }
    setStatus("idle");
  }

  if (status === "joined") return <p role="status">You're on the list. We'll email you on launch day.</p>;

  return (
    <form action={ENDPOINT} method="post" onSubmit={onSubmit}>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required placeholder="you@company.com" />
      <input type="hidden" name="ref" value="homepage" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error ? <p role="alert">{error}</p> : null}
      <button disabled={status === "sending"}>{status === "sending" ? "Joining" : "Join the waitlist"}</button>
    </form>
  );
}
```

**Vue**

Derselbe browserseitige Post in einer Vue-Single-File-Component. Das Formular behält seine `action`, funktioniert also auch, wenn das Skript nicht lädt.

`src/components/WaitlistForm.vue`:

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

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

const status = ref<"idle" | "sending" | "joined">("idle");
const error = ref("");

async function join(event: SubmitEvent) {
  const body = new FormData(event.target as HTMLFormElement);
  status.value = "sending";
  error.value = "";
  try {
    const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
    const result = await response.json();
    if (result.ok) {
      status.value = "joined";
      return;
    }
    error.value = result.error.fields?.email?.[0] ?? result.error.message;
  } catch {
    error.value = "Couldn't reach the server. Try again.";
  }
  status.value = "idle";
}
</script>

<template>
  <p v-if="status === 'joined'" role="status">You're on the list. We'll email you on launch day.</p>
  <form v-else :action="ENDPOINT" method="post" @submit.prevent="join">
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required placeholder="you@company.com" />
    <input type="hidden" name="ref" value="homepage" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="error" role="alert">{{ error }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Joining" : "Join the waitlist" }}</button>
  </form>
</template>
```

- Der Endpunkt ist die URL deines Formulars. Ersetze `frm_xxxxxxxxxxxxxxxx` durch die ID aus dem Dashboard.
- `name="email"` ist die Feld-ID. Lass sie bei `email`: Ikiform nutzt sie als Reply-to-Adresse der Benachrichtigungs-E-Mails und prüft sie gegen Wegwerf-E-Mail-Domains.
- Das versteckte Eingabefeld `_gotcha` ist der Honeypot. Menschen sehen es nie. Bots, die jedes Eingabefeld ausfüllen, markieren sich selbst als Spam, und die Server-Versionen reichen es unverändert weiter.
- Das versteckte Eingabefeld `ref` hält fest, woher die Anmeldung kam. Ändere seinen Wert pro Seite oder Kampagne.
- Mit einer Felddefinition im Tab **Fields** wird ein Eingabefeld, das sie nicht auflistet, mit `422` und einem `unknown_field`-Fehler abgelehnt, liste also `ref` ebenso auf wie `email`, wie unten.
- Ein einfacher HTML-Post leitet den Browser nach dem Erfolg weiter. Setze **Redirect after submit** in den Einstellungen des Formulars auf deine eigene Dankeseite, sonst zeigt Ikiform seine gehostete.

`Felder`:

```json
{
  "version": 1,
  "title": "Waitlist",
  "fields": [
    { "id": "email", "type": "email", "label": "Email", "required": true, "placeholder": "you@company.com" },
    { "id": "ref", "type": "text", "label": "Source", "validation": { "max_length": 40 } }
  ]
}
```

> 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 Server-Versionen reichen den `Origin`-Header des Browsers weiter, weil Ikiform kostenlose `localhost`-Tests daran erkennt und das eigene `fetch` eines Servers keinen sendet.

Die Framework-Anleitungen in der Doku gehen tiefer: [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) und [reines HTML](https://ikiform.com/de/docs/html.md). Das [SDK](https://ikiform.com/de/docs/sdk.md) kapselt den Browser-Request als `submit()`, und die [Wartelisten-Formularvorlage](https://ikiform.com/forms/waitlist) erzeugt die Version mit vier Feldern.

## Felder, nach denen es sich zu fragen lohnt

E-Mail ist das einzige Feld, das du brauchst. Wenn du die Liste vor dem Launch sortieren willst, füge höchstens zwei weitere hinzu. Probier unten die Version mit vier Feldern aus: Sie führt Ikiforms Validator in deinem Browser aus und zeigt, was gespeichert würde, oder den `422`, den du zurückbekämst.

Formular: Waitlist

- **E-Mail** (`email`, email, Pflichtfeld)
- **Name** (`name`, text)
- **Was machst du?** (`role`, select)
- **Wie hast du von uns erfahren?** (`source`, text)
- **Quelle** (`ref`, text)

Die Felder der [Wartelisten-Vorlage](https://ikiform.com/forms/waitlist) plus `ref`, das deine Seite mit einem versteckten Eingabefeld statt eines sichtbaren Felds füllt. Füge es im Tab **Fields** des Formulars ein, und Ikiform setzt es auf dem Server durch.

| Feld | Warum du fragen würdest | Beachte |
| --- | --- | --- |
| `email` | Die Liste selbst | Pflicht. Nutze `type="email"`, damit Handys die richtige Tastatur zeigen |
| `name` | Eine persönliche Launch-E-Mail | Optional. Ein Vorname reicht |
| `role` | Gründer zuerst einladen oder Entwicklern eine andere E-Mail schreiben | Ein `select` hält die Antworten sauber zum Filtern |
| `source` | Erfahren, welcher Kanal funktioniert, bevor du Geld dafür ausgibst | Freitext ist ehrlich, aber unordentlich. Ein verstecktes `ref`-Eingabefeld ist sauberer, siehe unten |
| `ref` | Anmeldungen pro Seite oder Kampagne zählen, ohne zu fragen | Ein verstecktes Eingabefeld. Optionaler `text` mit `max_length` 40 |

Um ohne Nachfrage zu verfolgen, woher Anmeldungen kommen, füge pro Seite oder Kampagne ein verstecktes Eingabefeld hinzu. Namen, die mit `_` beginnen, sind Anweisungen an Ikiform und werden nie gespeichert, nutze also einen einfachen Namen wie `ref`:

`Versteckte Eingabefelder`:

```html
<input type="hidden" name="ref" value="homepage" />
<input type="hidden" name="_redirect" value="https://example.com/thanks" />
```

Jede Einsendung hält außerdem Origin und Referrer der Seite fest, du siehst also auch ohne `ref`, von welcher Seite eine Anmeldung kam. Das Eingabefeld `_redirect` schickt den Browser auf deine eigene Dankeseite; es funktioniert nur für eine `https:`-URL auf einer der **Allowed domains** des Formulars.

> Wenn du mehr als Launch-Neuigkeiten senden wirst, etwa einen Newsletter, füge eine Pflicht-Checkbox für die Einwilligung hinzu, mit dem genauen Versprechen als Beschriftung. Ikiform speichert `true` mit dem Zeitstempel der Einsendung, was festhält, wann die Person zugestimmt hat. Die [Newsletter-Vorlage](https://ikiform.com/forms/newsletter-signup) hat eine.

## Halte Bots von der Liste fern

Ein öffentliches Formular mit E-Mail-Feld zieht innerhalb von Tagen Bots an. Falsche Anmeldungen blähen deine Zahlen auf, und schlimmer noch, deine Launch-E-Mail landet in Spam-Fallen, die deiner Absender-Reputation schaden. Ikiform stapelt vier Schutzschichten; die ersten beiden sind ohne Einrichtung aktiv.

| Abwehr | Was sie stoppt | Einrichtung |
| --- | --- | --- |
| Honeypot | Einfache Bots, die jedes Eingabefeld ausfüllen | Das versteckte Eingabefeld `_gotcha` im Formular oben |
| Spam-Bewertung | Wegwerfadressen (+2), identische Wiederholungen (+3), Links und bekannte Spam-Phrasen. Ab 5 wird eine Anmeldung als Spam gespeichert | Keine |
| Rate-Limits | Skripte, die in einer Schleife senden | Standardmäßig aktiv: Standard, 5 pro Minute pro Besucher, und 300 pro Minute für das ganze Formular |
| Menschlichkeitsprüfung | Bots, die einen echten Browser ausführen | Setze **Spam check** auf **Ikiform**. Es führt Cloudflare Turnstile aus, ohne dass du Schlüssel verwalten musst |

Spam wird mit einer normalen Erfolgsantwort gespeichert, ein Bot erfährt also nichts. Er löst keine E-Mail und keinen Webhook aus und zählt nicht zu deiner Nutzung. Schau im ersten Monat einmal pro Woche in den Filter **Spam** im Dashboard; landet dort eine echte Anmeldung, markiere sie als kein Spam, und ihr Webhook geht dann raus.

Füge deine Domain auch unter **Allowed domains** hinzu. Posts von anderen Sites werden dann als Spam markiert oder mit `403 origin_not_allowed` abgelehnt, wenn du **Reject** wählst. Mit aktiver Menschlichkeitsprüfung braucht das HTML-Formular keine Änderung. Die `fetch`-Version muss zuerst mit `challenge()` aus `@ikiform/sdk` ein Token anfordern; die [Spam-Doku](https://ikiform.com/de/docs/spam.md#human-check) zeigt die vier Zeilen.

> Launch-Posts kommen in Schüben. Wenn du die Seite irgendwo Großes postest, kann das formularweite Limit von 300 pro Minute greifen, bevor es das Limit eines einzelnen Besuchers tut. Erhöhe es am Tag davor mit einem Custom-Rate-Limit. Ein ganzes Büro oder eine Konferenz teilt sich eine IP, für solche Tage passt also Relaxed, 20 pro Minute pro Besucher.

## Dieselbe Person, zweimal

Leute melden sich erneut an, aus einem anderen Tab, von einem zweiten Gerät oder eine Woche später, weil sie es vergessen haben. Ikiform speichert jeden angenommenen Post und führt Wiederholungen nach E-Mail weder zusammen noch lehnt es sie ab. Das passiert mit einer Wiederholung:

| Fall | Ergebnis |
| --- | --- |
| Gleiche E-Mail, andere Felder oder mehr als 24 Stunden später | Eine zweite, normale Einsendung |
| Dieselben Felder erneut innerhalb von 24 Stunden | +3 auf den Spam-Score, unter 5, also eine zweite normale Einsendung |
| Eine identische Wiederholung von einer Wegwerf-E-Mail-Domain | +3 und +2, das ergibt 5: als Spam gespeichert |

Rechne also mit einigen Duplikaten und entferne sie dort, wo die Liste landet. Drei Stellen funktionieren, vom geringsten zum größten Aufwand:

1. **Im E-Mail-Tool.** Die meisten E-Mail-Tools identifizieren Kontakte über die Adresse, dieselbe Adresse zweimal hinzuzufügen aktualisiert also einen Kontakt, statt zwei anzulegen.
2. **Beim Export.** Entferne doppelte E-Mails aus der CSV, bevor du sie irgendwo importierst. Zwei Wege stehen unten.
3. **In deinem Relay.** Schreib die Adresse klein und trimme sie, und füge sie dann mit einem Unique-Constraint oder einem Upsert ein, sodass eine Wiederholung nichts bewirkt.

Willst du verhindern, dass ein Doppelklick überhaupt zwei Zeilen erzeugt? Sende mit der `fetch`-Version pro Seitenaufruf einen [Idempotenzschlüssel](https://ikiform.com/de/docs/frameworks.md#idempotency). Das stoppt versehentliche Wiederholungen; es hindert niemanden daran, sich absichtlich zweimal anzumelden.

## Anmeldungen bestätigen

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. Sende diese aus deinem E-Mail-Tool, ausgelöst durch einen [Webhook](https://ikiform.com/de/docs/webhooks.md). Das ist Absicht: Das Tool, das deine Launch-E-Mail sendet, sollte auch die Bestätigung senden, damit Abmeldungen und Bounces an einem Ort erfasst werden.

Zwei Wege, Ikiform damit zu verbinden:

- **Ohne Code.** Füge einen Webhook-Endpunkt hinzu, der auf ein Automatisierungstool zeigt, und nutze dann dessen Action für dein E-Mail-Tool. Siehe [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md), [Make](https://ikiform.com/blog/form-submissions-to-make.md) oder [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md).
- **Ein kleines Relay.** Ein Route-Handler auf einem beliebigen Node-Host verifiziert die Signatur und ruft die API deines E-Mail-Tools auf. Das Beispiel unten ist eine Next.js-Route; derselbe Code läuft in einem Cloudflare Worker.

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

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

export async function POST(request: Request) {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: process.env.IKIFORM_WEBHOOK_SECRET!,
    });
    if (event.type !== "submission.created") return new Response(null, { status: 204 });
    const email = String(event.data.fields.email ?? "").trim().toLowerCase();
    if (!email) return new Response(null, { status: 204 });
    const response = await fetch(process.env.LIST_API_URL!, {
      method: "POST",
      headers: {
        authorization: `Bearer ${process.env.LIST_API_KEY}`,
        "content-type": "application/json",
      },
      body: JSON.stringify({
        email,
        name: event.data.fields.name ?? null,
        tags: ["waitlist", String(event.data.fields.role ?? "unknown")],
      }),
    });
    if (response.status === 409) return new Response(null, { status: 204 });
    if (!response.ok) return new Response(`List answered ${response.status}`, { status: 502 });
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
}
```

- `verifyWebhook` prüft Signatur und Zeitstempel, damit niemand sonst über diese URL Adressen hinzufügen kann.
- Ersetze das `fetch` durch den Kontakt-hinzufügen-Aufruf deines E-Mail-Tools. Die meisten nehmen eine E-Mail und eine Liste oder einen Tag und senden ihre eigene Bestätigungs-E-Mail, wenn in diesem Tool Double-Opt-in aktiv ist.
- Mit `204` zu antworten sagt Ikiform, dass die Zustellung geklappt hat. Ein `502` lässt Ikiform es erneut versuchen, mit derselben `webhook-id`, ein kurzer Ausfall deines E-Mail-Tools kostet also keine Anmeldungen.

Füge den Endpunkt im Dashboard unter **Webhooks** hinzu und kopiere sein `whsec_`-Secret in `IKIFORM_WEBHOOK_SECRET`. Ein Workspace kann 3 Endpunkte haben. Die [Webhook-Doku](https://ikiform.com/de/docs/webhooks.md) behandelt Wiederholungen und die Payload, und [dein eigener Server](https://ikiform.com/blog/form-submissions-to-your-own-server.md) hat eine Version, die jede Zustellung genau einmal speichert.

## Launch-Tag

Diagramm: Eine Warteliste von der ersten Anmeldung bis zum Launch-Tag

1. Die Seite geht live, und das Formular sendet an Ikiform.
2. Teile die Seite jede Woche und lies die neuen Anmeldungen und ihre Antworten.
3. Exportiere eine Woche vor dem Launch die Liste als CSV, entferne Duplikate und teile sie nach Rolle auf.
4. Sende am Launch-Tag eine E-Mail aus deinem E-Mail-Tool an die Liste.

Die meiste Arbeit steckt im Export eine Woche vorher. Die Launch-E-Mail selbst ist kurz.

### Die Liste exportieren

Öffne im Dashboard **Submissions**, wähle das Wartelisten-Formular, setze den Status auf **Received** und den Zeitraum auf **All time** und wähle dann **Export CSV**. Die Spalten sind `id`, `created_at`, `form` und `status`, dann eine Spalte pro Feld. Die Datei ist UTF-8 mit Byte Order Mark, Umlaute und Akzente überstehen also Excel. Ein Export umfasst bis zu 100.000 Zeilen; darüber hinaus blätterst du über die [REST-API](https://ikiform.com/de/docs/guides/export.md#api).

Um Duplikate in Google Sheets zu entfernen, importiere die CSV und nutze diese Formel in einem leeren Tabellenblatt, mit der E-Mail in Spalte E:

`Google Sheets`:

```text
=SORTN(A2:F, 9^9, 2, LOWER(TRIM(E2:E)), TRUE)
```

Sie behält eine Zeile pro Adresse, ignoriert Groß- und Kleinschreibung sowie Leerzeichen und sortiert nach E-Mail. Oder führe das hier mit Node in dem Ordner aus, der den Export enthält. Es behält die erste Zeile pro Adresse und setzt voraus, dass Werte keine Kommas enthalten, was für ein reines E-Mail-Formular zutrifft:

`dedupe.mjs`:

```js
import { readFileSync, writeFileSync } from "node:fs";

const [header = "", ...rows] = readFileSync("submissions.csv", "utf8").replace(/^\uFEFF/, "").trim().split(/\r?\n/);
const columns = header.split(",");
const emailAt = columns.indexOf("email");
const seen = new Set();
const unique = rows.filter((row) => {
  const email = row.split(",")[emailAt]?.trim().toLowerCase();
  if (!email || seen.has(email)) return false;
  seen.add(email);
  return true;
});

writeFileSync("waitlist.csv", [header, ...unique].join("\n"));
console.log(`${rows.length} rows, ${unique.length} unique emails`);
```

### Die Launch-E-Mail senden

Sende sie aus deinem E-Mail-Tool, nicht aus deinem eigenen Postfach. Ein privates Postfach, das gleichzeitig an Hunderte Adressen sendet, sieht für Mail-Anbieter wie Spam aus, und sobald du sendest, brauchst du eine Abmeldefunktion. Halte sie kurz:

`Launch-E-Mail`:

```text
Subject: Acme is live. You're in.

Hi Ada,

You joined the Acme waitlist in the spring. Today it's open.

Sign in here: https://acme.example/start

You get the first 30 days free because you waited.
Reply to this email if anything breaks. It comes to me.

Ada, founder of Acme
```

- Sag, was live ist, und verlinke direkt auf die Registrierungs- oder Anmeldeseite.
- Belohne das Warten, etwa mit einer längeren Testphase oder einem Rabatt, und sag es einmal.
- Sende sie von einer Person. Antworten sind das beste frühe Feedback, das du bekommst.
- Gib Gründern und Entwicklern unterschiedliche erste Zeilen, wenn du `role` gesammelt hast. Genau dafür sammelst du es.

> Wenn du monatelang gesammelt hast, ohne Adressen zu bestätigen, sende zuerst an einen kleinen Teil und beobachte die Bounce-Rate. Eine Liste voller Tippfehler und toter Adressen schadet der Domain, von der du Produkt-E-Mails senden wirst.

## Wähle deinen Fall

Die Schritte oben funktionieren überall. Diese Seiten behandeln, was sich für einen bestimmten Launch oder Website-Baukasten ändert.

- [Launch-Liste für Product Hunt](https://ikiform.com/de/guides/waitlist/product-hunt.md): Sammle Wochen vor Product Hunt eine Launch-Liste, markiere jede Anmeldung nach Kanal und sende die Launch-E-Mail in zwei Wellen, abgestimmt auf den Reset nach Pacific Time.
- [Beta-Anmeldung mit Auswahl](https://ikiform.com/de/guides/waitlist/beta-signup.md): Ein Beta-Bewerbungsformular mit Auswahlfragen, die der Server durchsetzt, und ein Skript, das Bewerber bewertet und jede Kohorte aus der Liste auswählt.
- [Warteliste für mobile Apps](https://ikiform.com/de/guides/waitlist/mobile-app.md): Eine Pre-Launch-Warteliste für eine iOS- oder Android-App, die fragt, welches Handy jemand nutzt, iPhone-Tester zu TestFlight schickt und einen Play-Closed-Test füllt.
- [Warteliste mit Next.js](https://ikiform.com/de/guides/waitlist/nextjs.md): Eine Warteliste mit dem Next.js App Router: eine Server Action, die Anmeldungen mit Secret Key weiterleitet, useActionState, ein optimistisches Danke und ein Limit pro Besucher.
- [Warteliste auf Framer](https://ikiform.com/de/guides/waitlist/framer.md): Zwei Wege, auf Framer Wartelisten-Anmeldungen zu sammeln: Framers Formular mit einem Webhook an Ikiform oder eine Code-Komponente, die Limits pro Besucher und Spamprüfungen behält.
- [Warteliste auf Webflow](https://ikiform.com/de/guides/waitlist/webflow.md): Richte einen Webflow Form Block auf Ikiform aus, markiere Anmeldungen mit einem versteckten Feld nach Quelle und behalte mit einem kurzen Skript Webflows Erfolgszustand für jedes Formular.
- [Warteliste auf Carrd](https://ikiform.com/de/guides/waitlist/carrd.md): Sammle Wartelisten-Anmeldungen auf einer Carrd-Seite: ein Send-to-URL-Formular auf Pro Plus mit UTM-Tracking oder ein eingebettetes HTML-Formular mit Honeypot auf Pro Standard.
- [Warteliste in reinem HTML](https://ikiform.com/de/guides/waitlist/html.md): Eine komplette Wartelisten-Seite in reinem HTML ohne Build, die ohne JavaScript funktioniert, nach dem Laden ein Danke inline zeigt und bei Bedarf zurückfällt.
- [Astro-Warteliste](https://ikiform.com/de/guides/waitlist/astro.md): Eine Astro-Warteliste auf einer statischen Landingpage: Eine Action leitet jede Anmeldung weiter, ein Skript ruft sie auf, und eine On-Demand-Seite fängt Posts ohne JavaScript ab.
- [Warteliste mit TanStack Start](https://ikiform.com/de/guides/waitlist/tanstack-start.md): Eine TanStack-Start-Warteliste: Eine Server-Route beantwortet sowohl einen einfachen Formular-Post als auch fetch, und validateSearch trägt Quelle und Ergebnis in der URL.
- [React-Router-Warteliste](https://ikiform.com/de/guides/waitlist/react-router.md): Eine React-Router-Warteliste im Footer jeder Seite: Ein Fetcher sendet an eine Route-Action, markiert jede Anmeldung mit ihrer Seite und funktioniert ohne JavaScript.
- [Warteliste mit SvelteKit](https://ikiform.com/de/guides/waitlist/sveltekit.md): Eine SvelteKit-3-Warteliste: Ein Hook behält das erste ?ref= in einem Cookie, eine benannte Form-Action leitet die Anmeldung weiter, und wiederkehrende Besucher sehen, dass sie dabei sind.
- [Nuxt-Warteliste](https://ikiform.com/de/guides/waitlist/nuxt.md): Eine Nuxt-Warteliste auf einer vorgerenderten Landingpage: Eine Nitro-Route nimmt $fetch und einfache Formular-Posts an, prüft sie mit Zod und leitet ohne JavaScript weiter.

## Prüfen, ob es funktioniert

Geh das durch, bevor du die Seite teilst. Ikiform hat keinen kostenlosen Tarif. Testen ist kostenlos: Ein Workspace ohne Tarif nimmt 50 Testeinsendungen pro Monat von `localhost` an. Für den Livebetrieb wähle Pro ab $6/Monat für 500 Einsendungen oder zahle einmalig: 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 das Postfach jedes Benachrichtigungsempfängers. Jeder bekommt eine E-Mail mit den Feldern, und **Reply** geht an den Absender, wenn das Formular ein `email`-Feld hat.
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. Melde dich zweimal mit derselben Adresse an und prüfe, dass dein E-Mail-Tool weiterhin einen Kontakt zeigt.

## Fehlerbehebung

| Problem | Was zu tun ist |
| --- | --- |
| `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. |
| Der Browser landet auf der Dankeseite von Ikiform statt auf deiner | `_redirect` wird ignoriert, außer seine Domain steht unter **Allowed domains** und es ist eine `https:`-URL. Füge die Domain hinzu oder setze stattdessen **Redirect after submit** in den Einstellungen des Formulars. |
| Dieselbe E-Mail erscheint zweimal | Zu erwarten. Ikiform dedupliziert nicht nach E-Mail. Entferne Wiederholungen beim Export oder mach in deinem Relay oder E-Mail-Tool einen Upsert nach E-Mail. |
| Anmeldungen kommen in Ikiform an, aber nicht im E-Mail-Tool | Öffne den Endpunkt unter **Webhooks** und lies das Zustellungsprotokoll. Ein `400` heißt, das Secret ist falsch; ein `502` mit dem Status deines Tools heißt, das Tool hat den Kontakt abgelehnt. |
| `402 test_limit_reached` | Der Workspace hat keinen Tarif und hat seine 50 kostenlosen `localhost`-Tests diesen Monat aufgebraucht. Wähle einen Tarif oder nutze **Send test** im Dashboard. |

## Fragen

### Brauche ich ein Backend, um eine Wartelisten-Seite zu erstellen?

Keinen eigenen Server. Das Formular braucht ein Ziel für den Post, und genau das ist ein Formular-Backend: Richte die action des Formulars auf deinen Ikiform-Endpunkt aus, und es speichert, filtert und leitet jede Anmeldung weiter. Deine Landingpage kann statisch bleiben.

### Sendet Ikiform Leuten, die sich auf die Warteliste setzen, eine Bestätigungs-E-Mail?

Nein. Ikiform schreibt dir und deinem Team, nicht der Person, die sich anmeldet, es gibt also keine Bestätigungs- oder Double-Opt-in-E-Mail. Sende jede Anmeldung per Webhook an dein E-Mail-Tool und aktiviere dort Double-Opt-in.

### Wie verhindere ich falsche Anmeldungen in einem Wartelisten-Formular?

Behalte den versteckten _gotcha-Honeypot, lass das Standard-Rate-Limit aktiv, füge deine Domain unter Allowed domains hinzu und setze die Spamprüfung auf Ikiform, für eine Cloudflare-Turnstile-Prüfung ohne Schlüsselverwaltung. Wegwerfadressen erhöhen außerdem den Spam-Score.

### Wird dieselbe E-Mail zweimal gespeichert?

Das kann passieren. Ikiform behält jeden angenommenen Post und führt Wiederholungen nach E-Mail nicht zusammen. Dedupliziere dort, wo die Liste lebt: in deinem E-Mail-Tool, in einer Unique-Spalte deiner Datenbank oder in der CSV, bevor du sie importierst.

### Kann ich ein Empfehlungsprogramm oder einen Platz in der Warteschlange hinzufügen?

Nicht mit Ikiform allein. Es hat keine Empfehlungslinks oder Bestenlisten. Du kannst einen Empfehlungscode in einem versteckten Feld mitgeben und selbst zählen, oder ein Wartelisten-Produkt nutzen, das um Empfehlungen herum gebaut ist, wenn das für deinen Launch zentral ist.

### 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

- [Launch-Liste für Product Hunt](https://ikiform.com/de/guides/waitlist/product-hunt.md): Sammle Wochen vor Product Hunt eine Launch-Liste, markiere jede Anmeldung nach Kanal und sende die Launch-E-Mail in zwei Wellen, abgestimmt auf den Reset nach Pacific Time.
- [Beta-Anmeldung mit Auswahl](https://ikiform.com/de/guides/waitlist/beta-signup.md): Ein Beta-Bewerbungsformular mit Auswahlfragen, die der Server durchsetzt, und ein Skript, das Bewerber bewertet und jede Kohorte aus der Liste auswählt.
- [Warteliste für mobile Apps](https://ikiform.com/de/guides/waitlist/mobile-app.md): Eine Pre-Launch-Warteliste für eine iOS- oder Android-App, die fragt, welches Handy jemand nutzt, iPhone-Tester zu TestFlight schickt und einen Play-Closed-Test füllt.
- [Warteliste mit Next.js](https://ikiform.com/de/guides/waitlist/nextjs.md): Eine Warteliste mit dem Next.js App Router: eine Server Action, die Anmeldungen mit Secret Key weiterleitet, useActionState, ein optimistisches Danke und ein Limit pro Besucher.
- [Warteliste auf Framer](https://ikiform.com/de/guides/waitlist/framer.md): Zwei Wege, auf Framer Wartelisten-Anmeldungen zu sammeln: Framers Formular mit einem Webhook an Ikiform oder eine Code-Komponente, die Limits pro Besucher und Spamprüfungen behält.
- [Warteliste auf Webflow](https://ikiform.com/de/guides/waitlist/webflow.md): Richte einen Webflow Form Block auf Ikiform aus, markiere Anmeldungen mit einem versteckten Feld nach Quelle und behalte mit einem kurzen Skript Webflows Erfolgszustand für jedes Formular.
- [Warteliste auf Carrd](https://ikiform.com/de/guides/waitlist/carrd.md): Sammle Wartelisten-Anmeldungen auf einer Carrd-Seite: ein Send-to-URL-Formular auf Pro Plus mit UTM-Tracking oder ein eingebettetes HTML-Formular mit Honeypot auf Pro Standard.
- [Warteliste in reinem HTML](https://ikiform.com/de/guides/waitlist/html.md): Eine komplette Wartelisten-Seite in reinem HTML ohne Build, die ohne JavaScript funktioniert, nach dem Laden ein Danke inline zeigt und bei Bedarf zurückfällt.
- [Astro-Warteliste](https://ikiform.com/de/guides/waitlist/astro.md): Eine Astro-Warteliste auf einer statischen Landingpage: Eine Action leitet jede Anmeldung weiter, ein Skript ruft sie auf, und eine On-Demand-Seite fängt Posts ohne JavaScript ab.
- [Warteliste mit TanStack Start](https://ikiform.com/de/guides/waitlist/tanstack-start.md): Eine TanStack-Start-Warteliste: Eine Server-Route beantwortet sowohl einen einfachen Formular-Post als auch fetch, und validateSearch trägt Quelle und Ergebnis in der URL.
- [React-Router-Warteliste](https://ikiform.com/de/guides/waitlist/react-router.md): Eine React-Router-Warteliste im Footer jeder Seite: Ein Fetcher sendet an eine Route-Action, markiert jede Anmeldung mit ihrer Seite und funktioniert ohne JavaScript.
- [Warteliste mit SvelteKit](https://ikiform.com/de/guides/waitlist/sveltekit.md): Eine SvelteKit-3-Warteliste: Ein Hook behält das erste ?ref= in einem Cookie, eine benannte Form-Action leitet die Anmeldung weiter, und wiederkehrende Besucher sehen, dass sie dabei sind.
- [Nuxt-Warteliste](https://ikiform.com/de/guides/waitlist/nuxt.md): Eine Nuxt-Warteliste auf einer vorgerenderten Landingpage: Eine Nitro-Route nimmt $fetch und einfache Formular-Posts an, prüft sie mit Zod und leitet ohne JavaScript weiter.
- [Kontaktformular auf einer statischen Site](https://ikiform.com/de/guides/contact-form-static-site.md): Bau ein funktionierendes Kontaktformular in eine statische Site ohne Server ein: wohin der POST geht, Spam, Antworten per E-Mail und Code für jeden Host und jedes Framework.
- [Ein Formular an deine E-Mail senden](https://ikiform.com/de/guides/send-form-to-email.md): Bekomm jede Formulareinsendung ohne Mailserver in dein Postfach: warum mailto: und PHPs mail() scheitern und ein Setup, das Spam filtert und dich antworten lässt.
- [Spam im Kontaktformular stoppen](https://ikiform.com/de/guides/spam-protection.md): Stopp Kontaktformular-Spam mit Schichten, die echte Menschen nichts kosten: Honeypot, Timing, Inhaltsbewertung, Rate-Limits und eine Menschlichkeitsprüfung nur bei Bedarf.
