Formulaire de capture de leads6 min de lecture
Créer un formulaire de demande de consultation pour une entreprise de services
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.
Demandez le nom, l'e-mail, le moyen de joindre la personne et à peu près quand, plus un sujet général, et gardez tous les détails de sa situation pour l'appel. Enregistrez son fuseau horaire depuis le navigateur pour qu'une matinée demandée corresponde à sa matinée. Appliquez dans votre propre code les règles que le schéma du formulaire ne peut pas exprimer, comme un numéro de téléphone obligatoire seulement si la personne a choisi un appel téléphonique.
Ce qui change avec une demande de consultation
Une demande de consultation vient d'une personne qui a un problème : un courrier des impôts, un litige de bail, une entreprise à créer. Elle veut l'expliquer, et le formulaire l'y invite. C'est là le piège. Tout ce qu'elle tape est stocké avec la soumission, affiché dans les e-mails de notification et envoyé par webhook partout où vous les dirigez. Les numéros de compte et les détails d'un dossier n'ont leur place nulle part là-dedans.
La mission du formulaire est donc plus étroite qu'il n'y paraît : qui est la personne, comment et quand la joindre, et à peu près de quoi il s'agit. Tout le reste attend l'appel, où vous contrôlez ce qui est noté et où c'est conservé.
- Le formulaire demande qui est la personne : nom et e-mail.
- Il demande comment la joindre : e-mail, téléphone ou visio.
- Il demande quand : un jour préféré, un créneau et son fuseau horaire.
- Il demande de quoi il s'agit, sous la forme d'un sujet général.
- L'appel couvre sa situation, dans ses propres mots.
- Les documents passent par un canal que vous choisissez, pas par le formulaire.
- Les numéros de compte, identifiants et dossiers ne sont jamais tapés dans le formulaire.
- L'appel se termine par votre réponse sur l'adéquation et les honoraires.
Les champs
Aperçu
Exécute le validateur d'Ikiform dans votre navigateur. Rien n'est envoyé.
| Champ | Pourquoi cette forme |
|---|---|
contact_method | Un radio, pour que le premier contact se fasse comme la personne l'a demandé |
phone | Facultatif dans le schéma, exigé par votre code quand la personne choisit un appel téléphonique |
preferred_date | Un champ date stocke YYYY-MM-DD. Le min de son schéma est fixe, donc la page définit demain comme min dans le navigateur |
timezone | Rempli depuis le navigateur avec Intl.DateTimeFormat().resolvedOptions().timeZone, comme Europe/Berlin |
note | Limité à 500 caractères. Les e-mails de notification affichent jusqu'à 500 caractères par valeur, la note tient donc en entier |
Des règles conditionnelles dans votre propre code
Ikiform valide chaque champ séparément. Les règles qui dépendent d'un autre champ, comme le téléphone seulement pour les appels téléphoniques, s'exécutent dans votre code. Les versions serveur peuvent les imposer réellement ; les versions navigateur guident le visiteur et n'envoient rien d'inutile.
Le script met required sur l'input du téléphone quand Appel téléphonique est choisi, définit demain comme premier jour possible dans le calendrier du visiteur et remplit le fuseau horaire masqué.
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST" id="consult">
<label for="name">Name</label>
<input id="name" name="name" autocomplete="name" required />
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<fieldset>
<legend>How should we reach you?</legend>
<label><input type="radio" name="contact_method" value="email" checked /> Email</label>
<label><input type="radio" name="contact_method" value="phone" /> Phone call</label>
<label><input type="radio" name="contact_method" value="video" /> Video call</label>
</fieldset>
<label for="phone">Phone</label>
<input id="phone" name="phone" type="tel" autocomplete="tel" />
<label for="topic">What is it about?</label>
<select id="topic" name="topic" required>
<option value="new_business">Starting a business</option>
<option value="tax">Tax and bookkeeping</option>
<option value="other">Something else</option>
</select>
<label for="preferred_date">Preferred day</label>
<input id="preferred_date" name="preferred_date" type="date" />
<fieldset>
<legend>Best time</legend>
<label><input type="radio" name="time_window" value="morning" /> Morning</label>
<label><input type="radio" name="time_window" value="afternoon" /> Afternoon</label>
<label><input type="radio" name="time_window" value="evening" /> Evening</label>
</fieldset>
<input type="hidden" name="timezone" />
<label for="note">Anything we should know first?</label>
<textarea id="note" name="note" rows="3" maxlength="500"></textarea>
<p>A sentence or two. Please don't include account numbers or personal records.</p>
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button>Request a consultation</button>
</form>
<script>
const form = document.getElementById("consult");
const phone = form.elements.phone;
const tomorrow = new Date(Date.now() + 86400000);
const local = new Date(tomorrow.getTime() - tomorrow.getTimezoneOffset() * 60000);
form.elements.timezone.value = Intl.DateTimeFormat().resolvedOptions().timeZone;
form.elements.preferred_date.min = local.toISOString().slice(0, 10);
form.addEventListener("change", () => {
phone.required = form.elements.contact_method.value === "phone";
});
</script>La server action refuse un appel téléphonique sans numéro avant d'envoyer, efface un numéro tapé puis abandonné au profit d'un autre choix, et redirige vers une page de remerciement. Affichez le formulaire avec useActionState et montrez state.error.
"use server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
const KEYS = ["name", "email", "contact_method", "phone", "topic", "preferred_date", "time_window", "timezone", "note", "_gotcha"];
export async function requestConsultation(_previous: Record<string, string>, formData: FormData) {
const values = Object.fromEntries(KEYS.map((key) => [key, String(formData.get(key) ?? "").trim()]));
if (values.contact_method === "phone" && !values.phone) {
return { ...values, error: "Add a phone number, or choose email or video." };
}
if (values.contact_method !== "phone") values.phone = "";
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) {
const fields: Record<string, string[]> = result?.error?.fields ?? {};
const [field, messages] = Object.entries(fields)[0] ?? [];
return { ...values, error: field ? `${field}: ${messages?.[0]}` : (result?.error?.message ?? "Couldn't send. Try again.") };
}
redirect("/consultation/thanks");
}L'input du téléphone n'existe que si un appel téléphonique est choisi, et les valeurs vides sont retirées avant l'envoi : les champs facultatifs arrivent donc absents plutôt que vides.
<script setup lang="ts">
import { computed, reactive, ref } from "vue";
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const request = reactive({
name: "",
email: "",
contact_method: "email",
phone: "",
topic: "new_business",
preferred_date: "",
time_window: "",
note: "",
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
});
const status = ref<"idle" | "sending" | "sent">("idle");
const error = ref("");
const wantsCall = computed(() => request.contact_method === "phone");
async function send() {
status.value = "sending";
error.value = "";
const body = Object.fromEntries(Object.entries(request).filter(([, value]) => value !== ""));
if (!wantsCall.value) delete body.phone;
try {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const result = await response.json();
if (result.ok) {
status.value = "sent";
return;
}
const first = Object.entries(result.error.fields ?? {})[0] as [string, string[]] | undefined;
error.value = first ? `${first[0]}: ${first[1][0]}` : result.error.message;
} catch {
error.value = "Couldn't reach us. Try again, or call the office.";
}
status.value = "idle";
}
</script>
<template>
<p v-if="status === 'sent'" role="status">Thanks, {{ request.name }}. We'll contact you by {{ request.contact_method }} within one working day.</p>
<form v-else @submit.prevent="send">
<label for="name">Name</label>
<input id="name" v-model="request.name" autocomplete="name" required />
<label for="email">Email</label>
<input id="email" v-model="request.email" type="email" autocomplete="email" required />
<fieldset>
<legend>How should we reach you?</legend>
<label><input v-model="request.contact_method" type="radio" value="email" /> Email</label>
<label><input v-model="request.contact_method" type="radio" value="phone" /> Phone call</label>
<label><input v-model="request.contact_method" type="radio" value="video" /> Video call</label>
</fieldset>
<template v-if="wantsCall">
<label for="phone">Phone</label>
<input id="phone" v-model="request.phone" type="tel" autocomplete="tel" required />
</template>
<label for="topic">What is it about?</label>
<select id="topic" v-model="request.topic">
<option value="new_business">Starting a business</option>
<option value="tax">Tax and bookkeeping</option>
<option value="other">Something else</option>
</select>
<label for="preferred_date">Preferred day</label>
<input id="preferred_date" v-model="request.preferred_date" type="date" />
<label for="note">Anything we should know first?</label>
<textarea id="note" v-model="request.note" rows="3" maxlength="500" />
<p v-if="error" role="alert">{{ error }}</p>
<button :disabled="status === 'sending'">{{ status === "sending" ? "Sending" : "Request a consultation" }}</button>
</form>
</template>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 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.
Garder les détails sensibles hors du formulaire
- Dites-le dans le texte d'aide, comme le fait le formulaire ci-dessus. Les gens suivent une consigne claire plus souvent qu'on ne le croit.
- N'ajoutez pas de champs pour des numéros d'identité, des numéros de compte ou des informations médicales. Si votre profession encadre les données clients, vérifiez ces règles avant de collecter autre chose que des coordonnées, et recueillez le reste par un canal qui les respecte.
- Les noms de champ comptent aussi. Ikiform retient l'e-mail de notification pour vérification quand un nom de champ ressemble à un identifiant sensible, y compris les noms contenant
password,ssn,cvvouseed. Un champ nomméseed_rounddans le formulaire d'un cabinet de conseil pour startups déclenche ce mécanisme. Nommez-lefunding_stage. - Ne dirigez les webhooks que là où toute l'équipe a le droit de lire les noms des clients, comme un canal privé, pas un canal ouvert à toute l'entreprise.
La première réponse
L'e-mail de notification arrive avec Reply pointant vers le visiteur : la première réponse peut donc partir directement de votre boîte. Donnez-lui une seule mission : confirmer un horaire. Deux créneaux concrets dans la plage indiquée sont plus faciles à accepter qu'une question sur ce qui l'arrange.
- Convertissez sa plage horaire dans votre heure avant d'écrire. Un
morningàAustralia/Sydneycorrespond à la soirée de la veille à Londres. - Proposez deux créneaux dans son fuseau horaire, et nommez ce fuseau dans l'e-mail, pour que personne n'arrive une heure en avance.
- Dites comment vous appellerez : le numéro donné, ou un lien de visio que vous envoyez avec l'invitation.
- Si le sujet sort de votre domaine, dites-le dans la première réponse et suggérez quelqu'un qui s'en occupe. Cette réponse vaut quand même la peine d'être envoyée.
Vérifiez que tout fonctionne
- Choisissez Appel téléphonique, laissez le téléphone vide et envoyez. Le navigateur ou votre serveur l'arrête avec un message.
- Choisissez E-mail, envoyez et ouvrez la soumission. Elle n'a pas de valeur
phoneet contient votre fuseau horaire, commeAmerica/Chicago. - Essayez de choisir aujourd'hui comme jour préféré. Le sélecteur de date ne le propose pas.
- Vérifiez l'e-mail de notification. La note s'affiche en entier, et Reply écrit au visiteur.
- Listez vos id de champ et vérifiez qu'aucun ne contient
password,ssn,cvvouseed.
Pièges
- Une date sans fuseau horaire.
preferred_dateest un jour du calendrier, pas un instant. Lisez-le avectimezoneettime_windowavant d'appeler. - Des boutons radio sans choix. Un groupe de boutons radio sans sélection n'envoie rien. Ça convient pour
time_window, qui est facultatif, et c'est pourquoicontact_methodcommence avec E-mail coché. - Pas de confirmation de rendez-vous. Ikiform n'envoie pas d'e-mail à la personne qui remplit le formulaire. Votre première réponse confirme l'horaire. Si vous voulez que les gens choisissent eux-mêmes un créneau, passez la main à un outil de réservation comme dans le formulaire de demande de démo.
- Le format du téléphone. Le champ téléphone accepte de 6 à 15 chiffres tels que tapés. Activez
e164si vous appelez depuis un système qui exige+14155550123; les numéros sans indicatif pays sont alors refusés.
Questions
Le nom, l'e-mail, le moyen de contact souhaité, un jour et un moment préférés, et un sujet général. Gardez la situation complète, les documents et tous les numéros pour la consultation elle-même.
Pas dans la définition des champs, qui valide chaque champ séparément. Mettez required sur l'input dans le navigateur quand le téléphone est choisi, et vérifiez-le dans votre code serveur avant d'envoyer.
Remplissez un champ timezone masqué avec Intl.DateTimeFormat().resolvedOptions().timeZone dans le navigateur. Il stocke un nom IANA comme Europe/London à côté du jour et du créneau demandés.
Non. Ikiform envoie des e-mails à vous et à votre équipe, pas à l'expéditeur : il n'y a donc ni réponse automatique ni e-mail de confirmation. Envoyez un webhook à votre outil d'e-mailing et envoyez la confirmation depuis celui-ci.
Une étape vous bloque, ou vous voulez un guide pour votre cas ? Écrivez à hello@ikiform.com.