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

Publicado el 2026-10-25 · https://ikiform.com/es/guides/lead-capture/consultation-request

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.

Diagrama: Qué pregunta un formulario de consulta y qué espera a la llamada

1. El formulario pregunta quién es: nombre y email.
2. Pregunta cómo contactar: email, teléfono o videollamada.
3. Pregunta cuándo: un día preferido, una franja horaria y su zona horaria.
4. Pregunta de qué trata, como un tema general.
5. La llamada cubre su situación con sus propias palabras.
6. Los documentos llegan por un canal que tú eliges, no por el formulario.
7. Los números de cuenta, identificaciones y expedientes nunca se escriben en el formulario.
8. La llamada termina con tu respuesta sobre encaje y honorarios.

La fila de arriba basta para reservar la llamada y prepararla. La de abajo es para lo que sirve la llamada.

## Los campos

Formulario: Free consultation

- **Nombre** (`name`, text, obligatorio)
- **Email** (`email`, email, obligatorio)
- **¿Cómo prefieres que te contactemos?** (`contact_method`, radio, obligatorio)
- **Teléfono** (`phone`, phone)
- **¿De qué se trata?** (`topic`, select, obligatorio)
- **Día preferido** (`preferred_date`, date)
- **Mejor hora** (`time_window`, radio)
- **Zona horaria** (`timezone`, text)
- **¿Hay algo que debamos saber antes?** (`note`, textarea)

Elige **Phone call** (llamada telefónica) y deja el teléfono vacío: el esquema lo acepta, porque un campo no puede ser obligatorio solo a veces. El código de abajo cierra ese hueco.

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

**HTML**

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.

`consultation.html`:

```html
<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>
```

**Next.js**

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

`app/consultation/actions.ts`:

```ts
"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");
}
```

**Vue**

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.

`src/components/ConsultationForm.vue`:

```vue
<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](https://ikiform.com/es/docs/rate-limits.md) 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`, `cvv` o `seed`. Un campo llamado `seed_round` en el formulario de una consultora de startups lo activa. Llámalo `funding_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 `morning` en `Australia/Sydney` es 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

1. Elige **Phone call**, deja el teléfono vacío y envía. El navegador o tu servidor lo detienen con un mensaje.
2. Elige **Email**, envía y abre el envío. No tiene valor `phone` y tiene tu zona horaria, como `America/Chicago`.
3. Prueba a elegir hoy como día preferido. El selector de fecha no lo ofrece.
4. Revisa el email de notificación. La nota se ve entera, y **Reply** escribe al visitante.
5. Repasa tus ids de campo y comprueba que ninguno contiene `password`, `ssn`, `cvv` o `seed`.

## Errores comunes

- **Fecha sin zona horaria.** `preferred_date` es un día del calendario, no un momento. Léelo junto con `timezone` y `time_window` antes 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 que `contact_method` empieza 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](https://ikiform.com/es/guides/lead-capture/demo-request.md).
- **Formato del teléfono.** El campo de teléfono acepta de 6 a 15 dígitos tal como se escriben. Activa `e164` si llamas desde un sistema que necesita `+14155550123`; los números sin código de país se rechazan entonces.

## Preguntas

### ¿Qué debe preguntar un formulario de consulta gratuita?

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.

### ¿Puedo hacer obligatorio el teléfono solo cuando alguien elige una llamada telefónica?

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.

### ¿Cómo sé en qué zona horaria está la hora solicitada?

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.

### ¿Puede Ikiform enviar un email a la persona que envió el formulario?

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

## Más guías

- [Formulario de captación de leads](https://ikiform.com/es/guides/lead-capture.md): Un formulario de captación de leads en tu propio sitio que te avisa por email con reply-to, avisa al chat del equipo y alimenta tu CRM, para responder mientras el lead sigue ahí.
- [Formulario de solicitud de demo](https://ikiform.com/es/guides/lead-capture/demo-request.md): Un formulario para reservar una demo que guarda el lead y luego envía al visitante a Cal.com o Calendly con su nombre y email ya rellenados.
- [Formulario de consulta de proyecto](https://ikiform.com/es/guides/lead-capture/project-inquiry.md): Un formulario de inicio de proyecto para estudios y freelances que clasifica las consultas en calientes, templadas y sin encaje por presupuesto y plazos antes de que nadie lea el briefing.
- [Formulario de solicitud de presupuesto](https://ikiform.com/es/guides/lead-capture/quote-request.md): Un formulario de solicitud de presupuesto que recoge los briefs como enlaces compartidos en lugar de archivos, pone fecha límite a cada solicitud y evita que los leads con muchos enlaces acaben en spam.
- [Leads a HubSpot o Pipedrive](https://ikiform.com/es/guides/lead-capture/leads-to-crm.md): Convierte cada webhook firmado de Ikiform en el upsert de contactos de HubSpot o en las llamadas de personas y leads de Pipedrive, sin contactos duplicados cuando se reintenta una entrega.
- [Formulario de leads en Next.js](https://ikiform.com/es/guides/lead-capture/nextjs.md): Un formulario de leads en el App Router de Next.js que valida con Zod en una server action, guarda los UTMs del primer contacto en una cookie y envía los leads cualificados a tu calendario.
