# Comment créer une liste d'attente pour un nouveau SaaS

> Créez une liste d'attente avant lancement sur votre landing page : le formulaire, l'anti-spam, les doublons, les e-mails de confirmation et un export propre le jour J.

Publié le 2026-10-25 · https://ikiform.com/fr/guides/waitlist

Placez sur votre landing page un formulaire HTML simple qui envoie l'e-mail du visiteur à un backend de formulaires, pour ne pas gérer de serveur avant le lancement. Transmettez chaque inscription par webhook à l'outil d'e-mailing avec lequel vous lancerez, et laissez cet outil envoyer les éventuelles confirmations. Le jour du lancement, exportez la liste en CSV, supprimez les doublons par e-mail et envoyez un seul e-mail de lancement.

Une liste d'attente est le test le moins cher d'une idée de SaaS. Si des inconnus laissent leur e-mail pour quelque chose qui n'existe pas encore, vous avez une raison de le construire, et une liste de personnes à prévenir quand il sortira. Ce guide en construit une sur votre propre landing page, sans base de données ni serveur, et la prépare pour le jour du lancement.

## Comment fonctionne une liste d'attente

Toute liste d'attente a les trois mêmes éléments : un formulaire sur votre page, un endroit qui stocke chaque inscription, et l'outil depuis lequel vous écrirez à la liste. Ici, le formulaire envoie à Ikiform. Ikiform vérifie le spam, stocke l'envoi, vous prévient par e-mail et envoie un webhook signé. Un petit relais ou un outil d'automatisation transmet l'adresse à votre outil d'e-mailing.

Schéma : Le parcours d'une inscription, de votre landing page à votre outil d'e-mailing

1. Un visiteur saisit son e-mail sur votre landing page, et le formulaire l'envoie à votre endpoint Ikiform.
2. Ikiform vérifie le honeypot, le score de spam et la limite de débit, puis stocke l'inscription.
3. L'inscription apparaît dans le tableau de bord, où vous pouvez la rechercher et exporter la liste en CSV.
4. Ikiform vous écrit, à vous et à votre équipe. Il n'écrit jamais à la personne qui s'est inscrite.
5. Ikiform envoie un webhook submission.created signé.
6. Un relais ou un outil d'automatisation ajoute l'adresse à votre outil d'e-mailing, qui envoie les éventuelles confirmations et, plus tard, l'e-mail de lancement.

L'outil d'e-mailing est facultatif au départ. Vous pouvez collecter pendant des semaines et importer un CSV plus tard ; le webhook garde les deux synchronisés dès le début.

## Quatre façons de gérer une liste d'attente

Il n'y a pas d'outil idéal unique. Le tableau compare les quatre configurations courantes selon leurs points forts et ce qu'elles vous coûtent plus tard.

| Approche | Points forts | Compromis |
| --- | --- | --- |
| SaaS de liste d'attente, comme LaunchList ou Waitlister | Liens de parrainage, classements et position dans la file, clés en main. Une page hébergée si vous n'avez pas encore de site. | Votre liste vit dans un autre produit. Le widget ressemble au leur sauf si vous payez ou le restylez. Le prix augmente généralement avec les inscriptions. |
| Formulaire intégré d'un form builder, comme [Typeform](https://ikiform.com/alternatives/typeform) ou [Tally](https://ikiform.com/alternatives/tally) | Des parcours soignés à une question, sans aucun code. | Une iframe qui ne s'accorde pas à votre page et charge ses propres scripts. La marque, les limites de réponses et les webhooks dépendent de l'offre. |
| Votre propre base de données et route d'API | Contrôle total sur les données et toute logique, comme les codes de parrainage. | Vous construisez l'anti-spam, les limites de débit, les notifications et l'export, et il vous faut un serveur même si la landing page est statique. |
| Backend de formulaires, comme Ikiform | Votre propre HTML sur n'importe quel site. Filtrage du spam, stockage, alertes par e-mail, webhooks signés et export CSV, sans serveur. | Pas de classement de parrainage ni d'e-mails à la personne qui s'inscrit. Ceux-ci viennent de votre outil d'e-mailing. |

Choisissez un SaaS de liste d'attente si les mécaniques de parrainage sont tout votre plan. Choisissez un backend de formulaires si vous voulez que le formulaire ressemble au reste de votre site, garder une liste portable, et envoyer les e-mails depuis l'outil que vous utiliserez de toute façon après le lancement. La suite de ce guide utilise le backend de formulaires.

## Le formulaire

Pour commencer, ne demandez qu'un e-mail. Chaque champ supplémentaire coûte des inscriptions, et vous pourrez poser des questions plus tard, par e-mail, à des personnes qui ont déjà dit oui. Choisissez votre stack ci-dessous ; ce choix s'applique à tous les exemples de code de ces guides.

**HTML**

Collez le formulaire là où l'inscription doit apparaître. Il envoie de façon classique puis redirige, il fonctionne donc sans aucun JavaScript.

`index.html`:

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

Pour garder les visiteurs sur la page, ajoutez ce script après le formulaire. Il envoie avec `fetch`, remplace le formulaire par la phrase de remerciement et affiche les erreurs de champ du serveur.

`waitlist.js`:

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

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

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const button = form.querySelector("button");
  button.disabled = true;
  status.textContent = "";
  try {
    const response = await fetch(form.action, {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new FormData(form),
    });
    const result = await response.json();
    if (result.ok) {
      form.replaceWith("You're on the list. We'll email you at launch.");
      return;
    }
    const fields = Object.entries(result.error.fields ?? {});
    status.textContent = fields.length
      ? fields.map(([id, messages]) => `${labels[id] ?? id}: ${messages[0]}`).join(" ")
      : result.error.message;
  } catch {
    status.textContent = "Couldn't reach the server. Try again.";
  }
  button.disabled = false;
});
```

Pour aller plus loin avec HTML, lisez [Comment ajouter un formulaire de liste d'attente en HTML simple à n'importe quelle landing page](https://ikiform.com/fr/guides/waitlist/html.md).

**Next.js**

Une server action transmet l'inscription avec votre clé secrète, et `useActionState` affiche le résultat sans rechargement de page. Le formulaire envoie quand même si JavaScript n'est pas encore chargé.

`app/waitlist/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";

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

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

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      Origin: (await headers()).get("origin") ?? "",
    },
    body: JSON.stringify({
      email,
      ref: String(formData.get("ref") ?? "homepage"),
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json().catch(() => null);
  if (result?.ok) return { ok: true, message: "You're on the list. We'll email you on launch day." };
  return {
    ok: false,
    message: result?.error?.fields?.email?.[0] ?? result?.error?.message ?? "Couldn't join the list. Try again.",
    email,
  };
}
```

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

```tsx
"use client";

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

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

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

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

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

Pour aller plus loin avec Next.js, lisez [Comment créer une liste d'attente Next.js avec une server action](https://ikiform.com/fr/guides/waitlist/nextjs.md).

**Astro**

Une Astro Action avec `accept: "form"` valide l'e-mail avec Zod et le transmet. Le formulaire envoie sans JavaScript côté client, la page doit donc être rendue à la demande : ajoutez un adaptateur et gardez `prerender = false`.

`src/actions/index.ts`:

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

export const server = {
  joinWaitlist: defineAction({
    accept: "form",
    input: z.object({
      email: z.email(),
      ref: z.string().default("homepage"),
      _gotcha: z.string().nullish(),
    }),
    handler: async (input, context) => {
      const origin = context.request.headers.get("origin");
      const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          Authorization: `Bearer ${getSecret("IKIFORM_SECRET_KEY")}`,
          ...(origin ? { Origin: origin } : {}),
        },
        body: JSON.stringify(input),
      });
      const result = await response.json();
      if (!result.ok) {
        throw new ActionError({
          code: "BAD_REQUEST",
          message: result.error.fields?.email?.[0] ?? result.error.message,
        });
      }
      return { joined: true };
    },
  }),
};
```

`src/pages/index.astro`:

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

export const prerender = false;

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

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

Pour aller plus loin avec Astro, lisez [Créer une liste d'attente avec Astro](https://ikiform.com/fr/guides/waitlist/astro.md).

**TanStack Start**

Une fonction POST `createServerFn` reçoit le `FormData`, le vérifie dans `.validator()` et le transmet depuis le serveur. La route l'appelle via `useServerFn`.

`src/routes/index.tsx`:

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

const joinWaitlist = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    const email = String(data.get("email") ?? "").trim();
    if (!email.includes("@")) throw new Error("Enter the email we should send your invite to.");
    return {
      email,
      ref: String(data.get("ref") ?? "homepage"),
      _gotcha: String(data.get("_gotcha") ?? ""),
    };
  })
  .handler(async ({ data }) => {
    const origin = getRequestHeader("origin");
    const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
        ...(origin ? { Origin: origin } : {}),
      },
      body: JSON.stringify(data),
    });
    const result = await response.json();
    if (result.ok) return { ok: true, message: "You're on the list. We'll email you on launch day." };
    return { ok: false, message: String(result.error.fields?.email?.[0] ?? result.error.message) };
  });

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

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

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

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

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

Pour aller plus loin avec TanStack Start, lisez [Créer une liste d'attente avec TanStack Start](https://ikiform.com/fr/guides/waitlist/tanstack-start.md).

**React Router**

L'`action` de la route transmet l'envoi, et `actionData` renvoie le résultat au composant. `data()` conserve le code de statut d'Ikiform en cas d'erreur.

`app/routes/home.tsx`:

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

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

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      Origin: request.headers.get("origin") ?? "",
    },
    body: JSON.stringify({
      email,
      ref: String(formData.get("ref") ?? "homepage"),
      _gotcha: String(formData.get("_gotcha") ?? ""),
    }),
  });
  const result = await response.json();
  if (result.ok) return { ok: true, message: "You're on the list. We'll email you on launch day." };
  return data(
    { ok: false, message: String(result.error.fields?.email?.[0] ?? result.error.message) },
    { status: response.status },
  );
}

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

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

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

Pour aller plus loin avec React Router, lisez [Créer une liste d'attente avec React Router](https://ikiform.com/fr/guides/waitlist/react-router.md).

**SvelteKit**

Une form action par défaut transmet l'envoi et renvoie `fail()` avec le message en cas d'erreur. `use:enhance` envoie sans rechargement et garde l'e-mail saisi dans l'input. Dans SvelteKit 3, déclarez `IKIFORM_SECRET_KEY` dans `src/env.ts` avec `defineEnvVars` pour que `$app/env/private` puisse la lire.

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

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

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

    const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${IKIFORM_SECRET_KEY}`,
        Origin: request.headers.get("origin") ?? url.origin,
      },
      body: JSON.stringify({
        email,
        ref: String(data.get("ref") ?? "homepage"),
        _gotcha: String(data.get("_gotcha") ?? ""),
      }),
    });
    const result = await response.json();
    if (!result.ok) {
      return fail(response.status, {
        email,
        message: String(result.error.fields?.email?.[0] ?? result.error.message),
      });
    }
    return { joined: true };
  },
} satisfies Actions;
```

`src/routes/+page.svelte`:

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

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

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

Pour aller plus loin avec SvelteKit, lisez [Créer une liste d'attente avec SvelteKit](https://ikiform.com/fr/guides/waitlist/sveltekit.md).

**Nuxt**

Une route serveur garde la clé secrète hors du navigateur. Définissez `NUXT_IKIFORM_SECRET_KEY` dans l'environnement, et Nuxt en remplit `runtimeConfig.ikiformSecretKey`.

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

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

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

`nuxt.config.ts`:

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

`app/components/WaitlistForm.vue`:

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

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

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

Pour aller plus loin avec Nuxt, lisez [Créer une liste d'attente avec Nuxt](https://ikiform.com/fr/guides/waitlist/nuxt.md).

**React**

Une application monopage n'a pas de serveur à elle : le formulaire envoie donc directement à Ikiform depuis le navigateur. Ne mettez jamais de clé secrète dans ce code ; c'est **Allowed domains** qui protège le formulaire.

`src/WaitlistForm.tsx`:

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

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

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

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

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

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

**Vue**

Le même envoi côté navigateur dans un composant monofichier Vue. Le formulaire garde son `action`, il fonctionne donc même si le script ne se charge pas.

`src/components/WaitlistForm.vue`:

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

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

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

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

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

- L'endpoint est l'URL de votre formulaire. Remplacez `frm_xxxxxxxxxxxxxxxx` par l'id visible dans le tableau de bord.
- `name="email"` est l'id du champ. Gardez `email` : Ikiform l'utilise comme adresse de réponse des e-mails de notification, et le compare aux domaines d'e-mails jetables.
- L'input masqué `_gotcha` est le honeypot. Les humains ne le voient jamais. Les bots qui remplissent tous les inputs se signalent eux-mêmes comme spam, et les versions serveur le transmettent tel quel.
- L'input masqué `ref` enregistre la provenance de l'inscription. Changez sa valeur selon la page ou la campagne.
- Avec une définition de champs dans l'onglet **Fields**, un input qu'elle ne liste pas est refusé avec `422` et une erreur `unknown_field` : listez donc `ref` en plus d'`email`, comme ci-dessous.
- Un envoi HTML classique redirige le navigateur après succès. Réglez **Redirect after submit** dans les réglages du formulaire sur votre propre page de remerciement, sinon Ikiform affiche la sienne.

`Champs`:

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

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

Les versions serveur transmettent l'en-tête `Origin` du navigateur, car Ikiform s'en sert pour repérer les tests gratuits depuis `localhost` et le `fetch` d'un serveur n'en envoie aucun.

Les guides par framework de la documentation 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). Le [SDK](https://ikiform.com/fr/docs/sdk.md) encapsule la requête du navigateur dans `submit()`, et le [modèle de formulaire de liste d'attente](https://ikiform.com/forms/waitlist) génère la version à quatre champs.

## Les champs qui valent la peine

L'e-mail est le seul champ nécessaire. Si vous voulez trier la liste avant le lancement, ajoutez-en deux au plus. Essayez la version à quatre champs ci-dessous : elle exécute le validateur d'Ikiform dans votre navigateur et montre ce qui serait stocké, ou le `422` que vous recevriez.

Formulaire : Waitlist

- **E-mail** (`email`, email, obligatoire)
- **Nom** (`name`, text)
- **Que faites-vous ?** (`role`, select)
- **Comment nous avez-vous connus ?** (`source`, text)
- **Source** (`ref`, text)

Les champs du [modèle de liste d'attente](https://ikiform.com/forms/waitlist) plus `ref`, que votre page remplit avec un input masqué plutôt qu'un champ visible. Collez-la dans l'onglet **Fields** du formulaire et Ikiform l'applique côté serveur.

| Champ | Pourquoi le demander | À garder en tête |
| --- | --- | --- |
| `email` | La liste elle-même | Obligatoire. Utilisez `type="email"` pour que les téléphones affichent le bon clavier |
| `name` | Un e-mail de lancement personnalisé | Facultatif. Un prénom suffit |
| `role` | Inviter d'abord les fondateurs, ou écrire un e-mail différent aux ingénieurs | Un `select` garde les réponses propres pour le filtrage |
| `source` | Savoir quel canal fonctionne avant d'y investir | Le texte libre est sincère mais brouillon. Un input masqué `ref` est plus propre, voir ci-dessous |
| `ref` | Compter les inscriptions par page ou campagne sans rien demander | Un input masqué. `text` facultatif avec `max_length` à 40 |

Pour suivre la provenance des inscriptions sans la demander, ajoutez un input masqué par page ou par campagne. Les noms qui commencent par `_` sont des instructions pour Ikiform et ne sont jamais stockés : utilisez donc un nom simple comme `ref` :

`Inputs masqués`:

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

Chaque soumission enregistre aussi l'origine et le référent de la page, vous voyez donc de quelle page vient une inscription même sans `ref`. L'input `_redirect` envoie le navigateur vers votre propre page de remerciement ; il ne fonctionne que pour une URL `https:` sur l'un des **Allowed domains** du formulaire.

> Si vous comptez envoyer autre chose que des nouvelles du lancement, comme une newsletter, ajoutez une case de consentement obligatoire dont le libellé énonce la promesse exacte. Ikiform stocke `true` avec l'horodatage de la soumission, ce qui enregistre le moment où la personne a accepté. Le [modèle de newsletter](https://ikiform.com/forms/newsletter-signup) en contient une.

## Tenir les bots hors de la liste

Un formulaire public avec un champ e-mail attire les bots en quelques jours. Les fausses inscriptions gonflent vos chiffres et, pire, votre e-mail de lancement atterrit dans des pièges à spam qui nuisent à votre réputation d'expéditeur. Ikiform superpose quatre défenses ; les deux premières sont actives sans configuration.

| Défense | Ce qu'elle arrête | Configuration |
| --- | --- | --- |
| Honeypot | Les bots simples qui remplissent tous les inputs | L'input masqué `_gotcha` du formulaire ci-dessus |
| Score de spam | Adresses jetables (+2), répétitions identiques (+3), liens et formules de spam connues. À 5, une inscription est stockée comme spam | Aucune |
| Limites de débit | Les scripts qui envoient en boucle | Activée par défaut : Standard, 5 par minute et par visiteur, et 300 par minute pour l'ensemble du formulaire |
| Vérification humaine | Les bots qui pilotent un vrai navigateur | Réglez **Spam check** sur **Ikiform**. Cela exécute Cloudflare Turnstile sans clés à gérer |

Le spam est stocké avec une réponse de succès normale, pour que le bot n'apprenne rien. Il n'envoie ni e-mail ni webhook et ne compte pas dans votre consommation. Consultez le filtre **Spam** du tableau de bord une fois par semaine le premier mois ; si une vraie inscription s'y retrouve, marquez-la comme non-spam et son webhook part à ce moment-là.

Ajoutez aussi votre domaine dans **Allowed domains**. Les envois depuis d'autres sites sont alors marqués comme spam, ou refusés avec `403 origin_not_allowed` si vous choisissez **Reject**. Avec la vérification humaine activée, le formulaire HTML n'a besoin d'aucune modification. La version `fetch` doit d'abord demander un jeton avec `challenge()` de `@ikiform/sdk` ; la [documentation anti-spam](https://ikiform.com/fr/docs/spam.md#human-check) montre les quatre lignes.

> Les annonces de lancement arrivent par vagues. Si vous publiez la page quelque part de très visible, la limite globale de 300 par minute peut se déclencher avant celle d'un visiteur. Augmentez-la la veille avec une limite de débit Custom. Tout un bureau ou une conférence partage une même IP : Relaxed, 20 par minute et par visiteur, convient à ces journées.

## La même personne, deux fois

Les gens se réinscrivent depuis un autre onglet, un second appareil, ou une semaine plus tard quand ils ont oublié. Ikiform stocke chaque envoi accepté et ne fusionne ni ne rejette les répétitions par e-mail. Voici ce qui arrive à une répétition :

| Cas | Résultat |
| --- | --- |
| Même e-mail, champs différents ou plus de 24 heures plus tard | Une seconde soumission normale |
| Les mêmes champs à nouveau dans les 24 heures | +3 au score de spam, sous 5 : donc une seconde soumission normale |
| Une répétition identique depuis un domaine d'e-mail jetable | +3 et +2, ce qui atteint 5 : stockée comme spam |

Attendez-vous donc à quelques doublons, et supprimez-les là où la liste aboutit. Trois endroits conviennent, du moins au plus laborieux :

1. **Dans l'outil d'e-mailing.** La plupart des outils d'e-mailing identifient les contacts par adresse : ajouter deux fois la même adresse met à jour un seul contact au lieu d'en créer deux.
2. **À l'export.** Supprimez les e-mails en double du CSV avant de l'importer où que ce soit. Deux méthodes sont détaillées ci-dessous.
3. **Dans votre relais.** Passez l'adresse en minuscules et retirez les espaces, puis insérez-la avec une contrainte d'unicité ou un upsert, pour qu'une répétition soit sans effet.

Vous voulez empêcher un double clic de créer deux lignes ? Envoyez une [clé d'idempotence](https://ikiform.com/fr/docs/frameworks.md#idempotency) par chargement de page avec la version `fetch`. Cela arrête les répétitions accidentelles ; cela n'empêche pas une personne de s'inscrire deux fois exprès.

## Confirmer les inscriptions

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). C'est voulu : l'outil qui envoie votre e-mail de lancement doit aussi envoyer la confirmation, pour que désinscriptions et erreurs de remise soient suivies au même endroit.

Deux façons d'y connecter Ikiform :

- **Sans code.** Ajoutez un endpoint de webhook qui pointe vers un outil d'automatisation, puis utilisez son action pour votre outil d'e-mailing. Voir [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md), [Make](https://ikiform.com/blog/form-submissions-to-make.md) ou [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md).
- **Un petit relais.** Un route handler sur n'importe quel hébergeur Node vérifie la signature et appelle l'API de votre outil d'e-mailing. L'exemple ci-dessous est une route Next.js ; le même code tourne dans un Cloudflare Worker.

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

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

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

- `verifyWebhook` vérifie la signature et l'horodatage, pour que personne d'autre ne puisse ajouter d'adresses via cette URL.
- Remplacez le `fetch` par l'appel d'ajout de contact de votre outil d'e-mailing. La plupart prennent un e-mail et une liste ou un tag, et envoient leur propre e-mail de confirmation quand le double opt-in est activé dans l'outil.
- Répondre `204` indique à Ikiform que la livraison a réussi. Un `502` fait réessayer Ikiform, avec le même `webhook-id`, donc une courte panne de votre outil d'e-mailing ne fait perdre aucune inscription.

Ajoutez l'endpoint sous **Webhooks** dans le tableau de bord et copiez son secret `whsec_` dans `IKIFORM_WEBHOOK_SECRET`. Un espace de travail peut avoir 3 endpoints. La [documentation des webhooks](https://ikiform.com/fr/docs/webhooks.md) couvre les nouvelles tentatives et le payload, et [votre propre serveur](https://ikiform.com/blog/form-submissions-to-your-own-server.md) propose une version qui stocke chaque livraison une seule fois.

## Le jour du lancement

Schéma : Une liste d'attente, de la première inscription au jour du lancement

1. La page est mise en ligne avec le formulaire qui envoie à Ikiform.
2. Chaque semaine, partagez la page et lisez les nouvelles inscriptions et leurs réponses.
3. Une semaine avant le lancement, exportez la liste en CSV, supprimez les doublons et segmentez-la par rôle.
4. Le jour du lancement, envoyez un seul e-mail à la liste depuis votre outil d'e-mailing.

L'essentiel du travail est l'export une semaine avant. L'e-mail de lancement lui-même est court.

### Exporter la liste

Ouvrez **Submissions** dans le tableau de bord, choisissez le formulaire de liste d'attente, réglez le statut sur **Received** et la période sur **All time**, puis choisissez **Export CSV**. Les colonnes sont `id`, `created_at`, `form` et `status`, puis une colonne par champ. Le fichier est en UTF-8 avec BOM, les accents survivent donc à Excel. Un export contient jusqu'à 100 000 lignes ; au-delà, paginez via l'[API REST](https://ikiform.com/fr/docs/guides/export.md#api).

Pour supprimer les doublons dans Google Sheets, importez le CSV et utilisez cette formule dans une feuille vide, avec l'e-mail en colonne E :

`Google Sheets`:

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

Elle garde une ligne par adresse, sans tenir compte de la casse ni des espaces, triée par e-mail. Ou lancez ceci avec Node dans le dossier qui contient l'export. Le script garde la première ligne de chaque adresse et suppose qu'il n'y a pas de virgule dans les valeurs, ce qui est vrai pour un formulaire qui ne demande que l'e-mail :

`dedupe.mjs`:

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

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

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

### Envoyer l'e-mail de lancement

Envoyez-le depuis votre outil d'e-mailing, pas depuis votre propre boîte. Une boîte personnelle qui écrit à des centaines d'adresses d'un coup ressemble à du spam pour les fournisseurs de messagerie, et vous aurez besoin de gérer les désinscriptions dès l'envoi. Faites court :

`E-mail de lancement`:

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

Hi Ada,

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

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

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

Ada, founder of Acme
```

- Dites ce qui est en ligne et mettez un lien direct vers la page d'inscription ou de connexion.
- Récompensez l'attente, par exemple avec un essai plus long ou une réduction, et dites-le une fois.
- Envoyez-le au nom d'une personne. Les réponses sont les meilleurs retours que vous aurez au début.
- Adressez des premières lignes différentes aux fondateurs et aux ingénieurs si vous avez collecté `role`. C'est la raison de le collecter.

> Si vous avez collecté pendant des mois sans confirmer les adresses, envoyez d'abord à une petite partie de la liste et surveillez le taux d'erreurs de remise. Une liste pleine de fautes de frappe et d'adresses mortes nuit au domaine depuis lequel vous enverrez les e-mails du produit.

## Choisissez votre cas

Les étapes ci-dessus fonctionnent partout. Ces pages couvrent ce qui change pour un lancement ou un créateur de sites en particulier.

- [Liste de lancement Product Hunt](https://ikiform.com/fr/guides/waitlist/product-hunt.md): Collectez une liste des semaines avant Product Hunt, taguez chaque inscription par canal et envoyez l'e-mail du jour J en deux vagues calées sur l'heure du Pacifique.
- [Inscription bêta avec sélection](https://ikiform.com/fr/guides/waitlist/beta-signup.md): Un formulaire de candidature bêta avec des questions de sélection appliquées côté serveur, et un script qui note les candidats et choisit chaque cohorte.
- [Liste d'attente d'application mobile](https://ikiform.com/fr/guides/waitlist/mobile-app.md): Une liste d'attente avant lancement pour une app iOS ou Android qui demande le téléphone utilisé, envoie les testeurs iPhone vers TestFlight et remplit un test fermé Play.
- [Liste d'attente Next.js](https://ikiform.com/fr/guides/waitlist/nextjs.md): Une liste d'attente Next.js App Router : une server action qui transmet avec une clé secrète, useActionState, un merci optimiste et une limite par visiteur.
- [Liste d'attente sur Framer](https://ikiform.com/fr/guides/waitlist/framer.md): Deux façons de collecter des inscriptions sur Framer : le formulaire Framer avec un webhook vers Ikiform, ou un code component qui garde limites et anti-spam.
- [Liste d'attente sur Webflow](https://ikiform.com/fr/guides/waitlist/webflow.md): Faites pointer un Form Block Webflow vers Ikiform, taguez les inscriptions par source avec un champ masqué et gardez l'état de succès de Webflow avec un court script.
- [Liste d'attente sur Carrd](https://ikiform.com/fr/guides/waitlist/carrd.md): Collectez des inscriptions sur une page Carrd : un formulaire Send to URL sur Pro Plus avec suivi UTM, ou un formulaire HTML intégré avec honeypot sur Pro Standard.
- [Liste d'attente en HTML simple](https://ikiform.com/fr/guides/waitlist/html.md): Une page de liste d'attente complète en HTML simple, sans build, qui fonctionne sans JavaScript, remercie sur place quand il se charge, et se replie si besoin.
- [Liste d'attente Astro](https://ikiform.com/fr/guides/waitlist/astro.md): Une liste d'attente Astro sur une landing page statique : une Action transmet chaque inscription, un script l'appelle, une page à la demande gère le sans-JS.
- [Liste d'attente TanStack Start](https://ikiform.com/fr/guides/waitlist/tanstack-start.md): Une liste d'attente TanStack Start : une server route répond à un envoi classique comme à fetch, et validateSearch garde la source et le résultat dans l'URL.
- [Liste d'attente React Router](https://ikiform.com/fr/guides/waitlist/react-router.md): Une liste d'attente React Router en pied de chaque page : un fetcher envoie à une seule action, tague chaque inscription avec sa page et fonctionne sans JS.
- [Liste d'attente SvelteKit](https://ikiform.com/fr/guides/waitlist/sveltekit.md): Une liste d'attente SvelteKit 3 : un hook garde le premier ?ref= dans un cookie, une action nommée transmet l'inscription, les visiteurs qui reviennent voient qu'ils sont inscrits.
- [Liste d'attente Nuxt](https://ikiform.com/fr/guides/waitlist/nuxt.md): Une liste d'attente Nuxt sur une landing page pré-rendue : une route Nitro accepte $fetch et les envois classiques, valide avec Zod et redirige sans JavaScript.

## Vérifiez que tout fonctionne

Faites ces vérifications avant de partager la page. 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. Remplissez le formulaire sur votre page et envoyez-le. Vous arrivez sur votre page de remerciement, ou sur celle hébergée par Ikiform.
3. Ouvrez **Submissions** dans le tableau de bord. La nouvelle soumission y est, avec tous les champs envoyés.
4. Vérifiez la boîte de réception de chaque destinataire des notifications. Chacun reçoit un e-mail avec les champs, et **Reply** répond à l'expéditeur quand le formulaire a un champ `email`.
5. Si vous avez ajouté un webhook, ouvrez l'endpoint dans **Webhooks**. La livraison affiche `succeeded` avec la réponse de votre serveur.
6. 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.
7. 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.
8. Inscrivez-vous deux fois avec la même adresse et vérifiez que votre outil d'e-mailing n'affiche toujours qu'un seul contact.

## Dépannage

| Problème | Que faire |
| --- | --- |
| `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. |
| `429 rate_limited` | Un visiteur a envoyé plus que ce que permet la limite de débit du formulaire. Standard autorise 5 envois par minute et par visiteur. Attendez la durée de `Retry-After`, ou choisissez Relaxed ou Custom pour les formulaires qui envoient depuis une seule IP partagée. |
| `403 challenge_required` | Le spam check du formulaire est réglé sur **Ikiform** et un envoi `fetch` n'avait pas de jeton. Obtenez-en un avec `challenge(formId)` de `@ikiform/sdk` avant chaque envoi, ou désactivez la vérification. |
| 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. |
| Le navigateur arrive sur la page de remerciement d'Ikiform au lieu de la vôtre | `_redirect` est ignoré sauf si son domaine figure dans **Allowed domains** et qu'il s'agit d'une URL `https:`. Ajoutez le domaine, ou définissez plutôt **Redirect after submit** dans les réglages du formulaire. |
| Le même e-mail apparaît deux fois | C'est normal. Ikiform ne dédoublonne pas par e-mail. Supprimez les répétitions à l'export, ou faites un upsert par e-mail dans votre relais ou votre outil d'e-mailing. |
| Les inscriptions arrivent dans Ikiform mais pas dans l'outil d'e-mailing | Ouvrez l'endpoint sous **Webhooks** et lisez le journal des livraisons. Un `400` signifie que le secret est faux ; un `502` avec le statut de votre outil signifie que l'outil a refusé le contact. |
| `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

### Ai-je besoin d'un backend pour créer une page de liste d'attente ?

Pas d'un serveur à vous. Le formulaire a besoin d'un endroit où envoyer, et c'est le rôle d'un backend de formulaires : faites pointer l'action du formulaire vers votre endpoint Ikiform, qui stocke, filtre et transmet chaque inscription. Votre landing page peut rester statique.

### Ikiform envoie-t-il un e-mail de confirmation aux personnes qui rejoignent la liste d'attente ?

Non. Ikiform écrit à vous et à votre équipe, pas à la personne qui s'inscrit : il n'y a donc ni e-mail de confirmation ni double opt-in. Envoyez chaque inscription par webhook à votre outil d'e-mailing et activez-y le double opt-in.

### Comment empêcher les fausses inscriptions sur un formulaire de liste d'attente ?

Gardez le honeypot masqué _gotcha, laissez la limite de débit par défaut, ajoutez votre domaine dans Allowed domains, et réglez la vérification anti-spam sur Ikiform pour une vérification Cloudflare Turnstile sans clés à gérer. Les adresses jetables augmentent aussi le score de spam.

### Le même e-mail sera-t-il stocké deux fois ?

C'est possible. Ikiform conserve chaque envoi accepté et ne fusionne pas les répétitions par e-mail. Dédoublonnez là où vit la liste : votre outil d'e-mailing, une colonne unique dans votre base de données, ou le CSV avant de l'importer.

### Puis-je ajouter un programme de parrainage ou une position dans la file ?

Pas avec Ikiform seul. Il n'a ni liens de parrainage ni classements. Vous pouvez passer un code de parrainage dans un champ masqué et le compter vous-même, ou utiliser un produit de liste d'attente bâti autour du parrainage si c'est central pour votre lancement.

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

- [Liste de lancement Product Hunt](https://ikiform.com/fr/guides/waitlist/product-hunt.md): Collectez une liste des semaines avant Product Hunt, taguez chaque inscription par canal et envoyez l'e-mail du jour J en deux vagues calées sur l'heure du Pacifique.
- [Inscription bêta avec sélection](https://ikiform.com/fr/guides/waitlist/beta-signup.md): Un formulaire de candidature bêta avec des questions de sélection appliquées côté serveur, et un script qui note les candidats et choisit chaque cohorte.
- [Liste d'attente d'application mobile](https://ikiform.com/fr/guides/waitlist/mobile-app.md): Une liste d'attente avant lancement pour une app iOS ou Android qui demande le téléphone utilisé, envoie les testeurs iPhone vers TestFlight et remplit un test fermé Play.
- [Liste d'attente Next.js](https://ikiform.com/fr/guides/waitlist/nextjs.md): Une liste d'attente Next.js App Router : une server action qui transmet avec une clé secrète, useActionState, un merci optimiste et une limite par visiteur.
- [Liste d'attente sur Framer](https://ikiform.com/fr/guides/waitlist/framer.md): Deux façons de collecter des inscriptions sur Framer : le formulaire Framer avec un webhook vers Ikiform, ou un code component qui garde limites et anti-spam.
- [Liste d'attente sur Webflow](https://ikiform.com/fr/guides/waitlist/webflow.md): Faites pointer un Form Block Webflow vers Ikiform, taguez les inscriptions par source avec un champ masqué et gardez l'état de succès de Webflow avec un court script.
- [Liste d'attente sur Carrd](https://ikiform.com/fr/guides/waitlist/carrd.md): Collectez des inscriptions sur une page Carrd : un formulaire Send to URL sur Pro Plus avec suivi UTM, ou un formulaire HTML intégré avec honeypot sur Pro Standard.
- [Liste d'attente en HTML simple](https://ikiform.com/fr/guides/waitlist/html.md): Une page de liste d'attente complète en HTML simple, sans build, qui fonctionne sans JavaScript, remercie sur place quand il se charge, et se replie si besoin.
- [Liste d'attente Astro](https://ikiform.com/fr/guides/waitlist/astro.md): Une liste d'attente Astro sur une landing page statique : une Action transmet chaque inscription, un script l'appelle, une page à la demande gère le sans-JS.
- [Liste d'attente TanStack Start](https://ikiform.com/fr/guides/waitlist/tanstack-start.md): Une liste d'attente TanStack Start : une server route répond à un envoi classique comme à fetch, et validateSearch garde la source et le résultat dans l'URL.
- [Liste d'attente React Router](https://ikiform.com/fr/guides/waitlist/react-router.md): Une liste d'attente React Router en pied de chaque page : un fetcher envoie à une seule action, tague chaque inscription avec sa page et fonctionne sans JS.
- [Liste d'attente SvelteKit](https://ikiform.com/fr/guides/waitlist/sveltekit.md): Une liste d'attente SvelteKit 3 : un hook garde le premier ?ref= dans un cookie, une action nommée transmet l'inscription, les visiteurs qui reviennent voient qu'ils sont inscrits.
- [Liste d'attente Nuxt](https://ikiform.com/fr/guides/waitlist/nuxt.md): Une liste d'attente Nuxt sur une landing page pré-rendue : une route Nitro accepte $fetch et les envois classiques, valide avec Zod et redirige sans JavaScript.
- [Formulaire de contact sur un site statique](https://ikiform.com/fr/guides/contact-form-static-site.md): Ajoutez un formulaire de contact fonctionnel à un site statique sans serveur : où va le POST, le spam, les réponses par e-mail, et du code pour chaque hébergeur.
- [Recevoir un formulaire par e-mail](https://ikiform.com/fr/guides/send-form-to-email.md): 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.
- [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.
