# Créer un formulaire de capture de leads qui vous parvient vite

> Un formulaire de capture de leads sur votre propre site, qui vous écrit avec le lead en adresse de réponse, alerte le chat de l'équipe et alimente votre CRM, pour répondre tant que le lead est encore là.

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

Placez sur votre propre page un court formulaire qui envoie le nom, l'e-mail professionnel et une question de qualification à un backend de formulaires. Activez les e-mails de notification pour que chaque lead arrive dans votre boîte avec **Reply** pointant vers lui, et envoyez un webhook signé vers le chat de votre équipe et votre CRM. Mesurez ensuite les minutes entre l'envoi et la première réponse, car ce chiffre fait gagner plus d'affaires que le design du formulaire.

Un formulaire de leads est un formulaire de contact avec une mission : lancer une conversation commerciale. Les gens qui le remplissent vous comparent à quelqu'un d'autre, souvent dans un autre onglet. L'équipe qui répond en premier, avec une réponse utile d'une vraie personne, obtient généralement le rendez-vous. Ce guide construit le formulaire sur votre propre site, vous fait parvenir chaque lead en quelques secondes et laisse le CRM facultatif.

## Le trajet d'un lead

Le formulaire envoie à Ikiform. Ikiform vérifie que l'envoi n'est pas du spam, le stocke, puis fait trois choses à la fois : il écrit à votre équipe avec le lead comme adresse de réponse, envoie un webhook signé et affiche le lead dans le tableau de bord. C'est par le webhook que le lead atteint le chat de l'équipe et votre CRM.

Schéma : Comment un lead va de votre formulaire à la personne qui lui répond

1. Un visiteur remplit le formulaire de leads de votre site, qui envoie à votre endpoint Ikiform.
2. Ikiform vérifie le honeypot, le score de spam et la limite de débit, puis stocke le lead.
3. Quelques secondes plus tard, chaque destinataire des notifications reçoit un e-mail, et appuyer sur Répondre écrit au lead.
4. Le lead apparaît dans le tableau de bord, où vous pouvez le rechercher, le filtrer et l'exporter.
5. Ikiform envoie un webhook submission.created signé.
6. Un relais ou un outil d'automatisation publie le lead dans le chat de l'équipe et crée le contact dans votre CRM.

L'e-mail est le chemin le plus rapide, sans configuration. Le webhook ajoute les endroits que votre équipe surveille déjà.

## Quatre façons de capter des leads

Chaque configuration courante a son point fort. Choisissez selon l'endroit où vous voulez que vivent les leads et qui maintient le formulaire.

| Approche | Points forts | Compromis |
| --- | --- | --- |
| Le formulaire de votre CRM, comme les formulaires HubSpot ou les web forms de Pipedrive | Les leads arrivent dans le CRM comme contacts, sans colle. Suivi et lead scoring vivent dans un seul produit. | Le formulaire est intégré avec le script et le style du CRM. Changer de CRM oblige à refaire chaque formulaire. Certaines fonctionnalités dépendent de l'offre. |
| Formulaire intégré d'un form builder, comme [Typeform](https://ikiform.com/alternatives/typeform) ou [Tally](https://ikiform.com/alternatives/tally) | Des formulaires multi-étapes soignés avec logique, sans code. | Un embed qui ressemble au créateur, sauf si vous payez pour le restyler. Les webhooks et les limites de réponses dépendent de l'offre. |
| Votre propre route API et votre base de données | Toute la logique que vous voulez, comme un lead scoring avant de stocker quoi que ce soit. | Vous construisez le filtrage du spam, les limites de débit, les alertes e-mail, les nouvelles tentatives et l'export, et vous gardez un serveur en marche pour un seul formulaire. |
| Backend de formulaires, comme Ikiform | Votre propre HTML sur n'importe quel site. Filtrage du spam, alertes e-mail avec adresse de réponse, webhooks signés et export CSV, sans serveur. | Pas de lead scoring, pas de fiches CRM et pas d'e-mail au lead. Ceux-là viennent de votre CRM ou de votre outil d'e-mailing, via webhook. |

Si tout votre processus commercial tourne dans un seul CRM et que son formulaire vous convient, utilisez-le. Un backend de formulaires convient quand le formulaire doit ressembler à votre site, quand vous pourriez changer de CRM, ou quand vous voulez que le même formulaire atteigne l'e-mail, le chat et un CRM sans devoir choisir.

## Demander moins, qualifier une fois

Chaque champ ajouté coûte des soumissions. La plupart des formulaires de leads ont besoin de quatre choses : à qui répondre, où la personne travaille, ce qu'elle veut, et de la place pour expliquer. Une question à choix fixe sert à qualifier, car elle arrive sous la forme d'une valeur parmi quelques valeurs connues, sur laquelle votre routage peut compter. Essayez ci-dessous : l'outil exécute le validateur d'Ikiform dans votre navigateur et montre ce qui serait stocké, ou le `422` que vous recevriez.

Formulaire : Talk to sales

- **Nom** (`name`, text, obligatoire)
- **E-mail professionnel** (`email`, email, obligatoire)
- **Entreprise** (`company`, text)
- **Comment pouvons-nous vous aider ?** (`interest`, select, obligatoire)
- **Quelque chose à nous dire ?** (`message`, textarea)

Collez cette définition dans l'onglet **Fields** du formulaire et Ikiform l'applique côté serveur. Le [modèle de formulaire de contact](https://ikiform.com/forms/contact-form) en propose une version plus légère, avec un sujet au lieu d'un intérêt.

| Champ | Pourquoi | À garder en tête |
| --- | --- | --- |
| `name` | Une réponse qui commence par son nom | Gardez l'id `name`. Un lien dans un champ de nom est un fort signal de spam |
| `email` | À qui écrit **Reply** | Gardez l'id `email`. Le libellé **E-mail professionnel** incite à en utiliser un, sans l'imposer |
| `company` | Le rechercher avant de répondre | Facultatif. L'exiger coûte les soumissions des freelances |
| `interest` | Routage et priorité | Un `select` stocke une valeur listée ou refuse l'envoi : les automatisations peuvent donc s'y brancher |
| `message` | Du contexte pour la première réponse | Facultatif. Une zone de rédaction obligatoire est le champ devant lequel les gens abandonnent |

Les numéros de téléphone, la taille de l'équipe et le budget ont leur place dans les formulaires où ils changent ce que vous faites ensuite. Le [modèle de demande de devis](https://ikiform.com/forms/quote-request) est la version complète, avec service, budget et délai en choix fixes. Si un commercial appelle chaque lead, demandez un téléphone. Si vous routez selon la taille de l'équipe, demandez-la sous forme de `select`. Sinon, gardez-les pour la première réponse.

## Le formulaire

Choisissez votre stack. Chaque version affiche le message du serveur sous le champ concerné et garde ce que le visiteur a tapé, car perdre un message à moitié écrit à cause d'une faute de frappe, c'est ainsi que les leads abandonnent. L'état de succès indique qui répond et quand, pour que personne ne se demande si ça a marché.

**HTML**

Le formulaire envoie de façon classique et redirige vers votre page de remerciement : il fonctionne donc avant le chargement de tout script. Chaque champ a un emplacement vide pour son erreur.

`contact-sales.html`:

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

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

  <label for="company">Company</label>
  <input id="company" name="company" autocomplete="organization" />

  <label for="interest">What can we help with?</label>
  <select id="interest" name="interest" required>
    <option value="pricing">Pricing for my team</option>
    <option value="demo">A live demo</option>
    <option value="custom">A custom plan or contract</option>
    <option value="other">Something else</option>
  </select>

  <label for="message">Anything we should know?</label>
  <textarea id="message" name="message" rows="4"></textarea>
  <p class="error" data-error-for="message" hidden></p>

  <input type="hidden" name="page" value="pricing" />
  <input type="hidden" name="_redirect" value="https://example.com/thanks" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Talk to sales</button>
</form>
```

Le script garde le visiteur sur la page. Il affiche l'erreur de chaque champ renvoyée par le serveur sous ce champ, et remplace le formulaire par une ligne qui indique qui répond et quand.

`lead-form.js`:

```js
const form = document.getElementById("lead-form");

function showErrors(fields) {
  form.querySelectorAll("[data-error-for]").forEach((slot) => {
    const message = fields[slot.dataset.errorFor]?.[0];
    slot.textContent = message ?? "";
    slot.hidden = !message;
  });
}

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const button = form.querySelector("button");
  button.disabled = true;
  button.textContent = "Sending";
  try {
    const response = await fetch(form.action, {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new FormData(form),
    });
    const result = await response.json();
    if (result.ok) {
      const thanks = document.createElement("p");
      thanks.setAttribute("role", "status");
      thanks.textContent = `Thanks, ${form.elements.name.value.trim().split(" ")[0]}. A person on our team will reply to ${form.elements.email.value} within one working day.`;
      form.replaceWith(thanks);
      return;
    }
    showErrors(result.error.fields ?? {});
    if (!result.error.fields) alert(result.error.message);
  } catch {
    alert("Something went wrong. Email sales@example.com and we'll pick it up.");
  }
  button.disabled = false;
  button.textContent = "Talk to sales";
});
```

**Next.js**

La server action transmet le lead avec votre clé secrète et renvoie la première erreur de chaque champ, ainsi que les valeurs tapées par le visiteur : rien n'est perdu sur un `422`.

`app/contact-sales/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";

export interface LeadState {
  sent: boolean;
  firstName?: string;
  errors: Record<string, string>;
  values: Record<string, string>;
}

const KEYS = ["name", "email", "company", "interest", "message", "page", "_gotcha"] as const;

export async function sendLead(_previous: LeadState, formData: FormData): Promise<LeadState> {
  const values = Object.fromEntries(KEYS.map((key) => [key, String(formData.get(key) ?? "").trim()]));
  const origin = (await headers()).get("origin");

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify(values),
  });
  const result = await response.json().catch(() => null);
  if (result?.ok) return { sent: true, firstName: values.name.split(" ")[0], errors: {}, values: {} };

  const fields: Record<string, string[]> = result?.error?.fields ?? {};
  const errors = Object.fromEntries(Object.entries(fields).map(([key, messages]) => [key, messages[0] ?? ""]));
  if (!Object.keys(errors).length) errors.form = result?.error?.message ?? "Something went wrong. Email sales@example.com and we'll pick it up.";
  return { sent: false, errors, values };
}
```

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

```tsx
"use client";

import { useActionState } from "react";
import { type LeadState, sendLead } from "./actions";

const initial: LeadState = { sent: false, errors: {}, values: {} };

export function LeadForm() {
  const [state, action, pending] = useActionState(sendLead, initial);
  const { errors, values } = state;

  if (state.sent) {
    return <p role="status">Thanks, {state.firstName}. A person on our team will reply within one working day.</p>;
  }

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

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

      <label htmlFor="company">Company</label>
      <input id="company" name="company" autoComplete="organization" defaultValue={values.company} />

      <label htmlFor="interest">What can we help with?</label>
      <select id="interest" name="interest" defaultValue={values.interest ?? "pricing"}>
        <option value="pricing">Pricing for my team</option>
        <option value="demo">A live demo</option>
        <option value="custom">A custom plan or contract</option>
        <option value="other">Something else</option>
      </select>

      <label htmlFor="message">Anything we should know?</label>
      <textarea id="message" name="message" rows={4} defaultValue={values.message} />
      {errors.message ? <p role="alert">{errors.message}</p> : null}

      <input type="hidden" name="page" value="pricing" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form ? <p role="alert">{errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Talk to sales"}</button>
    </form>
  );
}
```

Pour aller plus loin avec Next.js, lisez [Créer un formulaire de leads qualifiant avec Next.js](https://ikiform.com/fr/guides/lead-capture/nextjs.md).

**Astro**

Zod vérifie les champs avant que quoi que ce soit ne quitte votre serveur, et `isInputError` vous donne ses messages par champ. La page doit être rendue à la demande pour que le formulaire puisse envoyer à l'action.

`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 = {
  sendLead: defineAction({
    accept: "form",
    input: z.object({
      name: z.string().trim().min(1, "Tell us who to reply to."),
      email: z.email("Enter a work email we can reply to."),
      company: z.string().trim().optional(),
      interest: z.enum(["pricing", "demo", "custom", "other"]),
      message: z.string().trim().max(5000).optional(),
      page: z.string().default("pricing"),
      _gotcha: z.string().nullish(),
    }),
    handler: async (input, context) => {
      const origin = context.request.headers.get("origin");
      const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
          Authorization: `Bearer ${getSecret("IKIFORM_SECRET_KEY")}`,
          ...(origin ? { Origin: origin } : {}),
        },
        body: JSON.stringify(input),
      });
      const result = await response.json();
      if (!result.ok) throw new ActionError({ code: "BAD_REQUEST", message: result.error.message });
      return { firstName: input.name.split(" ")[0] };
    },
  }),
};
```

`src/pages/contact-sales.astro`:

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

export const prerender = false;

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

{result?.data ? (
  <p role="status">Thanks, {result.data.firstName}. A person on our team will reply within one working day.</p>
) : (
  <form method="POST" action={actions.sendLead}>
    <label for="name">Name</label>
    <input id="name" name="name" autocomplete="name" required />
    {fields.name && <p role="alert">{fields.name[0]}</p>}

    <label for="email">Work email</label>
    <input id="email" name="email" type="email" autocomplete="email" required />
    {fields.email && <p role="alert">{fields.email[0]}</p>}

    <label for="company">Company</label>
    <input id="company" name="company" autocomplete="organization" />

    <label for="interest">What can we help with?</label>
    <select id="interest" name="interest">
      <option value="pricing">Pricing for my team</option>
      <option value="demo">A live demo</option>
      <option value="custom">A custom plan or contract</option>
      <option value="other">Something else</option>
    </select>

    <label for="message">Anything we should know?</label>
    <textarea id="message" name="message" rows="4"></textarea>

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

Pour aller plus loin avec Astro, lisez [Créer un formulaire de leads qualifiant avec Astro](https://ikiform.com/fr/guides/lead-capture/astro.md).

**TanStack Start**

La server function lit le `FormData` dans `.validator()`, le transmet et renvoie soit le prénom, soit une liste d'erreurs par champ que la route affiche sous chaque input.

`src/routes/contact-sales.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 Result = { ok: true; firstName: string } | { ok: false; errors: Record<string, string> };

const sendLead = createServerFn({ method: "POST" })
  .validator((data: unknown) => {
    if (!(data instanceof FormData)) throw new Error("Expected form data");
    const read = (key: string) => String(data.get(key) ?? "").trim();
    return {
      name: read("name"),
      email: read("email"),
      company: read("company"),
      interest: read("interest"),
      message: read("message"),
      page: read("page"),
      _gotcha: read("_gotcha"),
    };
  })
  .handler(async ({ data }): Promise<Result> => {
    const origin = getRequestHeader("origin");
    const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
        ...(origin ? { Origin: origin } : {}),
      },
      body: JSON.stringify(data),
    });
    const result = await response.json();
    if (result.ok) return { ok: true, firstName: data.name.split(" ")[0] ?? "" };
    const fields: Record<string, string[]> = result.error.fields ?? { form: [result.error.message] };
    return { ok: false, errors: Object.fromEntries(Object.entries(fields).map(([key, list]) => [key, list[0] ?? ""])) };
  });

export const Route = createFileRoute("/contact-sales")({ component: ContactSales });

function ContactSales() {
  const send = useServerFn(sendLead);
  const [result, setResult] = useState<Result | null>(null);
  const [pending, setPending] = useState(false);
  const errors = result && !result.ok ? result.errors : {};

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setPending(true);
    try {
      setResult(await send({ data: new FormData(event.currentTarget) }));
    } catch {
      setResult({ ok: false, errors: { form: "Something went wrong. Email sales@example.com and we'll pick it up." } });
    } finally {
      setPending(false);
    }
  }

  if (result?.ok) {
    return <p role="status">Thanks, {result.firstName}. A person on our team will reply within one working day.</p>;
  }

  return (
    <form method="post" onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      {errors.name ? <p role="alert">{errors.name}</p> : null}
      <label htmlFor="email">Work email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {errors.email ? <p role="alert">{errors.email}</p> : null}
      <label htmlFor="company">Company</label>
      <input id="company" name="company" autoComplete="organization" />
      <label htmlFor="interest">What can we help with?</label>
      <select id="interest" name="interest">
        <option value="pricing">Pricing for my team</option>
        <option value="demo">A live demo</option>
        <option value="custom">A custom plan or contract</option>
        <option value="other">Something else</option>
      </select>
      <label htmlFor="message">Anything we should know?</label>
      <textarea id="message" name="message" rows={4} />
      <input type="hidden" name="page" value="pricing" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form ? <p role="alert">{errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Talk to sales"}</button>
    </form>
  );
}
```

Pour aller plus loin avec TanStack Start, lisez [Créer un formulaire de leads qualifiant avec TanStack Start](https://ikiform.com/fr/guides/lead-capture/tanstack-start.md).

**React Router**

L'action de route transmet l'envoi et renvoie les erreurs de champ avec le code de statut d'Ikiform via `data()`. Le composant les lit dans `actionData`.

`app/routes/contact-sales.tsx`:

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

const KEYS = ["name", "email", "company", "interest", "message", "page", "_gotcha"];

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  const values = Object.fromEntries(KEYS.map((key) => [key, String(formData.get(key) ?? "").trim()]));
  const origin = request.headers.get("origin");

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify(values),
  });
  const result = await response.json();
  if (result.ok) return { sent: true as const, firstName: values.name.split(" ")[0] };
  const fields: Record<string, string[]> = result.error.fields ?? { form: [result.error.message] };
  return data(
    { sent: false as const, errors: Object.fromEntries(Object.entries(fields).map(([key, list]) => [key, list[0]])) },
    { status: response.status },
  );
}

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

  if (actionData?.sent) {
    return <p role="status">Thanks, {actionData.firstName}. A person on our team will reply within one working day.</p>;
  }
  const errors: Record<string, string | undefined> = actionData?.errors ?? {};

  return (
    <Form method="post">
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      {errors.name ? <p role="alert">{errors.name}</p> : null}
      <label htmlFor="email">Work email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {errors.email ? <p role="alert">{errors.email}</p> : null}
      <label htmlFor="company">Company</label>
      <input id="company" name="company" autoComplete="organization" />
      <label htmlFor="interest">What can we help with?</label>
      <select id="interest" name="interest">
        <option value="pricing">Pricing for my team</option>
        <option value="demo">A live demo</option>
        <option value="custom">A custom plan or contract</option>
        <option value="other">Something else</option>
      </select>
      <label htmlFor="message">Anything we should know?</label>
      <textarea id="message" name="message" rows={4} />
      <input type="hidden" name="page" value="pricing" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form ? <p role="alert">{errors.form}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Talk to sales"}</button>
    </Form>
  );
}
```

Pour aller plus loin avec React Router, lisez [Créer un formulaire de leads qualifiant avec React Router](https://ikiform.com/fr/guides/lead-capture/react-router.md).

**SvelteKit**

La form action renvoie `fail()` avec les valeurs et les erreurs, et `update({ reset: false })` garde à l'écran ce que le visiteur a tapé pendant qu'il corrige un champ. Déclarez `IKIFORM_SECRET_KEY` dans `src/env.ts` avec `defineEnvVars` pour que `$app/env/private` puisse la lire.

`src/routes/contact-sales/+page.server.ts`:

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

const KEYS = ["name", "email", "company", "interest", "message", "page", "_gotcha"];

export const actions = {
  default: async ({ request, fetch, url }) => {
    const data = await request.formData();
    const values = Object.fromEntries(KEYS.map((key) => [key, String(data.get(key) ?? "").trim()]));

    const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${IKIFORM_SECRET_KEY}`,
        Origin: request.headers.get("origin") ?? url.origin,
      },
      body: JSON.stringify(values),
    });
    const result = await response.json();
    if (!result.ok) {
      const fields: Record<string, string[]> = result.error.fields ?? { form: [result.error.message] };
      const errors = Object.fromEntries(Object.entries(fields).map(([key, list]) => [key, list[0]]));
      return fail(response.status, { values, errors });
    }
    return { firstName: values.name.split(" ")[0] };
  },
} satisfies Actions;
```

`src/routes/contact-sales/+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 errors = $derived<Record<string, string | undefined>>(form?.errors ?? {});
</script>

{#if form?.firstName}
  <p role="status">Thanks, {form.firstName}. A person on our team will reply within one working day.</p>
{:else}
  <form
    method="POST"
    use:enhance={() => {
      pending = true;
      return async ({ update }) => {
        await update({ reset: false });
        pending = false;
      };
    }}
  >
    <label for="name">Name</label>
    <input id="name" name="name" autocomplete="name" required value={form?.values?.name ?? ""} />
    {#if errors.name}<p role="alert">{errors.name}</p>{/if}

    <label for="email">Work email</label>
    <input id="email" name="email" type="email" autocomplete="email" required value={form?.values?.email ?? ""} />
    {#if errors.email}<p role="alert">{errors.email}</p>{/if}

    <label for="company">Company</label>
    <input id="company" name="company" autocomplete="organization" value={form?.values?.company ?? ""} />

    <label for="interest">What can we help with?</label>
    <select id="interest" name="interest" value={form?.values?.interest ?? "pricing"}>
      <option value="pricing">Pricing for my team</option>
      <option value="demo">A live demo</option>
      <option value="custom">A custom plan or contract</option>
      <option value="other">Something else</option>
    </select>

    <label for="message">Anything we should know?</label>
    <textarea id="message" name="message" rows="4">{form?.values?.message ?? ""}</textarea>

    <input type="hidden" name="page" value="pricing" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    {#if errors.form}<p role="alert">{errors.form}</p>{/if}
    <button disabled={pending}>{pending ? "Sending" : "Talk to sales"}</button>
  </form>
{/if}
```

Pour aller plus loin avec SvelteKit, lisez [Créer un formulaire de leads qualifiant avec SvelteKit](https://ikiform.com/fr/guides/lead-capture/sveltekit.md).

**Nuxt**

Une route serveur garde la clé hors du client et renvoie les erreurs de champ d'Ikiform dans le `data` de l'erreur. Définissez `NUXT_IKIFORM_SECRET_KEY` dans l'environnement.

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

```ts
export default defineEventHandler(async (event) => {
  const body = await readBody<Record<string, string | undefined>>(event);
  const config = useRuntimeConfig(event);
  const origin = getRequestHeader(event, "origin");
  const keys = ["name", "email", "company", "interest", "message", "page", "_gotcha"];
  const values = Object.fromEntries(keys.map((key) => [key, String(body[key] ?? "").trim()]));

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

`nuxt.config.ts`:

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

`app/components/LeadForm.vue`:

```vue
<script setup lang="ts">
const lead = reactive({ name: "", email: "", company: "", interest: "pricing", message: "", _gotcha: "" });
const errors = ref<Record<string, string[]>>({});
const failure = ref("");
const sending = ref(false);
const firstName = ref("");

async function send() {
  sending.value = true;
  errors.value = {};
  failure.value = "";
  try {
    const result = await $fetch("/api/lead", { method: "POST", body: { ...lead, page: "pricing" } });
    firstName.value = result.firstName ?? "there";
  } catch (error) {
    const data = (error as { data?: { message?: string; data?: { fields?: Record<string, string[]> } } }).data;
    errors.value = data?.data?.fields ?? {};
    if (!Object.keys(errors.value).length) failure.value = data?.message ?? "Something went wrong. Email sales@example.com and we'll pick it up.";
  } finally {
    sending.value = false;
  }
}
</script>

<template>
  <p v-if="firstName" role="status">Thanks, {{ firstName }}. A person on our team will reply within one working day.</p>
  <form v-else @submit.prevent="send">
    <label for="name">Name</label>
    <input id="name" v-model="lead.name" name="name" autocomplete="name" required />
    <p v-if="errors.name" role="alert">{{ errors.name[0] }}</p>
    <label for="email">Work email</label>
    <input id="email" v-model="lead.email" name="email" type="email" autocomplete="email" required />
    <p v-if="errors.email" role="alert">{{ errors.email[0] }}</p>
    <label for="company">Company</label>
    <input id="company" v-model="lead.company" name="company" autocomplete="organization" />
    <label for="interest">What can we help with?</label>
    <select id="interest" v-model="lead.interest" name="interest">
      <option value="pricing">Pricing for my team</option>
      <option value="demo">A live demo</option>
      <option value="custom">A custom plan or contract</option>
      <option value="other">Something else</option>
    </select>
    <label for="message">Anything we should know?</label>
    <textarea id="message" v-model="lead.message" name="message" rows="4" />
    <input v-model="lead._gotcha" name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="failure" role="alert">{{ failure }}</p>
    <button :disabled="sending">{{ sending ? "Sending" : "Talk to sales" }}</button>
  </form>
</template>
```

Pour aller plus loin avec Nuxt, lisez [Créer un formulaire de leads qualifiant avec Nuxt](https://ikiform.com/fr/guides/lead-capture/nuxt.md).

**React**

Sans serveur à vous, le formulaire envoie directement à Ikiform depuis le navigateur. Gardez les clés secrètes hors de ce code ; c'est **Allowed domains** (domaines autorisés) qui protège le formulaire.

`src/LeadForm.tsx`:

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

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

export function LeadForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "sent">("idle");
  const [errors, setErrors] = useState<Record<string, string[]>>({});
  const [firstName, setFirstName] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const body = new FormData(event.currentTarget);
    setStatus("sending");
    setErrors({});
    try {
      const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
      const result = await response.json();
      if (result.ok) {
        setFirstName(String(body.get("name")).trim().split(" ")[0] ?? "");
        setStatus("sent");
        return;
      }
      setErrors(result.error.fields ?? { form: [result.error.message] });
    } catch {
      setErrors({ form: ["Something went wrong. Email sales@example.com and we'll pick it up."] });
    }
    setStatus("idle");
  }

  if (status === "sent") {
    return <p role="status">Thanks, {firstName}. A person on our team will reply within one working day.</p>;
  }

  return (
    <form action={ENDPOINT} method="post" onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      {errors.name ? <p role="alert">{errors.name[0]}</p> : null}
      <label htmlFor="email">Work email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      {errors.email ? <p role="alert">{errors.email[0]}</p> : null}
      <label htmlFor="company">Company</label>
      <input id="company" name="company" autoComplete="organization" />
      <label htmlFor="interest">What can we help with?</label>
      <select id="interest" name="interest">
        <option value="pricing">Pricing for my team</option>
        <option value="demo">A live demo</option>
        <option value="custom">A custom plan or contract</option>
        <option value="other">Something else</option>
      </select>
      <label htmlFor="message">Anything we should know?</label>
      <textarea id="message" name="message" rows={4} />
      <input type="hidden" name="page" value="pricing" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {errors.form ? <p role="alert">{errors.form[0]}</p> : null}
      <button disabled={status === "sending"}>{status === "sending" ? "Sending" : "Talk to sales"}</button>
    </form>
  );
}
```

**Vue**

Le même envoi côté navigateur, en composant Vue. Le formulaire garde son `action` : un visiteur dont le script ne se charge pas peut quand même vous joindre.

`src/components/LeadForm.vue`:

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

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

const status = ref<"idle" | "sending" | "sent">("idle");
const errors = ref<Record<string, string[]>>({});
const firstName = ref("");

async function send(event: SubmitEvent) {
  const body = new FormData(event.target as HTMLFormElement);
  status.value = "sending";
  errors.value = {};
  try {
    const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body });
    const result = await response.json();
    if (result.ok) {
      firstName.value = String(body.get("name")).trim().split(" ")[0] ?? "";
      status.value = "sent";
      return;
    }
    errors.value = result.error.fields ?? { form: [result.error.message] };
  } catch {
    errors.value = { form: ["Something went wrong. Email sales@example.com and we'll pick it up."] };
  }
  status.value = "idle";
}
</script>

<template>
  <p v-if="status === 'sent'" role="status">Thanks, {{ firstName }}. A person on our team will reply within one working day.</p>
  <form v-else :action="ENDPOINT" method="post" @submit.prevent="send">
    <label for="name">Name</label>
    <input id="name" name="name" autocomplete="name" required />
    <p v-if="errors.name" role="alert">{{ errors.name[0] }}</p>
    <label for="email">Work email</label>
    <input id="email" name="email" type="email" autocomplete="email" required />
    <p v-if="errors.email" role="alert">{{ errors.email[0] }}</p>
    <label for="company">Company</label>
    <input id="company" name="company" autocomplete="organization" />
    <label for="interest">What can we help with?</label>
    <select id="interest" name="interest">
      <option value="pricing">Pricing for my team</option>
      <option value="demo">A live demo</option>
      <option value="custom">A custom plan or contract</option>
      <option value="other">Something else</option>
    </select>
    <label for="message">Anything we should know?</label>
    <textarea id="message" name="message" rows="4" />
    <input type="hidden" name="page" value="pricing" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="errors.form" role="alert">{{ errors.form[0] }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Sending" : "Talk to sales" }}</button>
  </form>
</template>
```

- Remplacez `frm_xxxxxxxxxxxxxxxx` par l'id de votre formulaire, visible dans le tableau de bord.
- 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.
- L'input masqué `page` enregistre la page d'où vient le lead : tarifs, page de fonctionnalité, landing page de campagne. Si vous définissez des champs, ajoutez `page` comme champ texte facultatif, sinon l'envoi est refusé avec `Unknown field`.
- Les versions serveur transmettent l'en-tête `Origin` du visiteur, car un fetch côté serveur n'en envoie aucun, et c'est grâce à lui qu'Ikiform détecte les tests gratuits depuis `localhost`.
- Le formulaire HTML simple a besoin de **Redirect after submit**, ou d'un input `_redirect` vers l'un de vos **Allowed domains**, pour arriver sur votre propre page de remerciement.

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

La documentation détaille la configuration de chaque framework : [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), [envoi depuis un serveur](https://ikiform.com/fr/docs/guides/server.md) et [HTML simple](https://ikiform.com/fr/docs/html.md).

## Joindre le lead en quelques minutes

Le formulaire, c'est la partie facile. Ce qui distingue un formulaire de leads qui vend d'un formulaire qui collecte, c'est le temps entre l'envoi et la réponse d'une personne. Voici à quoi ressemble ce trajet quand tout fonctionne :

Schéma : Les minutes entre l'envoi d'un lead et votre première réponse

1. Le lead envoie le formulaire.
2. Ikiform le stocke et répond au navigateur en bien moins d'une seconde.
3. L'e-mail de notification et le webhook partent d'une file d'attente quelques secondes plus tard.
4. Quelqu'un de l'équipe appuie sur Répondre dans sa boîte, ou sur le lien mailto dans le chat, et la réponse part au lead.

Tout jusqu'à l'e-mail prend quelques secondes. La dernière flèche est celle que vous maîtrisez, et celle qui fait gagner les affaires.

### Répondre depuis votre boîte

Chaque e-mail de notification utilise le lead comme adresse de réponse. Ikiform la prend d'abord dans un champ `_replyto`, puis dans un champ nommé `email`, `email_address`, `your_email`, `reply_to`, `contact_email`. Gardez l'id de champ `email` et **Reply** écrit au lead, pas à Ikiform. L'objet est `New submission:` suivi du nom du formulaire : nommez donc le formulaire d'après ce qu'il est, comme **Parler à un commercial**, et filtrez sur cet objet pour étiqueter ou marquer les leads.

Ajoutez jusqu'à 10 destinataires par formulaire dans ses réglages de notification : des membres de l'espace de travail, ou des adresses externes qui confirment d'abord par e-mail. Envoyez-le aux personnes qui répondent, pas à un alias partagé dont personne n'est responsable : un lead dans cinq boîtes que personne ne prend en charge attend plus longtemps qu'un lead dans une seule.

> Les e-mails de notification sont plafonnés à 50 par formulaire et par heure, ou 10 tant que l'espace de travail a moins de 7 jours, puis un seul récapitulatif pour le reste de l'heure. Un formulaire de leads normal n'en approche jamais. Si vous lancez une campagne qui pourrait le faire, routez aussi les leads vers le chat par webhook.

### Alerter l'équipe là où elle se trouve

L'e-mail fonctionne quand quelqu'un est responsable de la boîte. Une équipe qui vit dans le chat répond plus vite quand le lead y apparaît. Ajoutez un webhook vers Slack, Teams ou Discord en suivant [Slack](https://ikiform.com/blog/form-submissions-to-slack.md), [Microsoft Teams](https://ikiform.com/blog/form-submissions-to-microsoft-teams.md) ou [Discord](https://ikiform.com/blog/form-submissions-to-discord.md). Mettez un lien `mailto:` dans le message pour que la personne qui le prend en charge puisse répondre en un clic.

### Router selon la demande

Comme `interest` n'accepte que des valeurs listées, un petit relais peut s'y brancher en toute confiance. Cette route envoie les demandes de démo et les questions de contrat dans leurs propres canaux, et tout le reste dans le canal commercial principal :

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

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

const CHANNELS: Record<string, string | undefined> = {
  demo: process.env.SLACK_DEMO_WEBHOOK,
  custom: process.env.SLACK_ENTERPRISE_WEBHOOK,
};

export async function POST(request: Request) {
  const payload = await request.text();
  let event;
  try {
    event = await verifyWebhook({ payload, headers: request.headers, secret: process.env.IKIFORM_WEBHOOK_SECRET! });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
  if (event.type !== "submission.created") return new Response(null, { status: 204 });

  const lead = event.data.fields;
  const interest = String(lead.interest ?? "other");
  const target = CHANNELS[interest] ?? process.env.SLACK_SALES_WEBHOOK!;
  const text = [
    `*${lead.name}* from ${lead.company || "an unknown company"} wants: ${interest}`,
    `Reply: mailto:${lead.email}`,
    lead.message ? `> ${String(lead.message).slice(0, 300)}` : "",
  ]
    .filter(Boolean)
    .join("\n");

  const response = await fetch(target, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ text }),
  });
  if (!response.ok) return new Response(`Slack answered ${response.status}`, { status: 502 });
  return new Response(null, { status: 204 });
}
```

- `verifyWebhook` vérifie la signature et l'horodatage, pour que personne d'autre ne puisse publier de faux leads dans vos canaux.
- Un `502` quand Slack est en panne fait réessayer Ikiform pour la même livraison, avec le même `webhook-id`, pendant environ 3 jours.
- Changez librement le libellé d'une option. Gardez sa valeur stable dès qu'un relais ou une automatisation en dépend.

## Garder les déchets hors du pipeline

Les formulaires de leads attirent un type de spam particulier : des gens qui vous vendent du SEO, du netlinking et du développement externalisé. Il gaspille votre réponse la plus rapide : filtrez-le avant qu'il n'atteigne qui que ce soit.

| Défense | Ce qu'elle arrête | Configuration |
| --- | --- | --- |
| Honeypot | Les bots qui remplissent tous les inputs | L'input masqué `_gotcha` |
| Score de spam | Phrases de démarchage connues comme `seo services` (+2), liens dans le nom (+3), trois liens ou plus (+2), répétitions (+3). À 5, c'est du spam | Aucun |
| Limite de débit | Les scripts qui envoient en boucle | Réglez le formulaire sur Strict, 2 toutes les 10 minutes par visiteur. Un vrai lead envoie rarement deux fois en dix minutes |
| Domaines autorisés | Les envois depuis d'autres sites | Ajoutez votre domaine sous **Allowed domains** |
| Vérification humaine | Bots dans un vrai navigateur | Réglez **Spam check** sur **Ikiform**. Les versions `fetch` ont alors besoin d'un token de `challenge()` |

Le spam reçoit la même réponse de succès qu'un vrai lead, ne déclenche ni e-mail ni webhook et ne compte pas dans l'utilisation. Cela veut aussi dire qu'un lead mal jugé ne fait aucun bruit. Vérifiez le filtre **Spam** une fois par semaine ; marquer un envoi comme non-spam déclenche alors son e-mail et ses webhooks. La [documentation anti-spam](https://ikiform.com/fr/docs/spam.md#scoring) liste chaque signal.

## Savoir d'où viennent les leads

Chaque soumission enregistre déjà l'origine et le referrer de la page, qui apparaissent dans le webhook sous `meta.origin` et `meta.referrer`. Les tags de campagne demandent un peu d'aide, car le visiteur arrive souvent sur une page et envoie depuis une autre. Ce snippet copie les paramètres `utm_` dans des inputs masqués et les mémorise pour la session :

`attribution.html`:

```html
<input type="hidden" name="utm_source" />
<input type="hidden" name="utm_medium" />
<input type="hidden" name="utm_campaign" />

<script>
  const params = new URLSearchParams(location.search);
  for (const key of ["utm_source", "utm_medium", "utm_campaign"]) {
    let value = params.get(key);
    try {
      if (value) sessionStorage.setItem(key, value);
      else value = sessionStorage.getItem(key);
    } catch {}
    const input = document.querySelector(`#lead-form [name="${key}"]`);
    if (input && value) input.value = value.slice(0, 200);
  }
</script>
```

Dans un framework, lisez les mêmes paramètres dans l'URL côté serveur et affichez-les en inputs masqués. Si des champs sont définis, ajoutez chaque id `utm_` comme champ texte facultatif. Les noms qui commencent par `_` ne sont jamais stockés : ne les utilisez pas pour des données à conserver.

## Ce que le lead voit ensuite

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). Pour un formulaire de leads, c'est souvent très bien : une réponse personnelle rapide vaut mieux qu'une réponse automatique. Si vous voulez quand même un accusé de réception immédiat, envoyez-le depuis votre CRM ou votre outil d'e-mailing quand le webhook crée le contact, pour que réponses et désinscriptions soient suivies au même endroit.

Quoi que vous envoyiez, le message de succès sur la page est la promesse. Dites qui répond et quand, et tenez-le. Un formulaire qui annonce un jour ouvré et répond en dix minutes gagne la confiance ; un formulaire qui annonce dix minutes et répond le lendemain la perd.

## Les leads dans votre CRM

Il n'y a pas d'intégrations en un clic. Chaque outil est atteint par un [webhook](https://ikiform.com/fr/docs/webhooks.md) signé, directement ou via un outil d'automatisation comme [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) ou [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md). Pour un CRM, cela veut dire qu'un relais convertit le corps fixe du webhook d'Ikiform en appel de création de contact du CRM. [Les leads vers HubSpot ou Pipedrive](https://ikiform.com/fr/guides/lead-capture/leads-to-crm.md) détaille les deux, doublons et nouvelles tentatives compris. Tant que vous n'avez pas besoin de CRM, le tableau de bord et un [export CSV](https://ikiform.com/fr/docs/guides/export.md) hebdomadaire font un très bon pipeline.

## Choisissez votre cas

La configuration ci-dessus fonctionne pour tout lead. Ces pages couvrent ce qui change pour un type de lead particulier, et pour faire entrer les leads dans un CRM.

- [Formulaire de demande de démo](https://ikiform.com/fr/guides/lead-capture/demo-request.md): Un formulaire de réservation de démo qui stocke le lead, puis envoie le visiteur vers Cal.com ou Calendly avec son nom et son e-mail déjà remplis.
- [Formulaire de demande de devis](https://ikiform.com/fr/guides/lead-capture/quote-request.md): Un formulaire de demande de devis qui reçoit les briefs sous forme de liens partagés plutôt qu'en pièces jointes, fixe une échéance à chaque demande et garde les leads riches en liens hors du spam.
- [Formulaire de demande de consultation](https://ikiform.com/fr/guides/lead-capture/consultation-request.md): Un formulaire de demande de consultation pour comptables, avocats, coachs et cabinets, qui demande comment et quand appeler, et garde les détails sensibles pour l'appel.
- [Formulaire de demande de projet](https://ikiform.com/fr/guides/lead-capture/project-inquiry.md): Un formulaire de démarrage de projet pour studios et freelances, qui trie les demandes en chaudes, tièdes et hors cible selon le budget et le calendrier, avant que quiconque ne lise le brief.
- [Leads vers HubSpot ou Pipedrive](https://ikiform.com/fr/guides/lead-capture/leads-to-crm.md): Convertissez chaque webhook Ikiform signé en upsert de contact HubSpot ou en appels person et lead de Pipedrive, sans contacts en double quand une livraison est réessayée.
- [Formulaire de leads Next.js](https://ikiform.com/fr/guides/lead-capture/nextjs.md): Un formulaire de leads Next.js App Router qui valide avec Zod dans une server action, garde les UTM du premier contact dans un cookie et envoie les leads qualifiés vers votre agenda.
- [Formulaire de leads Astro](https://ikiform.com/fr/guides/lead-capture/astro.md): Un formulaire de leads Astro construit sur les Actions : validation Zod, UTM du premier contact sur un site statique, une page de contact à la demande et une redirection vers votre outil de réservation.
- [Formulaire de leads TanStack Start](https://ikiform.com/fr/guides/lead-capture/tanstack-start.md): Un formulaire de leads TanStack Start : un middleware de requête global pour les UTM du premier contact, une server function avec Zod et une redirection levée vers votre outil de réservation.
- [Formulaire de leads React Router](https://ikiform.com/fr/guides/lead-capture/react-router.md): Un formulaire de leads React Router en mode framework : un middleware racine avec un cookie de premier contact signé, une action de route qui valide et une redirection vers votre outil de réservation.
- [Formulaire de leads SvelteKit](https://ikiform.com/fr/guides/lead-capture/sveltekit.md): Un formulaire de leads SvelteKit : le hook handle enregistre les UTM du premier contact, une form action valide avec Valibot et redirect() envoie les leads qualifiés vers un agenda.
- [Formulaire de leads Nuxt](https://ikiform.com/fr/guides/lead-capture/nuxt.md): Un formulaire de leads Nuxt : un middleware serveur enregistre les UTM du premier contact, une route serveur validée par Zod sert fetch et les envois classiques, et les bons leads réservent un appel.

## Vérifiez que tout fonctionne

Faites ces vérifications avant d'envoyer du trafic vers 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 chaque destinataire. L'objet est `New submission:` suivi du nom du formulaire, et **Reply** ouvre un message vers l'adresse que vous avez saisie.
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. Chronométrez : envoyez depuis votre téléphone et notez le temps avant l'arrivée de l'e-mail et du message dans le chat.

## Dépannage

| Problème | Que faire |
| --- | --- |
| **Reply** écrit à l'adresse d'Ikiform, pas au lead | Le formulaire n'a aucun champ qu'Ikiform reconnaît comme l'e-mail du lead. Renommez le champ en `email`, ou envoyez un champ `_replyto` avec l'adresse. Noms acceptés : `email`, `email_address`, `your_email`, `reply_to`, `contact_email`. |
| `422` avec `Unknown field` pour `page` ou `utm_source` | Le formulaire a une définition de champs et l'input masqué n'y figure pas. Ajoutez-le comme champ texte facultatif, ou supprimez l'input. |
| `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. |
| `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. |
| Un destinataire ne reçoit aucun e-mail de notification | Une adresse extérieure à l'espace de travail doit d'abord confirmer, depuis l'e-mail qu'Ikiform lui envoie. Vérifiez qu'elle a été confirmée, puis vérifiez le dossier spam du destinataire. |

## Questions

### Qu'est-ce qu'un formulaire de capture de leads ?

Un formulaire sur votre site qui recueille les coordonnées d'un client potentiel et ce qu'il veut, pour que votre équipe puisse le relancer. Il se distingue d'un formulaire de contact général parce qu'il qualifie le lead, généralement avec une question à choix fixe, et qu'il est branché pour atteindre rapidement l'équipe commerciale.

### Comment recevoir les soumissions d'un formulaire de leads par e-mail ?

Faites pointer le formulaire vers votre endpoint Ikiform et laissez les e-mails de notification activés pour ce formulaire. Chaque lead arrive en quelques secondes, à 10 destinataires au plus, et appuyer sur Répondre écrit au lead, à condition que le champ e-mail s'appelle email ou que le formulaire envoie _replyto.

### Combien de champs doit avoir un formulaire de capture de leads ?

Le minimum qui vous permet de bien répondre : généralement le nom, l'e-mail, une question de qualification et un message facultatif. N'ajoutez téléphone, budget ou taille d'équipe que si la réponse change ce que fait votre équipe ensuite.

### Ikiform peut-il envoyer les leads vers HubSpot, Pipedrive ou Salesforce ?

Via un webhook, oui. Il n'y a pas d'intégrations en un clic : un petit relais ou un outil d'automatisation comme Zapier, Make ou n8n convertit chaque webhook signé en appel à l'API du CRM et crée le contact ou le lead.

### Le lead reçoit-il un e-mail de confirmation ?

Pas de la part d'Ikiform. Ikiform écrit à votre équipe, pas à la personne qui a rempli le formulaire. Envoyez tout accusé de réception depuis votre CRM ou votre outil d'e-mailing, déclenché par le webhook, ou répondez personnellement depuis l'e-mail de notification.

### Puis-je bloquer les adresses e-mail gratuites comme Gmail ?

Pas avec un réglage intégré. L'option allowed domains du champ e-mail est une liste d'autorisation, adaptée aux formulaires internes, pas aux formulaires publics. Acceptez toutes les adresses et triez plutôt les leads en messagerie gratuite dans votre relais ou votre CRM ; beaucoup de vrais acheteurs utilisent une adresse personnelle.

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

- [Formulaire de demande de démo](https://ikiform.com/fr/guides/lead-capture/demo-request.md): Un formulaire de réservation de démo qui stocke le lead, puis envoie le visiteur vers Cal.com ou Calendly avec son nom et son e-mail déjà remplis.
- [Formulaire de demande de devis](https://ikiform.com/fr/guides/lead-capture/quote-request.md): Un formulaire de demande de devis qui reçoit les briefs sous forme de liens partagés plutôt qu'en pièces jointes, fixe une échéance à chaque demande et garde les leads riches en liens hors du spam.
- [Formulaire de demande de consultation](https://ikiform.com/fr/guides/lead-capture/consultation-request.md): Un formulaire de demande de consultation pour comptables, avocats, coachs et cabinets, qui demande comment et quand appeler, et garde les détails sensibles pour l'appel.
- [Formulaire de demande de projet](https://ikiform.com/fr/guides/lead-capture/project-inquiry.md): Un formulaire de démarrage de projet pour studios et freelances, qui trie les demandes en chaudes, tièdes et hors cible selon le budget et le calendrier, avant que quiconque ne lise le brief.
- [Leads vers HubSpot ou Pipedrive](https://ikiform.com/fr/guides/lead-capture/leads-to-crm.md): Convertissez chaque webhook Ikiform signé en upsert de contact HubSpot ou en appels person et lead de Pipedrive, sans contacts en double quand une livraison est réessayée.
- [Formulaire de leads Next.js](https://ikiform.com/fr/guides/lead-capture/nextjs.md): Un formulaire de leads Next.js App Router qui valide avec Zod dans une server action, garde les UTM du premier contact dans un cookie et envoie les leads qualifiés vers votre agenda.
- [Formulaire de leads Astro](https://ikiform.com/fr/guides/lead-capture/astro.md): Un formulaire de leads Astro construit sur les Actions : validation Zod, UTM du premier contact sur un site statique, une page de contact à la demande et une redirection vers votre outil de réservation.
- [Formulaire de leads TanStack Start](https://ikiform.com/fr/guides/lead-capture/tanstack-start.md): Un formulaire de leads TanStack Start : un middleware de requête global pour les UTM du premier contact, une server function avec Zod et une redirection levée vers votre outil de réservation.
- [Formulaire de leads React Router](https://ikiform.com/fr/guides/lead-capture/react-router.md): Un formulaire de leads React Router en mode framework : un middleware racine avec un cookie de premier contact signé, une action de route qui valide et une redirection vers votre outil de réservation.
- [Formulaire de leads SvelteKit](https://ikiform.com/fr/guides/lead-capture/sveltekit.md): Un formulaire de leads SvelteKit : le hook handle enregistre les UTM du premier contact, une form action valide avec Valibot et redirect() envoie les leads qualifiés vers un agenda.
- [Formulaire de leads Nuxt](https://ikiform.com/fr/guides/lead-capture/nuxt.md): Un formulaire de leads Nuxt : un middleware serveur enregistre les UTM du premier contact, une route serveur validée par Zod sert fetch et les envois classiques, et les bons leads réservent un appel.
- [Formulaire d'inscription à la newsletter](https://ikiform.com/fr/guides/newsletter-signup.md): Créez un formulaire d'inscription à la newsletter avec votre propre markup, enregistrez le consentement, bloquez les inscriptions de bots et transmettez chaque abonné à Kit, MailerLite, beehiiv ou n'importe quel outil.
- [Envoi de formulaire HTML](https://ikiform.com/fr/guides/html-form-submission.md): Ce que fait un navigateur quand un formulaire est envoyé : quels champs il transmet, comment il les encode, ce que le serveur doit répondre, et comment Ikiform répond.
- [Inscription à un événement](https://ikiform.com/fr/guides/event-registration.md): Recevez les RSVP sur votre page d'événement : formulaire, limite d'invités, décompte fiable, clôture quand c'est complet et liste d'accueil, sans billetterie.
