Formulario de captación de leads6 min de lectura
Crea un formulario de solicitud de consulta para un negocio de servicios
Un formulario de solicitud de consulta para gestorías, abogados, coaches y clínicas que pregunta cómo y cuándo llamar, y deja los detalles sensibles para la llamada.
Pide nombre, email, cómo contactar y más o menos cuándo, además de un tema general, y deja todos los detalles de su situación para la llamada. Registra su zona horaria desde el navegador para que una mañana solicitada sea su mañana. Aplica en tu propio código las reglas que el esquema del formulario no puede expresar, como pedir teléfono solo cuando eligieron una llamada telefónica.
Qué tiene de distinto una solicitud de consulta
Una solicitud de consulta viene de una persona con un problema: una carta de Hacienda, un conflicto con el alquiler, un negocio que quiere montar. Quiere explicarlo, y el formulario la invita a hacerlo. Ahí está la trampa. Todo lo que escribe se guarda con el envío, aparece en los emails de notificación y se envía en webhooks a donde los apuntes. Los números de cuenta y los detalles del caso no pertenecen a ninguno de esos sitios.
Así que la tarea del formulario es más acotada de lo que parece: quién es, cómo y cuándo contactar, y más o menos de qué trata. Todo lo demás espera a la llamada, donde tú controlas qué se anota y dónde se guarda.
- El formulario pregunta quién es: nombre y email.
- Pregunta cómo contactar: email, teléfono o videollamada.
- Pregunta cuándo: un día preferido, una franja horaria y su zona horaria.
- Pregunta de qué trata, como un tema general.
- La llamada cubre su situación con sus propias palabras.
- Los documentos llegan por un canal que tú eliges, no por el formulario.
- Los números de cuenta, identificaciones y expedientes nunca se escriben en el formulario.
- La llamada termina con tu respuesta sobre encaje y honorarios.
Los campos
Vista previa
Ejecuta el validador de Ikiform en tu navegador. No se envía nada.
| Campo | Por qué tiene esta forma |
|---|---|
contact_method | Un radio, para que el primer contacto sea como lo pidieron |
phone | Opcional en el esquema, obligatorio en tu código cuando eligen una llamada telefónica |
preferred_date | Un campo date guarda YYYY-MM-DD. Su min del esquema es fijo, así que la página pone mañana como min en el navegador |
timezone | Se rellena desde el navegador con Intl.DateTimeFormat().resolvedOptions().timeZone, como Europe/Berlin |
note | Limitado a 500 caracteres. Los emails de notificación muestran hasta 500 caracteres por valor, así que la nota cabe entera |
Reglas condicionales en tu propio código
Ikiform valida cada campo por separado. Las reglas que dependen de otro campo, como el teléfono solo para llamadas telefónicas, se ejecutan en tu código. Las versiones de servidor pueden imponerlas de verdad; las de navegador guían al visitante y no envían nada que no haga falta.
El script pone required en el input del teléfono cuando se elige Phone call, fija mañana como el primer día posible en el calendario del propio visitante y rellena la zona horaria oculta.
<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 rechaza una llamada telefónica sin número antes de enviar, borra un número que escribieron y luego descartaron al cambiar de opción, y redirige a una página de agradecimiento. Renderiza el formulario con useActionState y muestra 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");
}El input del teléfono solo existe cuando se elige una llamada telefónica, y los valores vacíos se descartan antes del envío, así que los campos opcionales llegan ausentes en lugar de en blanco.
<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>Las versiones de servidor envían con una clave secreta, así que Ikiform se salta Allowed domains y su verificación humana. Entonces todos los envíos vienen de la IP de tu servidor, y el límite Standard de 5 por minuto por visitante cuenta a todos tus visitantes juntos. Elige el límite de frecuencia Relaxed o Custom para un formulario que envías a través de un servidor, y guarda IKIFORM_SECRET_KEY en variables de entorno solo de servidor. Las versiones de servidor reenvían la cabecera Origin del visitante, porque un fetch desde el servidor no envía ninguna e Ikiform detecta a partir de ella las pruebas gratuitas desde localhost.
Deja los detalles sensibles fuera del formulario
- Dilo en el texto de ayuda, como hace el formulario de arriba. La gente sigue una instrucción clara más a menudo de lo que crees.
- No añadas campos para números de identidad, números de cuenta ni datos médicos. Si tu profesión tiene normas sobre los datos de clientes, revísalas antes de recoger nada más allá de los datos de contacto, y recoge el resto por un canal que las cumpla.
- Los nombres de los campos también importan. Ikiform retiene para revisión el email de notificación cuando un nombre de campo parece una credencial, incluidos los nombres que contienen
password,ssn,cvvoseed. Un campo llamadoseed_rounden el formulario de una consultora de startups lo activa. Llámalofunding_stage. - Apunta los webhooks solo a sitios donde todo el equipo pueda leer nombres de clientes, como un canal privado, no uno de toda la empresa.
La primera respuesta
El email de notificación llega con Reply apuntando al visitante, así que la primera respuesta puede salir directamente de tu bandeja de entrada. Haz que cumpla una sola función: confirmar una hora. Dos huecos concretos dentro de su franja son más fáciles de aceptar que una pregunta sobre cuándo le viene bien.
- Convierte su franja a tu hora antes de escribir. Una
morningenAustralia/Sydneyes la tarde anterior en Londres. - Ofrece dos huecos en su zona horaria, y nombra la zona en el email, para que nadie se presente una hora antes.
- Di cómo vas a llamar: al número que dieron, o con un enlace de vídeo que envías con la invitación.
- Si el tema queda fuera de lo que haces, dilo en la primera respuesta y sugiere a alguien que sí lo haga. Esa respuesta también merece la pena enviarla.
Comprueba que funciona
- Elige Phone call, deja el teléfono vacío y envía. El navegador o tu servidor lo detienen con un mensaje.
- Elige Email, envía y abre el envío. No tiene valor
phoney tiene tu zona horaria, comoAmerica/Chicago. - Prueba a elegir hoy como día preferido. El selector de fecha no lo ofrece.
- Revisa el email de notificación. La nota se ve entera, y Reply escribe al visitante.
- Repasa tus ids de campo y comprueba que ninguno contiene
password,ssn,cvvoseed.
Errores comunes
- Fecha sin zona horaria.
preferred_datees un día del calendario, no un momento. Léelo junto contimezoneytime_windowantes de llamar. - Radios sin nada elegido. Un grupo de radios sin marcar no envía nada. Eso está bien para
time_window, que es opcional, y es la razón por la quecontact_methodempieza con Email marcado. - Sin confirmación de reserva. Ikiform no envía email a la persona que rellena el formulario. Tu primera respuesta confirma la hora. Si quieres que la gente elija un hueco por su cuenta, pasa a una herramienta de agenda como en Formulario de solicitud de demo.
- Formato del teléfono. El campo de teléfono acepta de 6 a 15 dígitos tal como se escriben. Activa
e164si llamas desde un sistema que necesita+14155550123; los números sin código de país se rechazan entonces.
Preguntas
Nombre, email, cómo quiere que le contacten, un día y una hora preferidos, y un tema general. Deja su situación completa, los documentos y cualquier número para la propia consulta.
No en la definición de campos, que valida cada campo por separado. Pon required en el input desde el navegador cuando se elige teléfono, y compruébalo en tu código de servidor antes de enviar.
Rellena un campo oculto timezone con Intl.DateTimeFormat().resolvedOptions().timeZone en el navegador. Guarda un nombre IANA como Europe/London junto al día y la franja horaria solicitados.
No. Ikiform te envía emails a ti y a tu equipo, no a quien envía el formulario, así que no hay respuesta automática ni email de confirmación. Envía un webhook a tu herramienta de email y manda la confirmación desde ahí.
¿Te atascaste en un paso o quieres una guía para tu caso? Escribe a hello@ikiform.com.