# Créer un formulaire de demande de démo qui fait réserver l'appel

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

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

Envoyez d'abord la demande de démo à un backend de formulaires, pour que le lead soit stocké et votre équipe prévenue même si le visiteur ne choisit jamais de créneau. Envoyez ensuite le navigateur vers votre page de réservation avec `name` et `email` dans l'URL, que Calendly et Cal.com utilisent pour pré-remplir leur formulaire. Dirigez les petites équipes ailleurs, par exemple vers une visite enregistrée, pour que votre agenda contienne des appels qui peuvent se conclure.

## Ce qui change avec une demande de démo

Quelqu'un qui demande une démo veut un créneau dans un agenda, pas un échange d'e-mails. Chaque aller-retour du type « mardi, ça vous va ? » est une occasion de réserver chez un concurrent. La mission du formulaire est donc courte : recueillir de quoi qualifier, puis mettre un agenda devant la personne tant que l'onglet est encore ouvert.

Trois contraintes guident la construction :

- **Le lead doit être stocké avant l'ouverture de l'outil de réservation.** Beaucoup de visiteurs ouvrent la page de réservation et la ferment. Si la seule trace d'eux est dans l'outil de réservation, ces leads sont perdus. Envoyez d'abord à Ikiform et ne redirigez qu'en cas de succès.
- **`_redirect` ne peut pas transporter ce qu'ils ont tapé.** C'est une URL fixe, qu'Ikiform ne suit que pour une URL `https:` sur l'un de vos **Allowed domains**. Y ajouter `cal.com` ou `calendly.com` laisserait aussi passer les envois depuis ces sites. Construisez plutôt le lien de réservation dans votre propre code.
- **Toutes les demandes ne méritent pas un appel en direct.** Un fondateur solo qui demande une démo d'un produit d'équipe sera mieux servi par une visite guidée et un lien d'inscription. Une question à choix fixe décide où va ensuite le navigateur.

Schéma : Où va une demande de démo une fois stockée

1. Le visiteur remplit le nom, l'e-mail professionnel, l'entreprise et la taille de l'équipe, et le formulaire envoie à Ikiform.
2. Ikiform stocke la demande et écrit à votre équipe. Rien d'autre ne se passe tant qu'il n'a pas répondu ok.
3. Votre code lit la taille d'équipe choisie par le visiteur.
4. Les équipes de 11 personnes ou plus vont vers la page de réservation, avec le nom et l'e-mail dans l'URL.
5. Les équipes de 1 à 10 personnes vont vers une visite enregistrée avec un lien d'inscription.

L'aiguillage se fait dans votre code, une fois qu'Ikiform a le lead. L'équipe reçoit chaque demande dans tous les cas : une petite équipe qui aurait mérité un appel reste donc visible.

## Quatre champs, dont un pour le routage

| Champ | Type | Pourquoi |
| --- | --- | --- |
| `name` | text | Pré-remplit le formulaire de réservation, pour que le visiteur ne le tape qu'une fois |
| `email` | e-mail | Pré-remplit le formulaire de réservation et devient l'adresse de réponse de l'e-mail de notification |
| `company` | text, obligatoire ici | Votre commercial le consulte avant l'appel |
| `team_size` | select | Décide entre page de réservation et visite, et arrive sous la forme d'une valeur parmi quatre connues |

Gardez le cas d'usage, le calendrier et le numéro de téléphone pour l'appel lui-même. L'outil de réservation peut poser une question à lui si vous avez besoin de contexte avant le rendez-vous, et Calendly comme Cal.com peuvent aussi pré-remplir des questions personnalisées.

## Envoyer, puis passer la main

Chaque version envoie la demande, attend la réponse d'Ikiform et seulement ensuite construit l'URL suivante. Les exemples utilisent Cal.com ; pour Calendly, remplacez par le lien de votre événement. Calendly documente `name` et `email` comme paramètres de pré-remplissage, et Cal.com pré-remplit tout champ de réservation par son identifiant, dont `name` et `email`.

**HTML**

Le formulaire et son script ensemble. Sans JavaScript, le formulaire envoie quand même et arrive sur votre page **Redirect after submit** : placez-y donc le lien de réservation comme solution de repli.

`demo.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST" id="demo-form">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required />

  <label for="email">Work email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />

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

  <label for="team_size">Team size</label>
  <select id="team_size" name="team_size" required>
    <option value="1_10">1 to 10</option>
    <option value="11_50">11 to 50</option>
    <option value="51_200">51 to 200</option>
    <option value="201_plus">201 or more</option>
  </select>

  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <p role="alert" id="demo-error" hidden></p>
  <button>Pick a time</button>
</form>

<script>
  const BOOKING = "https://cal.com/acme/demo";
  const SELF_SERVE = "/tour";
  const form = document.getElementById("demo-form");
  const error = document.getElementById("demo-error");

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const data = new FormData(form);
    form.querySelector("button").disabled = true;
    error.hidden = true;
    try {
      const response = await fetch(form.action, {
        method: "POST",
        headers: { Accept: "application/json" },
        body: data,
      });
      const result = await response.json();
      if (!result.ok) throw new Error(Object.values(result.error.fields ?? {})[0]?.[0] ?? result.error.message);
      const small = data.get("team_size") === "1_10";
      const next = new URL(small ? SELF_SERVE : BOOKING, location.href);
      next.searchParams.set("name", String(data.get("name")));
      next.searchParams.set("email", String(data.get("email")));
      location.assign(next);
    } catch (failure) {
      error.textContent = failure instanceof Error ? failure.message : "Couldn't send. Try again.";
      error.hidden = false;
      form.querySelector("button").disabled = false;
    }
  });
</script>
```

**Next.js**

La server action appelle `redirect()` avec l'URL de réservation une fois qu'Ikiform a répondu ok. Next.js y accepte les URL absolues, et le formulaire fonctionne avant l'hydratation, car une server action répond à un envoi de formulaire classique par un `303`. Gardez `redirect()` hors de tout bloc `try`, puisqu'il fonctionne en levant une exception.

`app/demo/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";
import { redirect } from "next/navigation";

const BOOKING = "https://cal.com/acme/demo";

export async function requestDemo(_previous: string, formData: FormData): Promise<string> {
  const lead = {
    name: String(formData.get("name") ?? "").trim(),
    email: String(formData.get("email") ?? "").trim(),
    company: String(formData.get("company") ?? "").trim(),
    team_size: String(formData.get("team_size") ?? ""),
    _gotcha: String(formData.get("_gotcha") ?? ""),
  };
  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(lead),
  });
  const result = await response.json().catch(() => null);
  if (!result?.ok) {
    const fields: Record<string, string[]> = result?.error?.fields ?? {};
    return Object.values(fields)[0]?.[0] ?? result?.error?.message ?? "Couldn't send. Try again.";
  }

  const next = new URL(lead.team_size === "1_10" ? "https://acme.example/tour" : BOOKING);
  next.searchParams.set("name", lead.name);
  next.searchParams.set("email", lead.email);
  redirect(next.toString());
}
```

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

```tsx
"use client";

import { useActionState } from "react";
import { requestDemo } from "./actions";

export function DemoForm() {
  const [error, action, pending] = useActionState(requestDemo, "");

  return (
    <form action={action}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      <label htmlFor="email">Work email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      <label htmlFor="company">Company</label>
      <input id="company" name="company" autoComplete="organization" required />
      <label htmlFor="team_size">Team size</label>
      <select id="team_size" name="team_size" required>
        <option value="1_10">1 to 10</option>
        <option value="11_50">11 to 50</option>
        <option value="51_200">51 to 200</option>
        <option value="201_plus">201 or more</option>
      </select>
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error ? <p role="alert">{error}</p> : null}
      <button disabled={pending}>{pending ? "Finding times" : "Pick a time"}</button>
    </form>
  );
}
```

**SvelteKit**

La form action renvoie `fail()` avec le premier message en cas d'erreur et appelle `redirect(303, url, { external: true })` en cas de succès ; SvelteKit 3 refuse une redirection vers un autre site sans cette option. Déclarez `IKIFORM_SECRET_KEY` dans `src/env.ts` avec `defineEnvVars` pour que `$app/env/private` puisse la lire. Associez-la à n'importe quel formulaire `+page.svelte` qui envoie à l'action par défaut ; sans JavaScript, le navigateur suit lui-même le `303`.

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

```ts
import { fail, redirect } 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 lead = Object.fromEntries(
      ["name", "email", "company", "team_size", "_gotcha"].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(lead),
    });
    const result = await response.json();
    if (!result.ok) {
      const fields: Record<string, string[]> = result.error.fields ?? {};
      return fail(response.status, { lead, message: Object.values(fields)[0]?.[0] ?? result.error.message });
    }

    const next = new URL(lead.team_size === "1_10" ? "https://acme.example/tour" : "https://cal.com/acme/demo");
    next.searchParams.set("name", lead.name);
    next.searchParams.set("email", lead.email);
    redirect(303, next.toString(), { external: true });
  },
} satisfies Actions;
```

**React**

Dans une single-page app, le navigateur envoie directement à Ikiform et poursuit avec `location.assign`. Utilisez **Allowed domains** pour protéger le formulaire, jamais une clé secrète.

`src/DemoForm.tsx`:

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

const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const BOOKING = "https://cal.com/acme/demo";

export function DemoForm() {
  const [error, setError] = useState("");
  const [pending, setPending] = useState(false);

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    setPending(true);
    setError("");
    try {
      const response = await fetch(ENDPOINT, { method: "POST", headers: { Accept: "application/json" }, body: data });
      const result = await response.json();
      if (!result.ok) {
        const fields: Record<string, string[]> = result.error.fields ?? {};
        setError(Object.values(fields)[0]?.[0] ?? result.error.message);
        setPending(false);
        return;
      }
      const next = new URL(data.get("team_size") === "1_10" ? "/tour" : BOOKING, window.location.href);
      next.searchParams.set("name", String(data.get("name")));
      next.searchParams.set("email", String(data.get("email")));
      window.location.assign(next);
    } catch {
      setError("Couldn't reach the server. Try again.");
      setPending(false);
    }
  }

  return (
    <form action={ENDPOINT} method="post" onSubmit={onSubmit}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" autoComplete="name" required />
      <label htmlFor="email">Work email</label>
      <input id="email" name="email" type="email" autoComplete="email" required />
      <label htmlFor="company">Company</label>
      <input id="company" name="company" autoComplete="organization" required />
      <label htmlFor="team_size">Team size</label>
      <select id="team_size" name="team_size" required>
        <option value="1_10">1 to 10</option>
        <option value="11_50">11 to 50</option>
        <option value="51_200">51 to 200</option>
        <option value="201_plus">201 or more</option>
      </select>
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {error ? <p role="alert">{error}</p> : null}
      <button disabled={pending}>{pending ? "Finding times" : "Pick a time"}</button>
    </form>
  );
}
```

> Les versions côté serveur envoient avec une clé secrète : Ikiform ignore donc **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 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 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`.

## E-mail professionnel, sans bloquer Gmail

Ikiform n'a pas de réglage qui refuse les fournisseurs d'e-mail gratuits. L'option `allowed_domains` du champ e-mail est une liste d'autorisation : utile pour un formulaire interne limité au domaine de votre propre entreprise, inadaptée à un formulaire de démo public. Ce n'est pas un problème, car bloquer Gmail bloque aussi les fondateurs, les consultants et les acheteurs qui remplissent des formulaires depuis leur téléphone.

Étiquetez plutôt ces demandes, là où votre équipe les voit. Cette fonction s'exécute dans le relais qui publie chaque demande dans le chat ou votre CRM, et donne un indice au commercial avant l'appel :

`lib/demo-tag.ts`:

```ts
const FREE_MAIL = new Set(["gmail.com", "googlemail.com", "outlook.com", "hotmail.com", "yahoo.com", "icloud.com", "proton.me"]);

export function demoTag(fields: Record<string, unknown>) {
  const domain = String(fields.email ?? "").split("@")[1]?.toLowerCase() ?? "";
  const size = String(fields.team_size ?? "");
  if (FREE_MAIL.has(domain)) return "personal address, check the company";
  if (size === "51_200" || size === "201_plus") return "priority";
  return "standard";
}
```

Appelez-la avec `event.data.fields` après `verifyWebhook`, puis ajoutez l'étiquette au message de chat ou à une propriété du CRM. [Les leads vers HubSpot ou Pipedrive](https://ikiform.com/fr/guides/lead-capture/leads-to-crm.md) montre un relais complet.

## Vérifiez que tout fonctionne

1. Ajoutez le domaine de votre site et `localhost` sous **Allowed domains**.
2. Envoyez avec la taille d'équipe **51 à 200**. La page de réservation s'ouvre avec votre nom et votre e-mail remplis.
3. Fermez la page de réservation sans choisir de créneau. La demande est toujours dans **Submissions**, et l'e-mail de notification est dans votre boîte.
4. Envoyez à nouveau avec **1 à 10** et vérifiez que vous arrivez sur la visite guidée.
5. Envoyez avec un e-mail invalide. L'erreur s'affiche sur le formulaire, et la page de réservation ne s'ouvre pas.
6. Réservez un rendez-vous de test, puis vérifiez que la confirmation de l'outil de réservation arrive à l'adresse saisie.

## Pièges

- **Deux enregistrements par démo réservée.** Ikiform contient la demande ; l'outil de réservation contient le rendez-vous. Rapprochez-les par e-mail dans votre CRM, pas par nom.
- **Pas de confirmation de la part d'Ikiform.** Ikiform n'envoie pas d'e-mail à la personne qui remplit le formulaire. La confirmation et l'invitation d'agenda de l'outil de réservation couvrent les appels réservés. Pour les demandes qui ne réservent jamais, répondez depuis l'e-mail de notification le jour même.
- **Les paramètres d'URL sont visibles.** Le nom et l'e-mail dans l'URL de réservation apparaissent dans l'historique du navigateur et dans les statistiques de l'outil de réservation. N'y passez rien d'autre.
- **Les embeds.** Si vous intégrez plutôt l'outil de réservation sur votre propre page `/book`, Cal.com ne transmet les paramètres d'URL de votre page que si `forwardQueryParams` est activé. Calendly documente le pré-remplissage des embeds via l'objet `prefill` de son embed JavaScript : utilisez-le plutôt qu'une simple iframe.
- **La vérification anti-spam.** Avec **Spam check** réglé sur **Ikiform**, les versions navigateur doivent appeler `challenge()` de `@ikiform/sdk` avant d'envoyer, sinon elles reçoivent `403 challenge_required`. Les versions serveur contournent la vérification.

## Questions

### Comment transmettre le nom et l'e-mail du formulaire à Calendly ou Cal.com ?

Ajoutez-les aux paramètres d'URL du lien de réservation, sous la forme ?name=Ada%20Lovelace&email=ada%40example.com. Construisez l'URL dans votre propre code une fois que le backend de formulaires a répondu ok, et envoyez-y le navigateur.

### Pourquoi ne pas envoyer directement les gens vers la page de réservation, sans formulaire ?

Beaucoup de visiteurs ouvrent un outil de réservation et repartent sans réserver, et l'outil n'enregistre que les personnes qui réservent. Un court formulaire avant garantit que votre équipe a chaque demande, avec une adresse de réponse, et peut relancer ceux qui n'ont pas choisi de créneau.

### Puis-je utiliser _redirect pour envoyer les gens vers ma page de réservation ?

Seulement vers une URL fixe sur l'un des domaines autorisés du formulaire, et elle ne peut pas inclure ce que le visiteur a tapé. Pour pré-remplir le formulaire de réservation, construisez plutôt l'URL dans le script de votre page ou dans votre server action.

### Le formulaire de démo peut-il refuser Gmail et les autres adresses gratuites ?

Il n'y a pas de liste de blocage intégrée. L'option allowed domains du champ e-mail n'autorise que les domaines listés, ce qui convient aux formulaires internes. Acceptez toutes les adresses et étiquetez les demandes en messagerie gratuite dans votre relais ou votre CRM.

## Autres guides

- [Formulaire de capture de leads](https://ikiform.com/fr/guides/lead-capture.md): 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à.
- [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 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 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 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.
- [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.
