# Recevoir les soumissions d'un formulaire HTML par e-mail

> Recevez chaque soumission dans votre boîte sans serveur mail : pourquoi mailto : et mail() de PHP échouent, et une solution qui filtre le spam et permet de répondre.

Publié le 2026-10-25 · https://ikiform.com/fr/guides/send-form-to-email

Un formulaire HTML ne peut pas envoyer d'e-mail tout seul, car un navigateur n'a aucun moyen de parler à un serveur mail. Faites pointer l'`action` du formulaire vers un backend de formulaires, qui reçoit l'envoi, filtre le spam, le stocke et vous l'envoie par e-mail, à vous et à votre équipe. Nommez le champ e-mail `email`, et le bouton **Reply** de votre boîte répond directement à la personne qui a rempli le formulaire.

La plupart des sites n'attendent qu'une chose d'un formulaire de contact : que le message arrive dans une boîte que quelqu'un lit. C'est plus difficile qu'il n'y paraît. HTML a un élément `<form>` mais aucun moyen d'envoyer du courrier, et les vieux raccourcis renvoient le travail au visiteur ou finissent en spam. Ce guide explique pourquoi, compare honnêtement les options et met en place un formulaire qui vous envoie un e-mail, sur n'importe quel site et n'importe quelle stack.

## Pourquoi un formulaire ne peut pas envoyer d'e-mail tout seul

Quand un visiteur clique sur **Send**, le navigateur fait une requête HTTP vers l'URL de l'`action` du formulaire. C'est tout ce qu'un formulaire sait faire. Envoyer un e-mail demande une conversation SMTP avec un serveur mail, depuis une machine en laquelle le serveur destinataire a confiance. Un navigateur ne peut pas ouvrir cette connexion, et même s'il le pouvait, un e-mail parti de l'ordinateur d'un visiteur quelconque serait rejeté. Il y a donc toujours quelque chose sur un serveur entre le formulaire et votre boîte : votre propre code, un script chez votre hébergeur ou un service.

Avec un backend de formulaires, ce quelque chose est une URL. Le formulaire envoie vers `https://ikiform.com/f/<id>`, et Ikiform fait le reste :

Schéma : Comment un envoi de formulaire devient un e-mail dans votre boîte

1. Le visiteur envoie votre formulaire, et le navigateur ou votre serveur le transmet à votre endpoint Ikiform.
2. Ikiform vérifie les champs selon la définition du formulaire, évalue le spam et stocke l'envoi.
3. Un envoi qui atteint le score de spam est stocké comme spam. Il reçoit la même réponse de succès, mais n'envoie ni e-mail ni webhook.
4. Tout autre envoi déclenche un e-mail par destinataire, avec l'adresse du visiteur comme adresse de réponse.
5. L'e-mail arrive dans votre boîte quelques secondes plus tard. Répondre écrit au visiteur.

Le visiteur ne voit jamais d'adresse e-mail et n'ouvre jamais d'appli de messagerie. Votre page ne contient qu'un formulaire.

## Six façons de recevoir un formulaire dans votre boîte

Chacune convient à quelqu'un. Le tableau montre ce que chacune vous coûte une fois que de vrais visiteurs, et de vrais bots, utilisent le formulaire.

| Approche | Points forts | Compromis |
| --- | --- | --- |
| `action="mailto:you@example.com"` | Ni serveur, ni compte, un seul attribut. | Rien n'est envoyé. Le navigateur ouvre l'appli de messagerie du visiteur avec un brouillon, ou ne fait rien si aucune n'est configurée. Votre adresse est dans la page, à la portée des scrapers. Voir [le guide Gmail](https://ikiform.com/fr/guides/send-form-to-email/gmail.md). |
| Un script PHP avec `mail()` | Fonctionne sur presque tous les hébergements PHP mutualisés, sans tiers. | Dépend de la configuration mail de l'hébergeur, et un e-mail qui prétend venir de votre domaine échoue souvent à SPF ou DMARC et finit en spam. Ni filtrage du spam, ni stockage, et pas de PHP du tout sur les hébergeurs statiques. Voir [sans PHP](https://ikiform.com/fr/guides/send-form-to-email/without-php.md). |
| Un SDK d'e-mail côté navigateur, comme EmailJS | Pas de serveur. Des modèles d'e-mail conçus dans leur tableau de bord. | Les clés et les id de modèles sont dans le JavaScript de votre page : n'importe qui peut donc envoyer vos modèles avec son propre texte. Voir [l'alternative à EmailJS](https://ikiform.com/fr/guides/send-form-to-email/emailjs-alternative.md). |
| Votre propre route plus une API d'e-mail, comme Resend, Postmark, Amazon SES ou Nodemailer en SMTP | Contrôle total : votre objet, votre HTML, une confirmation au visiteur. | Vous construisez la validation, le filtrage du spam, les limites de débit, le stockage et les nouvelles tentatives, vous vérifiez un domaine d'envoi, et il vous faut un serveur même pour un site statique. Voir [le guide Next.js](https://ikiform.com/fr/guides/send-form-to-email/nextjs.md). |
| Google Apps Script en application web | Tourne sur un compte Google que vous avez déjà. | Les quotas de scripts limitent le nombre de destinataires par jour, l'URL n'a aucune protection anti-spam, et c'est vous qui maintenez le script. |
| Un backend de formulaires, comme Ikiform, [Formspree](https://ikiform.com/alternatives/formspree) ou [Web3Forms](https://ikiform.com/alternatives/web3forms) | Une URL dans `action`. Filtrage du spam, stockage et e-mail sans serveur. | La mise en page et l'objet de l'e-mail sont ceux du service. Avec Ikiform, rien n'est jamais envoyé par e-mail au visiteur. |

Si vous avez besoin d'un e-mail entièrement à votre marque, ou d'une confirmation au visiteur, écrivez votre propre route avec une API d'e-mail. Si vous voulez le message dans votre boîte et un moyen d'y répondre, sans spam, un backend de formulaires demande le moins de code. La suite de ce guide utilise Ikiform.

## Le formulaire

Trois champs couvrent la plupart des formulaires de contact : qui c'est, où répondre et ce que la personne veut. Choisissez votre stack ci-dessous ; ce choix s'applique à tous les exemples de code de ces guides.

**HTML**

Ce formulaire fonctionne sans aucun JavaScript : le navigateur l'envoie, Ikiform vous écrit et le visiteur arrive sur la page indiquée dans `_redirect`. Chaque champ a sous lui un emplacement d'erreur vide pour le script ci-dessous.

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

Le script garde le visiteur sur la page. Il envoie avec `fetch`, écrit chaque erreur du serveur sous son champ, et remplace le formulaire par une phrase qui rappelle l'adresse à laquelle vous répondrez, pour qu'une faute de frappe saute aux yeux.

`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**

Une server action envoie le message avec votre clé secrète. Elle convertit l'`error.fields` d'Ikiform en un message par champ et renvoie les valeurs saisies, pour que rien de ce qu'a écrit le visiteur ne soit perdu en cas d'erreur.

`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>
  );
}
```

Les versions côté serveur envoient avec une clé secrète, donc Ikiform ignore **Allowed domains** et sa vérification humaine. Chaque envoi vient alors de l'IP de votre serveur, et la limite Standard de 5 par minute et par visiteur compte tous vos visiteurs ensemble. Choisissez la [limite de débit](https://ikiform.com/fr/docs/rate-limits.md) Relaxed ou Custom pour un formulaire que vous envoyez via un serveur, et gardez `IKIFORM_SECRET_KEY` dans des variables d'environnement réservées au serveur. Chaque exemple serveur transmet l'en-tête `Origin` du navigateur quand il existe, car le `fetch` d'un serveur n'en envoie aucun et Ikiform s'en sert pour compter les soumissions de test gratuites depuis `localhost`.

Pour aller plus loin avec Next.js, lisez [Un formulaire de contact Next.js qui envoie un e-mail](https://ikiform.com/fr/guides/send-form-to-email/nextjs.md).

**Astro**

Zod vérifie chaque champ dans l'action avant tout envoi, et `isInputError` vous donne ses messages par champ. La page doit être rendue à la demande : gardez `prerender = false` et ajoutez un adaptateur.

`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>
)}
```

Les versions côté serveur envoient avec une clé secrète, donc Ikiform ignore **Allowed domains** et sa vérification humaine. Chaque envoi vient alors de l'IP de votre serveur, et la limite Standard de 5 par minute et par visiteur compte tous vos visiteurs ensemble. Choisissez la [limite de débit](https://ikiform.com/fr/docs/rate-limits.md) Relaxed ou Custom pour un formulaire que vous envoyez via un serveur, et gardez `IKIFORM_SECRET_KEY` dans des variables d'environnement réservées au serveur. Chaque exemple serveur transmet l'en-tête `Origin` du navigateur quand il existe, car le `fetch` d'un serveur n'en envoie aucun et Ikiform s'en sert pour compter les soumissions de test gratuites depuis `localhost`.

**TanStack Start**

La server function lit le `FormData` dans `.validator()`, l'envoie depuis le serveur et renvoie soit l'adresse de réponse, soit une erreur par champ.

`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>
  );
}
```

Les versions côté serveur envoient avec une clé secrète, donc Ikiform ignore **Allowed domains** et sa vérification humaine. Chaque envoi vient alors de l'IP de votre serveur, et la limite Standard de 5 par minute et par visiteur compte tous vos visiteurs ensemble. Choisissez la [limite de débit](https://ikiform.com/fr/docs/rate-limits.md) Relaxed ou Custom pour un formulaire que vous envoyez via un serveur, et gardez `IKIFORM_SECRET_KEY` dans des variables d'environnement réservées au serveur. Chaque exemple serveur transmet l'en-tête `Origin` du navigateur quand il existe, car le `fetch` d'un serveur n'en envoie aucun et Ikiform s'en sert pour compter les soumissions de test gratuites depuis `localhost`.

**React Router**

L'`action` de la route transmet le message et renvoie les erreurs de champ d'Ikiform via `actionData`, en conservant le code de statut d'Ikiform grâce à `data()`.

`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>
  );
}
```

Les versions côté serveur envoient avec une clé secrète, donc Ikiform ignore **Allowed domains** et sa vérification humaine. Chaque envoi vient alors de l'IP de votre serveur, et la limite Standard de 5 par minute et par visiteur compte tous vos visiteurs ensemble. Choisissez la [limite de débit](https://ikiform.com/fr/docs/rate-limits.md) Relaxed ou Custom pour un formulaire que vous envoyez via un serveur, et gardez `IKIFORM_SECRET_KEY` dans des variables d'environnement réservées au serveur. Chaque exemple serveur transmet l'en-tête `Origin` du navigateur quand il existe, car le `fetch` d'un serveur n'en envoie aucun et Ikiform s'en sert pour compter les soumissions de test gratuites depuis `localhost`.

**SvelteKit**

L'action par défaut renvoie `fail()` avec les erreurs de champ. `update({ reset: false })` conserve ce que le visiteur a saisi quand le serveur répond par une erreur. Déclarez `IKIFORM_SECRET_KEY` dans `src/env.ts` avec `defineEnvVars` pour que `$app/env/private` puisse la lire.

`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}
```

Les versions côté serveur envoient avec une clé secrète, donc Ikiform ignore **Allowed domains** et sa vérification humaine. Chaque envoi vient alors de l'IP de votre serveur, et la limite Standard de 5 par minute et par visiteur compte tous vos visiteurs ensemble. Choisissez la [limite de débit](https://ikiform.com/fr/docs/rate-limits.md) Relaxed ou Custom pour un formulaire que vous envoyez via un serveur, et gardez `IKIFORM_SECRET_KEY` dans des variables d'environnement réservées au serveur. Chaque exemple serveur transmet l'en-tête `Origin` du navigateur quand il existe, car le `fetch` d'un serveur n'en envoie aucun et Ikiform s'en sert pour compter les soumissions de test gratuites depuis `localhost`.

**Nuxt**

Une route serveur détient la clé secrète et transmet le message d'Ikiform comme `message` de `createError`, et les erreurs de champ dans son `data` ; `$fetch` renvoie les deux côté client sous `data.message` et `data.data.fields`. Nuxt remplit `runtimeConfig.ikiformSecretKey` à partir de `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>
```

Les versions côté serveur envoient avec une clé secrète, donc Ikiform ignore **Allowed domains** et sa vérification humaine. Chaque envoi vient alors de l'IP de votre serveur, et la limite Standard de 5 par minute et par visiteur compte tous vos visiteurs ensemble. Choisissez la [limite de débit](https://ikiform.com/fr/docs/rate-limits.md) Relaxed ou Custom pour un formulaire que vous envoyez via un serveur, et gardez `IKIFORM_SECRET_KEY` dans des variables d'environnement réservées au serveur. Chaque exemple serveur transmet l'en-tête `Origin` du navigateur quand il existe, car le `fetch` d'un serveur n'en envoie aucun et Ikiform s'en sert pour compter les soumissions de test gratuites depuis `localhost`.

**React**

Sans serveur à vous, le navigateur envoie directement à Ikiform. Aucune clé ne figure dans ce code ; **Allowed domains** décide quels sites peuvent envoyer.

`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**

Le même envoi côté navigateur sous forme de composant Vue. Le formulaire garde son `action`, il s'envoie donc même si le script ne se charge pas.

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

- Remplacez `frm_xxxxxxxxxxxxxxxx` par l'id de votre formulaire, visible dans le tableau de bord.
- `name="email"` fait de ce champ l'adresse de réponse de l'e-mail de notification. Gardez ce nom, même si le libellé dit « Votre e-mail ».
- `page` est un champ masqué ordinaire. Il apparaît dans l'e-mail, pour que vous sachiez de quelle page vient le message.
- `_gotcha` est le honeypot. Les humains ne le voient jamais ; un bot qui le remplit est stocké comme spam et n'envoie d'e-mail à personne.
- `_redirect` ne fonctionne que pour une URL `https:` sur l'un des **Allowed domains** du formulaire. Omettez-le et définissez plutôt **Redirect after submit** dans les réglages du formulaire, sinon Ikiform affiche sa propre page de remerciement.

Les guides par framework vont plus loin : [Next.js](https://ikiform.com/fr/docs/guides/nextjs.md), [Astro](https://ikiform.com/fr/docs/guides/astro.md), [SvelteKit](https://ikiform.com/fr/docs/guides/sveltekit.md), [Nuxt](https://ikiform.com/fr/docs/guides/nuxt.md), [React](https://ikiform.com/fr/docs/guides/react.md), [Vue](https://ikiform.com/fr/docs/guides/vue.md) et [HTML simple](https://ikiform.com/fr/docs/html.md). Les créateurs de sites comme [Webflow](https://ikiform.com/fr/docs/guides/webflow.md) et [Carrd](https://ikiform.com/fr/docs/guides/carrd.md) ont leurs propres pages, et le [modèle de formulaire de contact](https://ikiform.com/forms/contact-form) ajoute un champ de sujet.

Définissez aussi les champs dans le tableau de bord : Ikiform refuse alors un envoi non conforme avec `422 validation_failed` et un message par champ, que le code ci-dessus affiche à côté de chaque input. Essayez ici ; cela exécute le même validateur dans votre navigateur :

Formulaire : Contact

- **Nom** (`name`, text, obligatoire)
- **Votre e-mail** (`email`, email, obligatoire)
- **Message** (`message`, textarea, obligatoire)

Collez cette définition dans l'onglet **Fields** du formulaire. Sans définition, Ikiform accepte n'importe quel nom de champ et envoie par e-mail tout ce qui arrive.

## À quoi ressemble l'e-mail

Chaque soumission acceptée envoie un e-mail simple et lisible. Voici la version texte de l'un d'eux :

`E-mail de notification`:

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

- Il vient de `notifications@ikiform.com`. Vous n'avez ni enregistrements DNS ni domaine d'envoi à configurer, car le courrier part du domaine d'Ikiform.
- L'objet est toujours `New submission: <form name>`. `_subject` ne change pas l'objet des e-mails de notification. Il est transmis aux webhooks et à l'API sous `meta.subject`. Pour distinguer vos formulaires, donnez à chacun un nom clair dans le tableau de bord, comme « Contact, page tarifs ».
- Les champs apparaissent dans l'ordre de l'envoi, libellés par leur `name`. Les champs qui commencent par `_` sont omis.
- Un formulaire ou une réponse très longs sont raccourcis, et l'e-mail indique combien de champs supplémentaires se trouvent dans le tableau de bord. La [page des limites](https://ikiform.com/fr/docs/limits.md#notifications) donne les chiffres.
- Les champs qui ressemblent à des mots de passe ou à des numéros de carte sont omis, et l'e-mail d'un envoi qui en contient est retenu pour vérification. Ne collectez pas d'identifiants avec un backend de formulaires.
- Les membres de l'espace de travail ont un bouton **View submission**. Les personnes extérieures à l'espace de travail reçoivent les champs et un lien de désinscription.

## Qui reçoit l'e-mail

Les destinataires se règlent par formulaire dans le tableau de bord, jamais par le formulaire lui-même. Un champ qui désignerait un destinataire permettrait à n'importe qui d'utiliser votre formulaire pour écrire à n'importe qui : Ikiform n'en a donc pas.

| Qui | Comment les ajouter | Remarques |
| --- | --- | --- |
| Par défaut | Rien à faire | Le membre choisi comme **Notification email** de l'espace de travail, ou tous les propriétaires de l'espace de travail si personne n'est choisi |
| Membres choisis | Cochez-les dans les réglages **Notifications** du formulaire | Uniquement les membres dont l'adresse e-mail est vérifiée |
| Personnes extérieures à l'espace de travail | **Add someone outside the workspace**, puis **Send link** | Elles ne reçoivent d'e-mails qu'après confirmation. Jusqu'à 5 par formulaire, et chacune peut se désinscrire |

Chaque formulaire envoie des e-mails à 10 adresses au plus. Chaque destinataire reçoit sa propre copie plutôt qu'une copie partagée, personne ne voit donc les adresses des autres. Si la boîte d'un destinataire renvoie une erreur, Ikiform suspend un moment les e-mails vers cette adresse et le tableau de bord affiche **Emails bounced** à côté. [Plusieurs destinataires](https://ikiform.com/fr/guides/send-form-to-email/multiple-recipients.md) couvre l'aiguillage par sujet, que les destinataires seuls ne permettent pas.

## Répondre à la personne, pas à Ikiform

Ikiform définit l'adresse de réponse d'abord à partir d'un champ `_replyto`, puis d'un champ nommé `email`, `email_address`, `your_email`, `reply_to`, `contact_email`. La valeur doit être une seule adresse e-mail valide, sinon elle est ignorée. Avec une adresse de réponse, cliquer sur **Reply** dans Gmail, Outlook ou Apple Mail ouvre une réponse au visiteur, et votre réponse part de votre propre adresse.

Sans elle, une réponse part vers `notifications@ikiform.com`, que personne ne lit. C'est la surprise la plus fréquente, et c'est généralement un champ appelé `user_email` ou `Email Address` avec une espace. Renommez-le, ou ajoutez `_replyto`. [Le guide sur l'adresse de réponse](https://ikiform.com/fr/guides/send-form-to-email/reply-to.md) couvre les formulaires à deux champs e-mail et les créateurs de sites qui renomment les champs à votre place.

## Quand beaucoup d'envois arrivent d'un coup

Chaque formulaire envoie jusqu'à 50 e-mails de notification par heure, ou 10 tant que l'espace de travail a moins de 7 jours. Au-delà, les autres soumissions de l'heure sont stockées normalement et arrivent dans un seul e-mail récapitulatif à la fin de l'heure. Ce plafond protège votre boîte, et la réputation des e-mails d'Ikiform auprès de votre fournisseur, quand un formulaire est partagé à grande échelle ou qu'un bot passe à travers les filtres.

Schéma : Ce qui arrive aux e-mails de notification pendant une heure chargée

1. Les 50 premières soumissions d'un formulaire dans une heure pleine envoient chacune un e-mail comme d'habitude.
2. Les soumissions suivantes de la même heure sont stockées et affichées tout de suite dans le tableau de bord, sans e-mail individuel.
3. À la fin de l'heure, chaque destinataire reçoit un récapitulatif qui les compte et renvoie vers le tableau de bord.

Le plafond s'applique par formulaire : un pic sur un formulaire n'en ralentit pas un autre. Les webhooks ne sont pas plafonnés et partent toujours pour chaque soumission.

L'objet du récapitulatif indique le nombre et le nom du formulaire ; il ne liste pas les champs et ne définit pas d'adresse de réponse, répondez donc à ces messages depuis le tableau de bord. Si vous dépassez régulièrement le plafond, l'e-mail n'est pas le bon canal pour ce formulaire : envoyez-le plutôt vers [Slack](https://ikiform.com/blog/form-submissions-to-slack.md) ou une boîte partagée via un [webhook](https://ikiform.com/fr/docs/webhooks.md).

## Tenir le spam hors de votre boîte

L'e-mail rend le spam coûteux : chaque envoi de bot est une notification que quelqu'un ouvre. Ikiform stocke le spam avec une réponse de succès normale, pour que le bot n'apprenne rien, et n'envoie aucun e-mail pour lui. Quatre éléments décident de ce qui compte comme spam :

| Défense | Ce qu'elle arrête | Configuration |
| --- | --- | --- |
| Honeypot | Les bots qui remplissent tous les inputs | L'input masqué `_gotcha` |
| Score de spam | Des liens dans le champ nom, trois liens ou plus, des formules de spam connues, des adresses jetables et des répétitions identiques. À 5, l'envoi est du spam | Aucune |
| Domaines autorisés | Les envois depuis d'autres sites | Ajoutez votre domaine dans les réglages du formulaire |
| Vérification humaine | Les bots qui pilotent un vrai navigateur | Réglez **Spam check** sur **Ikiform** |

Jetez de temps en temps un œil au filtre **Spam** du tableau de bord. Si un vrai message s'y retrouve, marquez-le comme non-spam et son e-mail part à ce moment-là. La [documentation anti-spam](https://ikiform.com/fr/docs/spam.md) explique chaque signal, y compris quand activer la vérification humaine.

## Ce que l'e-mail ne sait pas faire

Ikiform n'envoie pas d'e-mail à la personne qui remplit le formulaire. Les e-mails de notification vont uniquement à vous et à votre équipe : il n'y a donc ni réponse automatique, ni e-mail de confirmation, ni double opt-in. Envoyez-les depuis votre outil d'e-mailing, déclenché par un [webhook](https://ikiform.com/fr/docs/webhooks.md). Si votre site promet « nous avons bien reçu votre message », affichez-le sur la page après l'envoi, là où le code ci-dessus le fait déjà.

- **Une boîte d'équipe partagée.** Ajoutez l'adresse de l'équipe comme destinataire, ou faites un transfert depuis votre fournisseur de messagerie.
- **Un canal de discussion.** Envoyez chaque soumission vers [Slack](https://ikiform.com/blog/form-submissions-to-slack.md), [Discord](https://ikiform.com/blog/form-submissions-to-discord.md) ou [Microsoft Teams](https://ikiform.com/blog/form-submissions-to-microsoft-teams.md) par webhook. Le chat convient aux formulaires qui reçoivent plus de quelques messages par jour.
- **Un tableur ou un CRM.** [Google Sheets](https://ikiform.com/blog/form-submissions-to-google-sheets.md), ou n'importe quel outil via [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) ou [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md).
- **Les pièces jointes.** L'envoi de fichiers n'est pas encore pris en charge. Un envoi avec un fichier reçoit `422 files_not_supported`. Demandez plutôt un lien vers un fichier partagé.

### Désactiver l'e-mail

Certains formulaires se passent mieux d'e-mail, comme une inscription à une newsletter très active. Désactivez **Email new submissions** dans les réglages **Notifications** du formulaire. Les soumissions sont toujours stockées, et les webhooks partent toujours.

> Le plafond est plus bas sur les nouveaux espaces de travail : 10 e-mails par formulaire et par heure pendant les 7 premiers jours. Si vous testez avec un script, attendez-vous à recevoir un récapitulatif plus tôt que plus tard.

## Choisissez votre cas

La configuration ci-dessus est la même partout. Ces pages couvrent une boîte précise, une stack précise ou l'outil que vous quittez.

- [Sans PHP](https://ikiform.com/fr/guides/send-form-to-email/without-php.md): Remplacez un script contact.php qui appelle mail() par un formulaire qui envoie vers un endpoint hébergé, en gardant vos noms de champs, votre page de remerciement et les réponses.
- [Vers Gmail](https://ikiform.com/fr/guides/send-form-to-email/gmail.md): Recevez les soumissions dans Gmail sans liens mailto:, mots de passe d'application ni Apps Script, puis libellez-les, évitez les spams et répondez depuis Gmail.
- [Plusieurs destinataires](https://ikiform.com/fr/guides/send-form-to-email/multiple-recipients.md): Envoyez un formulaire à toute votre équipe, ou aiguillez chaque message vers la bonne personne selon le sujet, sans laisser le formulaire choisir qui reçoit l'e-mail.
- [Répondre depuis votre boîte](https://ikiform.com/fr/guides/send-form-to-email/reply-to.md): Faites que Répondre sur une notification écrive à la personne qui a rempli le formulaire : noms de champs valides, quand utiliser _replyto, et pourquoi les réponses s'égarent.
- [Alternative à EmailJS](https://ikiform.com/fr/guides/send-form-to-email/emailjs-alternative.md): Quittez EmailJS pour un formulaire de contact : pourquoi les clés côté navigateur sont un risque, ce qui change dans le code, et ce que vous perdez, comme la réponse automatique.
- [Next.js](https://ikiform.com/fr/guides/send-form-to-email/nextjs.md): Deux façons d'envoyer un formulaire Next.js par e-mail avec une Server Action : l'envoyer vous-même avec Resend ou Nodemailer, ou le transmettre à un backend.

## Vérifiez que tout fonctionne

Faites ces vérifications avant de mettre le formulaire en ligne. Ikiform n'a pas d'offre gratuite. Les tests sont gratuits : un espace de travail sans offre accepte 50 soumissions de test par mois depuis `localhost`. Pour passer en production, choisissez Pro à partir de 6 $/mois pour 500 soumissions, ou payez une seule fois : 10 000 soumissions pour 49 $ ou 50 000 soumissions pour 149 $, avec des crédits qui n'expirent jamais. Voir les [tarifs](https://ikiform.com/fr/pricing).

1. Dans **Allowed domains** du formulaire, ajoutez le domaine de votre site, plus `localhost` pendant le développement. Laissez vide pour accepter les envois de partout.
2. Dans les réglages **Notifications** du formulaire, vérifiez que **Email new submissions** est activé et que les bonnes personnes sont cochées.
3. Remplissez le formulaire sur votre page et envoyez-le. Vous arrivez sur votre page de remerciement, ou sur celle hébergée par Ikiform.
4. Ouvrez **Submissions** dans le tableau de bord. La nouvelle soumission y est, avec tous les champs envoyés.
5. Ouvrez l'e-mail de notification. L'objet est `New submission:` suivi du nom du formulaire, et tous les champs envoyés sont listés.
6. Cliquez sur **Reply**. La ligne **To** affiche l'adresse saisie dans le formulaire, pas celle d'Ikiform.
7. Envoyez avec le honeypot rempli, `_gotcha=bot`. L'envoi reçoit quand même une réponse de succès, mais la soumission est stockée comme spam et n'envoie ni e-mail ni webhook.
8. Rien n'est arrivé ? Passez le filtre de statut du tableau de bord sur **Spam**. Le spam reçoit la même réponse de succès qu'une vraie soumission.

## Dépannage

| Problème | Que faire |
| --- | --- |
| La soumission est dans le tableau de bord mais aucun e-mail n'est arrivé | Regardez d'abord dans votre dossier spam ou promotions, et ajoutez `notifications@ikiform.com` à vos contacts. Vérifiez ensuite les réglages **Notifications** du formulaire : l'e-mail est peut-être désactivé, une adresse extérieure affiche peut-être encore **Waiting for confirmation**, ou **Emails bounced**. |
| La soumission est dans le tableau de bord mais aucun e-mail ni webhook n'est arrivé | Elle a été stockée comme spam. Le spam n'envoie rien. Ouvrez-la sous le filtre **Spam** pour voir les raisons, et marquez-la comme non-spam pour envoyer son e-mail et ses webhooks. |
| **Reply** répond à notifications@ikiform.com | Aucune adresse de réponse n'a été trouvée. Nommez le champ e-mail `email`, `email_address`, `your_email`, `reply_to` ou `contact_email`, ou ajoutez un champ `_replyto` avec l'adresse du visiteur. |
| Les e-mails se sont arrêtés et un récapitulatif est arrivé à la place | Le formulaire a dépassé 50 e-mails dans l'heure (10 sur un nouvel espace de travail). Tout est dans le tableau de bord. Cherchez un pic de bots sous **Spam**, ou utilisez un webhook pour les formulaires très actifs. |
| `_subject` ne change pas l'objet | `_subject` ne change pas l'objet des e-mails de notification. Il est transmis aux webhooks et à l'API sous `meta.subject`. |
| `403 origin_not_allowed`, ou tous les envois finissent en spam | Le domaine de la page n'est pas dans les **Allowed domains** du formulaire. Ajoutez-le, plus `localhost` pour les tests en local. Les sous-domaines d'un domaine listé sont autorisés. |
| `422 validation_failed` | L'envoi ne correspond pas à la définition des champs du formulaire. `error.fields` nomme chaque champ et la raison. Faites correspondre le `name` de l'input à l'id du champ. |
| `402 test_limit_reached` | L'espace de travail n'a pas d'offre et a utilisé ses 50 tests gratuits depuis `localhost` ce mois-ci. Choisissez une offre, ou utilisez **Send test** dans le tableau de bord. |

## Questions

### Un formulaire HTML peut-il envoyer un e-mail sans serveur ?

Pas tout seul. Un formulaire ne fait qu'une requête HTTP, et envoyer un e-mail demande un serveur qui parle à un serveur mail. Un backend de formulaires est ce serveur : faites pointer l'action du formulaire vers lui et il vous envoie un e-mail, votre site peut donc rester statique.

### Pourquoi ne pas utiliser mailto : dans l'action du formulaire ?

Une action mailto : n'envoie rien. Elle demande à l'ordinateur du visiteur d'ouvrir une appli de messagerie avec un brouillon, ce qui échoue pour quiconque n'en a pas configuré, et le visiteur doit encore cliquer sur Envoyer. Beaucoup abandonnent à ce stade, et vous ne saurez jamais qu'ils ont essayé.

### Puis-je changer l'objet de l'e-mail de notification ?

Non. L'objet est toujours « New submission : » suivi du nom du formulaire : donnez donc un nom clair à chaque formulaire. Un champ _subject est stocké et transmis aux webhooks et à l'API, mais ne change pas l'e-mail.

### Combien de personnes peuvent recevoir l'e-mail ?

Jusqu'à 10 par formulaire : les membres de l'espace de travail que vous cochez dans les réglages du formulaire, plus jusqu'à 5 adresses extérieures qui confirment par lien. Chacun reçoit sa propre copie.

### Ikiform peut-il envoyer un e-mail à la personne qui a rempli le formulaire ?

Non. Ikiform envoie des e-mails à vous et à votre équipe, pas à l'expéditeur : il n'y a donc ni réponse automatique ni e-mail de confirmation. Envoyez un webhook à votre outil d'e-mailing et envoyez la confirmation depuis celui-ci.

### Les e-mails du formulaire vont-ils finir en spam ?

Ils sont envoyés depuis le domaine d'Ikiform, vous n'avez donc pas besoin d'enregistrements SPF ou DKIM pour votre domaine. Le premier peut quand même atterrir dans les spams ou les promotions ; ajoutez notifications@ikiform.com à vos contacts ou créez un filtre pour que cela n'arrive jamais.

### Ikiform est-il gratuit ?

Non, il n'y a pas d'offre gratuite. Les tests sont gratuits : vous pouvez envoyer 50 soumissions de test par mois depuis localhost avant de payer. Pour passer en production, Pro commence à 6 $/mois pour 500 soumissions, ou vous pouvez payer une seule fois 10 000 soumissions pour 49 $ ou 50 000 soumissions pour 149 $, avec des crédits qui n'expirent jamais.

## Autres guides

- [Sans PHP](https://ikiform.com/fr/guides/send-form-to-email/without-php.md): Remplacez un script contact.php qui appelle mail() par un formulaire qui envoie vers un endpoint hébergé, en gardant vos noms de champs, votre page de remerciement et les réponses.
- [Vers Gmail](https://ikiform.com/fr/guides/send-form-to-email/gmail.md): Recevez les soumissions dans Gmail sans liens mailto:, mots de passe d'application ni Apps Script, puis libellez-les, évitez les spams et répondez depuis Gmail.
- [Plusieurs destinataires](https://ikiform.com/fr/guides/send-form-to-email/multiple-recipients.md): Envoyez un formulaire à toute votre équipe, ou aiguillez chaque message vers la bonne personne selon le sujet, sans laisser le formulaire choisir qui reçoit l'e-mail.
- [Répondre depuis votre boîte](https://ikiform.com/fr/guides/send-form-to-email/reply-to.md): Faites que Répondre sur une notification écrive à la personne qui a rempli le formulaire : noms de champs valides, quand utiliser _replyto, et pourquoi les réponses s'égarent.
- [Alternative à EmailJS](https://ikiform.com/fr/guides/send-form-to-email/emailjs-alternative.md): Quittez EmailJS pour un formulaire de contact : pourquoi les clés côté navigateur sont un risque, ce qui change dans le code, et ce que vous perdez, comme la réponse automatique.
- [Next.js](https://ikiform.com/fr/guides/send-form-to-email/nextjs.md): Deux façons d'envoyer un formulaire Next.js par e-mail avec une Server Action : l'envoyer vous-même avec Resend ou Nodemailer, ou le transmettre à un backend.
- [Arrêter le spam des formulaires de contact](https://ikiform.com/fr/guides/spam-protection.md): Arrêtez le spam des formulaires de contact avec des couches invisibles pour les vrais visiteurs : honeypot, délai, score du contenu, limites de débit, vérification humaine si besoin.
- [Dépasser les formulaires intégrés](https://ikiform.com/fr/guides/website-builder-forms.md): Quand les formulaires de Webflow, Netlify, Carrd ou Framer atteignent un plafond ou manquent d'une fonctionnalité, gardez le formulaire que vous avez conçu et faites-le pointer vers un backend de formulaires.
- [Formulaire de feedback pour un site ou une application](https://ikiform.com/fr/guides/feedback-form.md): Placez un formulaire de feedback sur n'importe quelle page ou dans votre application : une note, un commentaire, la page d'origine, le filtrage du spam et des alertes quand une note est basse.
