# HTML-Formulareinsendungen an deine E-Mail senden

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

Veröffentlicht am 2026-10-25 · https://ikiform.com/de/guides/send-form-to-email

Ein HTML-Formular kann von sich aus keine E-Mail senden, weil ein Browser keinen Weg hat, mit einem Mailserver zu sprechen. Richte die `action` des Formulars auf ein Formular-Backend aus, das den Post annimmt, Spam filtert, ihn speichert und dir und deinem Team eine E-Mail schickt. Nenne das E-Mail-Feld `email`, dann schreibt **Reply** in deinem Postfach direkt an die Person zurück, die das Formular ausgefüllt hat.

Die meisten Sites brauchen von einem Kontaktformular genau eins: die Nachricht in einem Postfach, das jemand liest. Das ist schwieriger, als es aussieht. HTML hat ein `<form>`-Element, aber keine Möglichkeit, Mails zu senden, und die alten Abkürzungen schieben die Arbeit entweder dem Besucher zu oder landen im Spam. Diese Anleitung erklärt, warum, vergleicht die Optionen ehrlich und richtet ein Formular ein, das dir auf jeder Site und mit jedem Stack E-Mails schickt.

## Warum ein Formular nicht von allein E-Mails senden kann

Wenn ein Besucher auf **Send** klickt, macht der Browser einen HTTP-Request an die URL in der `action` des Formulars. Mehr kann ein Formular nicht. Eine E-Mail zu senden, erfordert eine SMTP-Unterhaltung mit einem Mailserver, von einer Maschine aus, der der empfangende Server vertraut. Ein Browser kann diese Verbindung nicht öffnen, und selbst wenn, würde Mail vom Laptop eines beliebigen Besuchers abgelehnt. Zwischen Formular und Postfach sitzt also immer etwas auf einem Server: dein eigener Code, ein Skript bei deinem Host oder ein Dienst.

Mit einem Formular-Backend ist dieses Etwas eine URL. Das Formular sendet an `https://ikiform.com/f/<id>`, und Ikiform erledigt den Rest:

Diagramm: Wie aus einem Formular-Post eine E-Mail in deinem Postfach wird

1. Der Besucher sendet dein Formular ab, und der Browser oder dein Server schickt es an deinen Ikiform-Endpunkt.
2. Ikiform prüft die Felder gegen die Definition des Formulars, bewertet den Post auf Spam und speichert ihn.
3. Ein Post, der den Spam-Schwellenwert erreicht, wird als Spam gespeichert. Er bekommt dieselbe Erfolgsantwort, löst aber weder E-Mail noch Webhook aus.
4. Jeder andere Post sendet eine E-Mail an jeden Empfänger, mit der Adresse des Besuchers als Reply-To.
5. Die E-Mail kommt Sekunden später in deinem Postfach an. Mit Antworten schreibst du dem Besucher.

Der Besucher sieht nie eine E-Mail-Adresse und öffnet nie eine Mail-App. Das Einzige auf deiner Seite ist ein Formular.

## Sechs Wege, ein Formular in dein Postfach zu bekommen

Jeder davon funktioniert für irgendwen. Die Tabelle zeigt, was dich jeder kostet, sobald echte Besucher und echte Bots das Formular nutzen.

| Ansatz | Stark bei | Nachteile |
| --- | --- | --- |
| `action="mailto:you@example.com"` | Kein Server, kein Konto, ein Attribut. | Nichts wird gesendet. Der Browser öffnet die Mail-App des Besuchers mit einem Entwurf oder tut gar nichts, wenn keine Mail-App eingerichtet ist. Deine Adresse steht für Scraper in der Seite. Siehe [die Gmail-Anleitung](https://ikiform.com/de/guides/send-form-to-email/gmail.md). |
| Ein PHP-Skript mit `mail()` | Läuft auf fast jedem Shared-PHP-Host, ohne Drittanbieter. | Hängt vom Mail-Setup des Hosts ab, und Mail, die angeblich von deiner Domain kommt, fällt oft bei SPF oder DMARC durch und landet im Spam. Kein Spamfilter, keine Speicherung und auf statischen Hosts überhaupt kein PHP. Siehe [ohne PHP](https://ikiform.com/de/guides/send-form-to-email/without-php.md). |
| Ein E-Mail-SDK für den Browser, etwa EmailJS | Kein Server. E-Mail-Vorlagen, die du in deren Dashboard gestaltest. | Die Schlüssel und Vorlagen-IDs stehen im JavaScript deiner Seite, sodass jeder deine Vorlagen mit eigenem Text verschicken kann. Siehe [die EmailJS-Alternative](https://ikiform.com/de/guides/send-form-to-email/emailjs-alternative.md). |
| Deine eigene Route plus eine E-Mail-API wie Resend, Postmark, Amazon SES oder Nodemailer über SMTP | Volle Kontrolle: dein Betreff, dein HTML, eine Bestätigung an den Besucher. | Du baust Validierung, Spamfilter, Rate-Limits, Speicherung und Wiederholungsversuche, verifizierst eine Absenderdomain und brauchst selbst für eine statische Site einen Server. Siehe [die Next.js-Anleitung](https://ikiform.com/de/guides/send-form-to-email/nextjs.md). |
| Google Apps Script als Web-App | Läuft auf einem Google-Konto, das du schon hast. | Skript-Kontingente begrenzen, wie viele Empfänger es pro Tag anschreiben kann, die URL hat keinen Spamschutz, und du pflegst das Skript. |
| Ein Formular-Backend wie Ikiform, [Formspree](https://ikiform.com/alternatives/formspree) oder [Web3Forms](https://ikiform.com/alternatives/web3forms) | Eine URL in `action`. Spamfilter, Speicherung und E-Mail ohne Server. | Layout und Betreff der E-Mail bestimmt der Dienst. Bei Ikiform wird dem Besucher nie etwas gemailt. |

Wenn du eine vollständig gebrandete E-Mail oder eine Bestätigung an den Besucher brauchst, schreib deine eigene Route mit einer E-Mail-API. Wenn du die Nachricht im Postfach und eine Antwortmöglichkeit willst, ohne Spam, ist ein Formular-Backend der wenigste Code. Der Rest dieser Anleitung nutzt Ikiform.

## Das Formular

Drei Felder decken die meisten Kontaktformulare ab: wer die Person ist, wohin die Antwort geht und was sie will. Wähle unten deinen Stack; die Wahl gilt für jedes Codebeispiel in diesen Anleitungen.

**HTML**

Dieses Formular funktioniert ganz ohne JavaScript: Der Browser sendet es, Ikiform schickt dir eine E-Mail, und der Besucher landet auf der Seite in `_redirect`. Unter jedem Feld steht ein leerer Platz für Fehler, den das Skript unten nutzt.

`contact.html`:

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

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

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="6" required minlength="10"></textarea>
  <p data-error="message"></p>

  <input type="hidden" name="page" value="/contact" />
  <input type="hidden" name="_redirect" value="https://example.com/contact/sent" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Send message</button>
  <p id="contact-status" role="status"></p>
</form>
```

Das Skript hält den Besucher auf der Seite. Es sendet mit `fetch`, schreibt jeden Serverfehler unter sein Feld und ersetzt das Formular durch eine Zeile, die die Adresse wiederholt, an die du antworten wirst, damit ein Tippfehler leicht auffällt.

`contact.js`:

```js
const form = document.getElementById("contact");
const status = document.getElementById("contact-status");
const button = form.querySelector("button");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  for (const slot of form.querySelectorAll("[data-error]")) slot.textContent = "";
  button.disabled = true;
  status.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 email = form.elements.namedItem("email").value;
      const done = document.createElement("p");
      done.setAttribute("role", "status");
      done.textContent = `Sent. We'll reply to ${email}.`;
      form.replaceWith(done);
      return;
    }
    for (const [field, messages] of Object.entries(result.error.fields ?? {})) {
      const slot = form.querySelector(`[data-error="${field}"]`);
      if (slot) slot.textContent = messages[0];
    }
    status.textContent = result.error.fields ? "Check the fields above." : result.error.message;
  } catch {
    status.textContent = "Couldn't send. Check your connection and try again.";
  }
  button.disabled = false;
});
```

**Next.js**

Eine Server Action sendet die Nachricht mit deinem Secret Key. Sie ordnet `error.fields` von Ikiform je einer Meldung pro Feld zu und gibt die eingegebenen Werte zurück, sodass bei einem Fehler nichts verloren geht, was der Besucher geschrieben hat.

`app/contact/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";

export interface ContactState {
  sent: boolean;
  email?: string;
  errors: Record<string, string>;
  values?: { name: string; email: string; message: string };
}

export async function sendMessage(_previous: ContactState, formData: FormData): Promise<ContactState> {
  const values = {
    name: String(formData.get("name") ?? "").trim(),
    email: String(formData.get("email") ?? "").trim(),
    message: String(formData.get("message") ?? "").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, page: "/contact", _gotcha: String(formData.get("_gotcha") ?? "") }),
  });
  const result = await response.json().catch(() => null);
  if (result?.ok) return { sent: true, email: values.email, errors: {} };

  const errors: Record<string, string> = {};
  for (const [field, messages] of Object.entries(result?.error?.fields ?? {})) {
    errors[field] = (messages as string[])[0] ?? "Check this field.";
  }
  if (!Object.keys(errors).length) errors.form = result?.error?.message ?? "Couldn't send your message. Try again.";
  return { sent: false, errors, values };
}
```

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

```tsx
"use client";

import { useActionState } from "react";
import { type ContactState, sendMessage } from "./actions";

const initial: ContactState = { sent: false, errors: {} };

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

  if (state.sent) return <p role="status">Sent. We'll reply to {state.email}.</p>;

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

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

      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} required minLength={10} defaultValue={state.values?.message} />
      {state.errors.message ? <p role="alert">{state.errors.message}</p> : null}

      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state.errors.form ? <p role="alert">{state.errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send message"}</button>
    </form>
  );
}
```

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. Jedes Server-Beispiel reicht den `Origin`-Header des Browsers weiter, falls vorhanden, weil das eigene `fetch` eines Servers keinen sendet und Ikiform kostenlose `localhost`-Testeinsendungen daran erkennt.

Mehr zu Next.js? Lies [Ein Next.js-Kontaktformular, das E-Mails sendet](https://ikiform.com/de/guides/send-form-to-email/nextjs.md).

**Astro**

Zod prüft jedes Feld in der Action, bevor etwas gesendet wird, und `isInputError` liefert dir die Meldungen pro Feld. Die Seite muss bei Bedarf gerendert werden, also behalte `prerender = false` und füge einen Adapter hinzu.

`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 = {
  sendMessage: defineAction({
    accept: "form",
    input: z.object({
      name: z.string().trim().min(1, "Tell us your name.").max(120),
      email: z.email("Enter an address we can reply to."),
      message: z.string().trim().min(10, "Write at least a sentence."),
      _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, page: "/contact" }),
      });
      const result = await response.json();
      if (!result.ok) throw new ActionError({ code: "BAD_REQUEST", message: result.error.message });
      return { email: input.email };
    },
  }),
};
```

`src/pages/contact.astro`:

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

export const prerender = false;

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

{result?.data ? (
  <p role="status">Sent. We'll reply to {result.data.email}.</p>
) : (
  <form method="POST" action={actions.sendMessage}>
    <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">Your email</label>
    <input id="email" name="email" type="email" autocomplete="email" required />
    {fields.email && <p role="alert">{fields.email[0]}</p>}

    <label for="message">Message</label>
    <textarea id="message" name="message" rows="6" required minlength="10"></textarea>
    {fields.message && <p role="alert">{fields.message[0]}</p>}

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

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. Jedes Server-Beispiel reicht den `Origin`-Header des Browsers weiter, falls vorhanden, weil das eigene `fetch` eines Servers keinen sendet und Ikiform kostenlose `localhost`-Testeinsendungen daran erkennt.

**TanStack Start**

Die Serverfunktion liest die `FormData` in `.validator()`, sendet sie vom Server und gibt entweder die Antwortadresse oder einen Fehler pro Feld zurück.

`src/routes/contact.tsx`:

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

type Outcome = { sent: true; email: string } | { sent: false; errors: Record<string, string> };

const sendMessage = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    return {
      name: String(data.get("name") ?? "").trim(),
      email: String(data.get("email") ?? "").trim(),
      message: String(data.get("message") ?? "").trim(),
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
  })
  .handler(async ({ data }): Promise<Outcome> => {
    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, page: "/contact" }),
    });
    const result = await response.json();
    if (result.ok) return { sent: true, email: data.email };
    const errors: Record<string, string> = { form: String(result.error.message) };
    for (const [field, messages] of Object.entries(result.error.fields ?? {})) {
      errors[field] = String((messages as string[])[0]);
    }
    return { sent: false, errors };
  });

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

function Contact() {
  const send = useServerFn(sendMessage);
  const [outcome, setOutcome] = useState<Outcome | null>(null);
  const [pending, setPending] = useState(false);
  const errors = outcome && !outcome.sent ? outcome.errors : {};

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    setPending(true);
    try {
      setOutcome(await send({ data }));
    } catch {
      setOutcome({ sent: false, errors: { form: "Couldn't send your message. Try again." } });
    } finally {
      setPending(false);
    }
  }

  if (outcome?.sent) return <p role="status">Sent. We'll reply to {outcome.email}.</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">Your email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {errors.email ? <p role="alert">{errors.email}</p> : null}
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} required minLength={10} />
      {errors.message ? <p role="alert">{errors.message}</p> : null}
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form && !errors.name && !errors.email && !errors.message ? <p role="alert">{errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send message"}</button>
    </form>
  );
}
```

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. Jedes Server-Beispiel reicht den `Origin`-Header des Browsers weiter, falls vorhanden, weil das eigene `fetch` eines Servers keinen sendet und Ikiform kostenlose `localhost`-Testeinsendungen daran erkennt.

**React Router**

Die `action` der Route leitet die Nachricht weiter und gibt die Feldfehler von Ikiform über `actionData` zurück, wobei `data()` den Statuscode von Ikiform beibehält.

`app/routes/contact.tsx`:

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

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const email = String(formData.get("email") ?? "").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({
      name: String(formData.get("name") ?? "").trim(),
      email,
      message: String(formData.get("message") ?? "").trim(),
      page: "/contact",
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json();
  if (result.ok) return { sent: true as const, email };
  const fields: Record<string, string[]> = result.error.fields ?? {};
  return data(
    { sent: false as const, message: String(result.error.message), fields },
    { status: response.status },
  );
}

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

  if (actionData?.sent) return <p role="status">Sent. We'll reply to {actionData.email}.</p>;
  const fields = actionData?.fields ?? {};

  return (
    <Form method="post">
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      {fields.name ? <p role="alert">{fields.name[0]}</p> : null}
      <label htmlFor="email">Your email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {fields.email ? <p role="alert">{fields.email[0]}</p> : null}
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} required minLength={10} />
      {fields.message ? <p role="alert">{fields.message[0]}</p> : null}
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {actionData && !Object.keys(fields).length ? <p role="alert">{actionData.message}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send message"}</button>
    </Form>
  );
}
```

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. Jedes Server-Beispiel reicht den `Origin`-Header des Browsers weiter, falls vorhanden, weil das eigene `fetch` eines Servers keinen sendet und Ikiform kostenlose `localhost`-Testeinsendungen daran erkennt.

**SvelteKit**

Die Standard-Action gibt `fail()` mit den Feldfehlern zurück. `update({ reset: false })` behält, was der Besucher getippt hat, wenn der Server mit einem Fehler antwortet. Deklariere `IKIFORM_SECRET_KEY` in `src/env.ts` mit `defineEnvVars`, damit `$app/env/private` ihn lesen kann.

`src/routes/contact/+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 values = {
      name: String(data.get("name") ?? "").trim(),
      email: String(data.get("email") ?? "").trim(),
      message: String(data.get("message") ?? "").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, page: "/contact", _gotcha: String(data.get("_gotcha") ?? "") }),
    });
    const result = await response.json();
    if (!result.ok) {
      const fields: Record<string, string[]> = result.error.fields ?? {};
      return fail(response.status, { values, fields, message: String(result.error.message) });
    }
    return { sentTo: values.email };
  },
} satisfies Actions;
```

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

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

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

{#if form?.sentTo}
  <p role="status">Sent. We'll reply to {form.sentTo}.</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 />
    {#if fields.name}<p role="alert">{fields.name[0]}</p>{/if}
    <label for="email">Your email</label>
    <input id="email" name="email" type="email" autocomplete="email" required />
    {#if fields.email}<p role="alert">{fields.email[0]}</p>{/if}
    <label for="message">Message</label>
    <textarea id="message" name="message" rows="6" required minlength="10"></textarea>
    {#if fields.message}<p role="alert">{fields.message[0]}</p>{/if}
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {#if form?.message && !Object.keys(fields).length}<p role="alert">{form.message}</p>{/if}
    <button disabled={pending}>{pending ? "Sending" : "Send message"}</button>
  </form>
{/if}
```

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. Jedes Server-Beispiel reicht den `Origin`-Header des Browsers weiter, falls vorhanden, weil das eigene `fetch` eines Servers keinen sendet und Ikiform kostenlose `localhost`-Testeinsendungen daran erkennt.

**Nuxt**

Eine Server-Route hält den Secret Key und reicht die Meldung von Ikiform als `message` von `createError` und die Feldfehler in dessen `data` weiter; `$fetch` gibt beides auf dem Client als `data.message` und `data.data.fields` zurück. Nuxt füllt `runtimeConfig.ikiformSecretKey` aus `NUXT_IKIFORM_SECRET_KEY`.

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

```ts
export default defineEventHandler(async (event) => {
  const body = await readBody<{ name?: string; email?: string; message?: 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({
      name: body.name?.trim() ?? "",
      email: body.email?.trim() ?? "",
      message: body.message?.trim() ?? "",
      page: "/contact",
      _gotcha: body._gotcha ?? "",
    }),
  });
  const result = await response.json();
  if (!result.ok) {
    throw createError({
      status: response.status,
      message: String(result.error.message),
      data: { fields: result.error.fields ?? {} },
    });
  }
  return { sentTo: body.email };
});
```

`nuxt.config.ts`:

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

`app/components/ContactForm.vue`:

```vue
<script setup lang="ts">
const values = reactive({ name: "", email: "", message: "", _gotcha: "" });
const fields = ref<Record<string, string[]>>({});
const error = ref("");
const sentTo = ref("");
const sending = ref(false);

async function send() {
  sending.value = true;
  fields.value = {};
  error.value = "";
  try {
    const result = await $fetch("/api/contact", { method: "POST", body: values });
    sentTo.value = result.sentTo ?? values.email;
  } catch (failure) {
    const data = (failure as { data?: { message?: string; data?: { fields?: Record<string, string[]> } } }).data;
    fields.value = data?.data?.fields ?? {};
    error.value = data?.message ?? "Couldn't send your message. Try again.";
  } finally {
    sending.value = false;
  }
}
</script>

<template>
  <p v-if="sentTo" role="status">Sent. We'll reply to {{ sentTo }}.</p>
  <form v-else @submit.prevent="send">
    <label for="name">Name</label>
    <input id="name" v-model="values.name" name="name" autocomplete="name" required />
    <p v-if="fields.name" role="alert">{{ fields.name[0] }}</p>
    <label for="email">Your email</label>
    <input id="email" v-model="values.email" name="email" type="email" autocomplete="email" required />
    <p v-if="fields.email" role="alert">{{ fields.email[0] }}</p>
    <label for="message">Message</label>
    <textarea id="message" v-model="values.message" name="message" rows="6" required minlength="10" />
    <p v-if="fields.message" role="alert">{{ fields.message[0] }}</p>
    <input v-model="values._gotcha" name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="error && !Object.keys(fields).length" role="alert">{{ error }}</p>
    <button :disabled="sending">{{ sending ? "Sending" : "Send message" }}</button>
  </form>
</template>
```

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. Jedes Server-Beispiel reicht den `Origin`-Header des Browsers weiter, falls vorhanden, weil das eigene `fetch` eines Servers keinen sendet und Ikiform kostenlose `localhost`-Testeinsendungen daran erkennt.

**React**

Ohne eigenen Server sendet der Browser direkt an Ikiform. In diesen Code kommt kein Schlüssel; **Allowed domains** entscheidet, welche Sites senden dürfen.

`src/ContactForm.tsx`:

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

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

export function ContactForm() {
  const [sentTo, setSentTo] = useState("");
  const [sending, setSending] = useState(false);
  const [fields, setFields] = useState<Record<string, string[]>>({});
  const [error, setError] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const body = new FormData(event.currentTarget);
    setSending(true);
    setFields({});
    setError("");
    try {
      const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
      const result = await response.json();
      if (result.ok) {
        setSentTo(String(body.get("email")));
        return;
      }
      setFields(result.error.fields ?? {});
      setError(result.error.message);
    } catch {
      setError("Couldn't reach the server. Try again.");
    } finally {
      setSending(false);
    }
  }

  if (sentTo) return <p role="status">Sent. We'll reply to {sentTo}.</p>;

  return (
    <form action={ENDPOINT} method="post" onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      {fields.name ? <p role="alert">{fields.name[0]}</p> : null}
      <label htmlFor="email">Your email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {fields.email ? <p role="alert">{fields.email[0]}</p> : null}
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" rows={6} required minLength={10} />
      {fields.message ? <p role="alert">{fields.message[0]}</p> : null}
      <input type="hidden" name="page" value="/contact" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error && !Object.keys(fields).length ? <p role="alert">{error}</p> : null}
      <button disabled={sending}>{sending ? "Sending" : "Send message"}</button>
    </form>
  );
}
```

**Vue**

Derselbe browserseitige Post als Vue-Komponente. Das Formular behält seine `action`, sendet also auch dann, wenn das Skript nicht lädt.

`src/components/ContactForm.vue`:

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

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

const sentTo = ref("");
const sending = ref(false);
const fields = ref<Record<string, string[]>>({});
const error = ref("");

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

<template>
  <p v-if="sentTo" role="status">Sent. We'll reply to {{ sentTo }}.</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="fields.name" role="alert">{{ fields.name[0] }}</p>
    <label for="email">Your email</label>
    <input id="email" name="email" type="email" autocomplete="email" required />
    <p v-if="fields.email" role="alert">{{ fields.email[0] }}</p>
    <label for="message">Message</label>
    <textarea id="message" name="message" rows="6" required minlength="10" />
    <p v-if="fields.message" role="alert">{{ fields.message[0] }}</p>
    <input type="hidden" name="page" value="/contact" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="error && !Object.keys(fields).length" role="alert">{{ error }}</p>
    <button :disabled="sending">{{ sending ? "Sending" : "Send message" }}</button>
  </form>
</template>
```

- Ersetze `frm_xxxxxxxxxxxxxxxx` durch die ID deines Formulars aus dem Dashboard.
- `name="email"` macht dieses Feld zur Reply-To-Adresse der Benachrichtigungs-E-Mail. Behalte den Namen, auch wenn das Label „Deine E-Mail“ lautet.
- `page` ist ein gewöhnliches verstecktes Feld. Es erscheint in der E-Mail, sodass du weißt, von welcher Seite die Nachricht kam.
- `_gotcha` ist der Honeypot. Menschen sehen ihn nie; ein Bot, der ihn ausfüllt, wird als Spam gespeichert und löst keine E-Mail aus.
- `_redirect` funktioniert nur mit einer `https:`-URL auf einer der **Allowed domains** des Formulars. Lass es weg und setze stattdessen **Redirect after submit** in den Einstellungen des Formulars, sonst zeigt Ikiform seine eigene Dankeseite.

Die Framework-Anleitungen gehen weiter: [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 [einfaches HTML](https://ikiform.com/de/docs/html.md). Website-Baukästen wie [Webflow](https://ikiform.com/de/docs/guides/webflow.md) und [Carrd](https://ikiform.com/de/docs/guides/carrd.md) haben eigene Seiten, und die [Kontaktformular-Vorlage](https://ikiform.com/forms/contact-form) ergänzt ein Themenfeld.

Definiere die Felder auch im Dashboard, dann lehnt Ikiform einen nicht passenden Post mit `422 validation_failed` und einer Meldung pro Feld ab, die der Code oben neben jedem Eingabefeld anzeigt. Probier es hier aus; das führt denselben Validator in deinem Browser aus:

Formular: Contact

- **Name** (`name`, text, Pflichtfeld)
- **Deine E-Mail** (`email`, email, Pflichtfeld)
- **Nachricht** (`message`, textarea, Pflichtfeld)

Füge diese Definition im Tab **Fields** des Formulars ein. Ohne Definition akzeptiert Ikiform beliebige Feldnamen und mailt alles, was ankommt.

## Wie die E-Mail aussieht

Jede angenommene Einsendung löst eine schlichte, gut lesbare E-Mail aus. Hier die Textversion einer solchen:

`Benachrichtigungs-E-Mail`:

```text
From: Ikiform <notifications@ikiform.com>
To: you@yourcompany.com
Reply-To: ada@example.com
Subject: New submission: Contact

Contact got a new submission on Oct 25, 2026, 9:14 AM UTC.

name: Ada Lovelace
email: ada@example.com
message: Do you ship to Canada? I'd like to order 40 units before December.
page: /contact

View it in Ikiform:
https://ikiform.com/dashboard/submissions?form=frm_xxxxxxxxxxxxxxxx&submission=sub_...
```

- Sie kommt von `notifications@ikiform.com`. Du richtest keine DNS-Einträge und keine Absenderdomain ein, weil die Mail von der Domain von Ikiform gesendet wird.
- Der Betreff lautet immer `New submission: <form name>`. `_subject` ändert nicht den Betreff der Benachrichtigungs-E-Mails. Es wird als `meta.subject` an Webhooks und die API weitergegeben. Um Formulare auseinanderzuhalten, gib jedem Formular im Dashboard einen klaren Namen, etwa „Kontakt, Preisseite“.
- Felder erscheinen in der Reihenfolge, in der sie gesendet wurden, beschriftet mit ihrem `name`. Felder, die mit `_` beginnen, werden weggelassen.
- Ein sehr langes Formular oder eine sehr lange Antwort wird gekürzt, und die E-Mail sagt, wie viele weitere Felder im Dashboard stehen. Die [Seite zu Limits](https://ikiform.com/de/docs/limits.md#notifications) nennt die Zahlen.
- Felder, die wie Passwörter oder Kartennummern aussehen, werden weggelassen, und die E-Mail zu einem Post, der sie enthält, wird zur Prüfung zurückgehalten. Sammle mit einem Formular-Backend keine Zugangsdaten.
- Workspace-Mitglieder bekommen einen Button **View submission**. Personen außerhalb des Workspace bekommen die Felder und einen Abmeldelink.

## Wer die E-Mail bekommt

Empfänger werden pro Formular im Dashboard festgelegt, nie durch das Formular selbst. Ein Feld, das einen Empfänger angibt, würde jedem erlauben, über dein Formular beliebige Leute anzuschreiben, deshalb hat Ikiform keines.

| Wer | Wie sie hinzugefügt werden | Hinweise |
| --- | --- | --- |
| Der Standard | Nichts zu tun | Das Mitglied, das als **Notification email** des Workspace gewählt ist, oder jeder Owner des Workspace, wenn keines gewählt ist |
| Ausgewählte Mitglieder | Hake sie in den Einstellungen **Notifications** des Formulars an | Nur Mitglieder mit verifizierter E-Mail-Adresse |
| Personen außerhalb des Workspace | **Add someone outside the workspace**, dann **Send link** | Sie bekommen erst E-Mails, nachdem sie bestätigt haben. Bis zu 5 pro Formular, und jede Person kann sich abmelden |

Jedes Formular mailt an höchstens 10 Adressen. Jeder Empfänger bekommt seine eigene Kopie statt eines gemeinsamen CC, sodass niemand die Adressen der anderen sieht. Wenn das Postfach eines Empfängers Bounces erzeugt, pausiert Ikiform eine Weile die E-Mails an diese Adresse, und das Dashboard zeigt daneben **Emails bounced**. [Mehrere Empfänger](https://ikiform.com/de/guides/send-form-to-email/multiple-recipients.md) behandelt das Weiterleiten nach Thema, was Empfänger allein nicht können.

## Der Person antworten, nicht Ikiform

Ikiform setzt die Reply-To-Adresse zuerst aus einem Feld `_replyto`, dann aus einem Feld namens `email`, `email_address`, `your_email`, `reply_to`, `contact_email`. Der Wert muss eine einzelne gültige E-Mail-Adresse sein, sonst wird er ignoriert. Ist ein Reply-To gesetzt, öffnet **Reply** in Gmail, Outlook oder Apple Mail eine Antwort an den Besucher, und deine Antwort kommt von deiner eigenen Adresse.

Ohne Reply-To geht eine Antwort an `notifications@ikiform.com`, das niemand liest. Das ist die häufigste Überraschung, und meist steckt ein Feld namens `user_email` oder `Email Address` mit Leerzeichen dahinter. Benenne es um oder füge `_replyto` hinzu. [Die Reply-To-Anleitung](https://ikiform.com/de/guides/send-form-to-email/reply-to.md) behandelt Formulare mit zwei E-Mail-Feldern und Baukästen, die Felder für dich umbenennen.

## Wenn viel auf einmal ankommt

Jedes Formular sendet bis zu 50 Benachrichtigungs-E-Mails pro Stunde, oder 10, solange der Workspace jünger als 7 Tage ist. Darüber hinaus werden die restlichen Einsendungen der Stunde wie gewohnt gespeichert und kommen nach Ende der Stunde als eine Sammel-E-Mail an. Die Grenze schützt dein Postfach und den Ruf von Ikiforms Mails bei deinem Mail-Anbieter, wenn ein Formular irgendwo Großes geteilt wird oder ein Bot an den Filtern vorbeikommt.

Diagramm: Was in einer vollen Stunde mit Benachrichtigungs-E-Mails passiert

1. Die ersten 50 Einsendungen an ein Formular in einer vollen Stunde lösen wie gewohnt je eine E-Mail aus.
2. Spätere Einsendungen in derselben Stunde werden gespeichert und sofort im Dashboard angezeigt, ohne einzelne E-Mail.
3. Nach Ende der Stunde bekommt jeder Empfänger eine Sammel-E-Mail, die sie zählt und auf das Dashboard verlinkt.

Die Grenze zählt pro Formular, ein Ansturm auf ein Formular bremst also kein anderes. Webhooks sind nicht begrenzt und feuern weiterhin für jede Einsendung.

Der Betreff der Sammel-E-Mail besteht aus der Anzahl und dem Formularnamen, und sie listet weder die Felder auf noch setzt sie ein Reply-To, also beantworte diese Nachrichten aus dem Dashboard. Wenn du regelmäßig über der Grenze liegst, ist E-Mail nicht der richtige Kanal für dieses Formular: Schick es stattdessen über einen [Webhook](https://ikiform.com/de/docs/webhooks.md) an [Slack](https://ikiform.com/blog/form-submissions-to-slack.md) oder ein gemeinsames Postfach.

## Spam aus deinem Postfach heraushalten

E-Mail macht Spam teuer: Jeder Bot-Post ist eine Benachrichtigung, die jemand öffnet. Ikiform speichert Spam mit einer normalen Erfolgsantwort, der Bot lernt also nichts, und sendet keine E-Mail dafür. Vier Dinge entscheiden, was als Spam zählt:

| Abwehr | Was sie abfängt | Einrichtung |
| --- | --- | --- |
| Honeypot | Bots, die jedes Eingabefeld ausfüllen | Das versteckte Eingabefeld `_gotcha` |
| Spam-Bewertung | Links im Namensfeld, drei oder mehr Links, bekannte Spam-Phrasen, Wegwerfadressen und identische Wiederholungen. Ab 5 ist der Post Spam | Keine |
| Erlaubte Domains | Posts von anderen Sites | Füge deine Domain in den Einstellungen des Formulars hinzu |
| Menschlichkeitsprüfung | Bots, die einen echten Browser ausführen | Setze **Spam check** auf **Ikiform** |

Schau ab und zu in den Filter **Spam** im Dashboard. Wenn dort eine echte Nachricht landet, markiere sie als kein Spam, dann geht ihre E-Mail raus. Die [Spam-Dokumentation](https://ikiform.com/de/docs/spam.md) erklärt jedes Signal, auch wann sich die Menschlichkeitsprüfung lohnt.

## Was E-Mail nicht kann

Ikiform schickt 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. Verschicke sie aus deinem E-Mail-Tool, ausgelöst durch einen [Webhook](https://ikiform.com/de/docs/webhooks.md). Wenn deine Site „Wir haben deine Nachricht erhalten“ verspricht, zeig das nach dem Absenden auf der Seite an, wie es der Code oben schon tut.

- **Ein gemeinsames Team-Postfach.** Füge die Team-Adresse als Empfänger hinzu oder leite sie über deinen Mail-Anbieter weiter.
- **Ein Chat-Kanal.** Schick jede Einsendung per Webhook an [Slack](https://ikiform.com/blog/form-submissions-to-slack.md), [Discord](https://ikiform.com/blog/form-submissions-to-discord.md) oder [Microsoft Teams](https://ikiform.com/blog/form-submissions-to-microsoft-teams.md). Chat passt zu Formularen, die mehr als ein paar Nachrichten am Tag bekommen.
- **Eine Tabelle oder ein CRM.** [Google Sheets](https://ikiform.com/blog/form-submissions-to-google-sheets.md) oder jedes Tool über [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) oder [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md).
- **Anhänge.** Datei-Uploads werden noch nicht unterstützt. Ein Post mit einer Datei bekommt `422 files_not_supported`. Bitte stattdessen um einen Link zu einer geteilten Datei.

### E-Mail abschalten

Manche Formulare sind ohne E-Mail besser, etwa eine stark genutzte Newsletter-Anmeldung. Schalte **Email new submissions** in den Einstellungen **Notifications** des Formulars aus. Einsendungen werden weiterhin gespeichert, und Webhooks feuern weiterhin.

> Bei neuen Workspaces beginnt die Grenze niedriger: 10 E-Mails pro Formular und Stunde in den ersten 7 Tagen. Wenn du mit einem Skript testest, rechne früher mit einer Sammel-E-Mail als später.

## Wähle deinen Fall

Das Setup oben ist überall gleich. Diese Seiten behandeln ein bestimmtes Postfach, einen bestimmten Stack oder das Tool, von dem du wegwechselst.

- [Ohne PHP](https://ikiform.com/de/guides/send-form-to-email/without-php.md): Ersetze ein contact.php-Skript, das mail() aufruft, durch ein Formular, das an einen gehosteten Endpunkt sendet, mit deinen Feldnamen, deiner Dankeseite und Antworten.
- [An Gmail](https://ikiform.com/de/guides/send-form-to-email/gmail.md): Bekomm Formulareinsendungen in Gmail ohne mailto:-Links, App-Passwörter oder Apps Script, dann beschrifte sie, halte sie aus dem Spam heraus und antworte aus Gmail.
- [Mehrere Empfänger](https://ikiform.com/de/guides/send-form-to-email/multiple-recipients.md): Mail ein Formular an dein ganzes Team oder leite jede Nachricht nach Thema an die richtige Person, ohne dass das Formular selbst bestimmt, wer die Mail bekommt.
- [Aus deinem Postfach antworten](https://ikiform.com/de/guides/send-form-to-email/reply-to.md): Sorg dafür, dass Antworten auf eine Formular-Benachrichtigung an die Person gehen, die das Formular ausgefüllt hat: welche Feldnamen funktionieren, wann _replyto nötig ist und warum Antworten fehlgehen.
- [Alternative zu EmailJS](https://ikiform.com/de/guides/send-form-to-email/emailjs-alternative.md): Lös ein Kontaktformular von EmailJS: warum E-Mail-Schlüssel im Browser ein Risiko sind, was sich im Code ändert und worauf du verzichtest, etwa die automatische Antwort.
- [Next.js](https://ikiform.com/de/guides/send-form-to-email/nextjs.md): Zwei Wege, ein Next.js-Formular per Server Action zu mailen: selbst mit Resend oder Nodemailer senden oder an ein Formular-Backend weiterleiten, ehrlich verglichen.

## Prüfen, ob es funktioniert

Geh das durch, bevor du das Formular live schaltest. 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. Prüfe in den Einstellungen **Notifications** des Formulars, ob **Email new submissions** an ist und die richtigen Leute angehakt sind.
3. Füll das Formular auf deiner Seite aus und sende es ab. Du landest auf deiner Dankeseite oder der gehosteten von Ikiform.
4. Öffne **Submissions** im Dashboard. Die neue Einsendung ist da, mit jedem Feld, das du gesendet hast.
5. Öffne die Benachrichtigungs-E-Mail. Der Betreff ist `New submission:` plus der Name des Formulars, und jedes gesendete Feld ist aufgeführt.
6. Klick auf **Reply**. Die Zeile **To** zeigt die Adresse, die du ins Formular getippt hast, nicht die von Ikiform.
7. 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.
8. Nichts angekommen? Stell den Statusfilter im Dashboard auf **Spam**. Spam bekommt dieselbe Erfolgsantwort wie eine echte Einsendung.

## Fehlerbehebung

| Problem | Was zu tun ist |
| --- | --- |
| Die Einsendung ist im Dashboard, aber keine E-Mail kam an | Schau zuerst in deinen Spam- oder Werbung-Ordner und füge `notifications@ikiform.com` zu deinen Kontakten hinzu. Prüfe dann die Einstellungen **Notifications** des Formulars: E-Mail ist vielleicht aus, bei einer externen Adresse steht vielleicht noch **Waiting for confirmation**, oder sie zeigt **Emails bounced**. |
| 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. |
| **Reply** geht an notifications@ikiform.com | Es wurde kein Reply-To gefunden. Nenne das E-Mail-Feld `email`, `email_address`, `your_email`, `reply_to` oder `contact_email`, oder füge ein Feld `_replyto` mit der Adresse des Besuchers hinzu. |
| E-Mails hörten auf, und stattdessen kam eine Sammel-E-Mail | Das Formular hat 50 E-Mails in einer Stunde überschritten (10 bei einem neuen Workspace). Alles steht im Dashboard. Such unter **Spam** nach einem Bot-Ansturm oder nutze für stark genutzte Formulare einen Webhook. |
| `_subject` ändert den Betreff nicht | `_subject` ändert nicht den Betreff der Benachrichtigungs-E-Mails. Es wird als `meta.subject` an Webhooks und die API weitergegeben. |
| `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. |
| `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. |
| `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

### Kann ein HTML-Formular ohne Server E-Mails senden?

Nicht von allein. Ein Formular macht nur einen HTTP-Request, und Mail zu senden braucht einen Server, der mit einem Mailserver spricht. Ein Formular-Backend ist dieser Server: Richte die action des Formulars darauf aus, und es schickt dir E-Mails, sodass deine eigene Site statisch bleiben kann.

### Warum nicht mailto: in der action des Formulars verwenden?

Eine mailto:-action sendet gar nichts. Sie bittet den Computer des Besuchers, eine Mail-App mit einem Entwurf zu öffnen, was bei allen ohne eingerichtete Mail-App scheitert, und der Besucher muss trotzdem noch auf Senden klicken. Viele geben da auf, und du erfährst nie, dass sie es versucht haben.

### Kann ich die Betreffzeile der Benachrichtigungs-E-Mail ändern?

Nein. Der Betreff ist immer „New submission:“ gefolgt vom Namen des Formulars, also benenne jedes Formular klar. Ein Feld _subject wird gespeichert und an Webhooks und die API weitergegeben, ändert aber die E-Mail nicht.

### Wie viele Personen können die E-Mail bekommen?

Bis zu 10 pro Formular: Workspace-Mitglieder, die du in den Einstellungen des Formulars anhakst, plus bis zu 5 externe Adressen, die per Link bestätigen. Jede Person bekommt ihre eigene Kopie.

### Kann Ikiform der Person, die das Formular gesendet hat, eine E-Mail schicken?

Nein. Ikiform schickt E-Mails an dich und dein Team, nicht an den Absender, es gibt also keinen Autoresponder und keine Bestätigungs-E-Mail. Sende einen Webhook an dein E-Mail-Tool und verschicke die Bestätigung von dort.

### Landen Formular-E-Mails im Spam?

Sie werden von der eigenen Domain von Ikiform gesendet, du brauchst also keine SPF- oder DKIM-Einträge für deine Domain. Die erste kann trotzdem im Spam oder in Werbung landen; füge notifications@ikiform.com zu deinen Kontakten hinzu oder leg einen Filter an, damit das nie passiert.

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

- [Ohne PHP](https://ikiform.com/de/guides/send-form-to-email/without-php.md): Ersetze ein contact.php-Skript, das mail() aufruft, durch ein Formular, das an einen gehosteten Endpunkt sendet, mit deinen Feldnamen, deiner Dankeseite und Antworten.
- [An Gmail](https://ikiform.com/de/guides/send-form-to-email/gmail.md): Bekomm Formulareinsendungen in Gmail ohne mailto:-Links, App-Passwörter oder Apps Script, dann beschrifte sie, halte sie aus dem Spam heraus und antworte aus Gmail.
- [Mehrere Empfänger](https://ikiform.com/de/guides/send-form-to-email/multiple-recipients.md): Mail ein Formular an dein ganzes Team oder leite jede Nachricht nach Thema an die richtige Person, ohne dass das Formular selbst bestimmt, wer die Mail bekommt.
- [Aus deinem Postfach antworten](https://ikiform.com/de/guides/send-form-to-email/reply-to.md): Sorg dafür, dass Antworten auf eine Formular-Benachrichtigung an die Person gehen, die das Formular ausgefüllt hat: welche Feldnamen funktionieren, wann _replyto nötig ist und warum Antworten fehlgehen.
- [Alternative zu EmailJS](https://ikiform.com/de/guides/send-form-to-email/emailjs-alternative.md): Lös ein Kontaktformular von EmailJS: warum E-Mail-Schlüssel im Browser ein Risiko sind, was sich im Code ändert und worauf du verzichtest, etwa die automatische Antwort.
- [Next.js](https://ikiform.com/de/guides/send-form-to-email/nextjs.md): Zwei Wege, ein Next.js-Formular per Server Action zu mailen: selbst mit Resend oder Nodemailer senden oder an ein Formular-Backend weiterleiten, ehrlich verglichen.
- [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.
- [Grenzen von Baukasten-Formularen](https://ikiform.com/de/guides/website-builder-forms.md): Wenn Formulare in Webflow, Netlify, Carrd oder Framer an ein Limit oder eine fehlende Funktion stoßen, behalte dein Formular und richte es auf ein Formular-Backend aus.
- [Feedback-Formular für eine Website oder App](https://ikiform.com/de/guides/feedback-form.md): Setze ein Feedback-Formular auf jede Seite oder in deine App: eine Bewertung, ein Kommentar, die Herkunftsseite, Spamfilter und Alerts bei niedrigen Werten.
