# Monte um formulário de pedido de consulta para um negócio de serviços

> Um formulário de pedido de consulta para contadores, advogados, coaches e clínicas que pergunta como e quando ligar, e deixa os detalhes sensíveis para a conversa.

Publicado em 2026-10-25 · https://ikiform.com/pt-br/guides/lead-capture/consultation-request

Peça nome, e-mail, como falar com a pessoa e mais ou menos quando, além de um assunto geral, e deixe todos os detalhes da situação dela para a conversa. Registre o fuso horário a partir do navegador para que uma manhã pedida seja a manhã dela. Aplique no seu próprio código as regras que o schema do formulário não consegue expressar, como exigir telefone só quando a pessoa escolheu uma ligação.

## O que muda num pedido de consulta

Um pedido de consulta vem de uma pessoa com um problema: uma carta da Receita, uma disputa de aluguel, um negócio que ela quer abrir. Ela quer explicar, e o formulário a convida a isso. Aí está a armadilha. Tudo o que ela digitar é armazenado com o envio, mostrado nos e-mails de notificação e enviado em webhooks para onde quer que você os aponte. Números de conta e detalhes de processos não pertencem a nenhum desses lugares.

Então a missão do formulário é mais estreita do que parece: quem é a pessoa, como e quando falar com ela e, de forma geral, do que se trata. Todo o resto espera pela conversa, em que você controla o que fica registrado e onde fica guardado.

Diagrama: O que um formulário de consulta pergunta, e o que espera pela conversa

1. O formulário pergunta quem é a pessoa: nome e e-mail.
2. Ele pergunta como falar com ela: e-mail, telefone ou vídeo.
3. Ele pergunta quando: um dia preferido, uma faixa de horário e o fuso horário.
4. Ele pergunta do que se trata, como um assunto geral.
5. A conversa cobre a situação dela, nas palavras dela.
6. Documentos passam por um canal que você escolhe, não pelo formulário.
7. Números de conta, documentos de identificação e registros nunca são digitados no formulário.
8. A conversa termina com a sua resposta sobre adequação e honorários.

A linha de cima basta para agendar a conversa e se preparar para ela. A linha de baixo é o motivo da conversa.

## Os campos

Formulário: Free consultation

- **Nome** (`name`, text, obrigatório)
- **E-mail** (`email`, email, obrigatório)
- **Como devemos falar com você?** (`contact_method`, radio, obrigatório)
- **Telefone** (`phone`, phone)
- **Do que se trata?** (`topic`, select, obrigatório)
- **Dia preferido** (`preferred_date`, date)
- **Melhor horário** (`time_window`, radio)
- **Fuso horário** (`timezone`, text)
- **Algo que devemos saber antes?** (`note`, textarea)

Escolha **Ligação telefônica** e deixe o telefone vazio: o schema aceita, porque um campo não pode ser obrigatório só às vezes. O código abaixo fecha essa brecha.

| Campo | Por que ele tem esse formato |
| --- | --- |
| `contact_method` | Um `radio`, para que o primeiro contato aconteça do jeito que a pessoa pediu |
| `phone` | Opcional no schema, obrigatório no seu código quando a pessoa escolhe ligação telefônica |
| `preferred_date` | Um campo `date` armazena `YYYY-MM-DD`. O `min` do schema é fixo, então a página define amanhã como `min` no navegador |
| `timezone` | Preenchido a partir do navegador com `Intl.DateTimeFormat().resolvedOptions().timeZone`, como `Europe/Berlin` |
| `note` | Limitado a 500 caracteres. Os e-mails de notificação mostram até 500 caracteres por valor, então a nota inteira cabe |

## Regras condicionais no seu próprio código

O Ikiform valida cada campo isoladamente. Regras que dependem de outro campo, como telefone só para ligações, rodam no seu código. As versões no servidor conseguem aplicá-las de verdade; as versões no navegador orientam o visitante e não enviam nada que não precise.

**HTML**

O script define `required` no input de telefone quando **Phone call** é escolhido, define amanhã como o primeiro dia possível no calendário do próprio visitante e preenche o fuso horário oculto.

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

A server action recusa uma ligação telefônica sem número antes de enviar, limpa um número que a pessoa digitou e depois trocou de opção, e redireciona para uma página de agradecimento. Renderize o formulário com `useActionState` e mostre `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**

O input de telefone só existe quando uma ligação é escolhida, e os valores vazios são descartados antes do envio, então os campos opcionais chegam ausentes em vez de em branco.

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

As versões no servidor enviam com uma chave secreta, então o Ikiform pula **Allowed domains** e a verificação humana. Todo envio passa a vir do IP do seu servidor, e o limite Standard de 5 por minuto por visitante conta todos os seus visitantes juntos. Escolha o [limite de taxa](https://ikiform.com/pt-br/docs/rate-limits.md) Relaxed ou Custom para um formulário que você envia por um servidor, e mantenha `IKIFORM_SECRET_KEY` em variáveis de ambiente só do servidor. As versões no servidor repassam o header `Origin` do visitante, porque um fetch feito no servidor não envia nenhum e o Ikiform detecta por ele os testes gratuitos de `localhost`.

## Mantenha detalhes sensíveis fora do formulário

- Diga isso no texto de ajuda, como o formulário acima faz. As pessoas seguem uma instrução simples com mais frequência do que você imagina.
- Não adicione campos para números de documentos, números de conta ou detalhes médicos. Se a sua profissão tem regras sobre dados de clientes, confira-as antes de coletar qualquer coisa além dos dados de contato, e colete o resto por um canal que as cumpra.
- Os nomes dos campos também importam. O Ikiform segura o e-mail de notificação para revisão quando o nome de um campo parece uma credencial, incluindo nomes que contêm `password`, `ssn`, `cvv` ou `seed`. Um campo chamado `seed_round` no formulário de uma consultoria para startups dispara isso. Chame-o de `funding_stage`.
- Aponte webhooks só para lugares em que toda a equipe pode ler nomes de clientes, como um canal privado, não um canal da empresa inteira.

## A primeira resposta

O e-mail de notificação chega com o **Reply** apontado para o visitante, então a primeira resposta pode sair direto da sua caixa de entrada. Faça com que ela tenha uma única função: confirmar um horário. Dois horários concretos dentro da faixa da pessoa são mais fáceis de aceitar do que uma pergunta sobre quando fica bom para ela.

- Converta a faixa da pessoa para o seu horário antes de escrever. Uma `morning` em `Australia/Sydney` é a noite anterior em Londres.
- Ofereça dois horários no fuso da pessoa e cite o fuso no e-mail, para ninguém aparecer uma hora adiantado.
- Diga como você vai ligar: no número que a pessoa informou, ou por um link de vídeo que você envia com o convite.
- Se o assunto estiver fora do que você faz, diga isso na primeira resposta e indique alguém que faça. Essa resposta ainda vale a pena.

## Confira se funciona

1. Escolha **Phone call**, deixe o telefone vazio e envie. O navegador ou o seu servidor barra o envio com uma mensagem.
2. Escolha **Email**, envie e abra o envio. Ele não tem valor `phone` e tem o seu fuso horário, como `America/Chicago`.
3. Tente escolher hoje como dia preferido. O seletor de data não oferece essa opção.
4. Confira o e-mail de notificação. A nota aparece inteira, e o **Reply** escreve para o visitante.
5. Liste os ids dos seus campos e confira se nenhum contém `password`, `ssn`, `cvv` ou `seed`.

## Pegadinhas

- **Data sem fuso horário.** `preferred_date` é um dia do calendário, não um momento. Leia-o junto com `timezone` e `time_window` antes de ligar.
- **Radios sem nada escolhido.** Um grupo de radios sem nada marcado não envia nada. Isso não é problema para `time_window`, que é opcional, e é o motivo de `contact_method` começar com **Email** marcado.
- **Nenhuma confirmação de agendamento.** O Ikiform não envia e-mail para quem preenche o formulário. A sua primeira resposta confirma o horário. Se você quer que as pessoas escolham um horário sozinhas, passe para um agendador como em [Formulário de pedido de demonstração](https://ikiform.com/pt-br/guides/lead-capture/demo-request.md).
- **Formato do telefone.** O campo de telefone aceita de 6 a 15 dígitos como digitados. Ligue `e164` se você liga a partir de um sistema que precisa de `+14155550123`; números sem código de país passam então a ser recusados.

## Perguntas

### O que um formulário de consulta gratuita deve perguntar?

Nome, e-mail, como a pessoa quer ser contatada, um dia e horário preferidos e um assunto geral. Deixe a situação completa, documentos e quaisquer números para a própria consulta.

### Posso tornar o telefone obrigatório só quando alguém escolhe ligação telefônica?

Não na definição de campos, que valida cada campo isoladamente. Defina required no input pelo navegador quando telefone for escolhido, e confira isso no código do seu servidor antes de enviar.

### Como sei em que fuso horário está o horário pedido?

Preencha um campo oculto timezone com Intl.DateTimeFormat().resolvedOptions().timeZone no navegador. Ele armazena um nome IANA como Europe/London junto com o dia e a faixa de horário pedidos.

### O Ikiform pode enviar e-mail para quem preencheu o formulário?

Não. O Ikiform envia e-mail para você e sua equipe, não para quem enviou, então não há resposta automática nem e-mail de confirmação. Envie um webhook para sua ferramenta de e-mail e mande a confirmação de lá.

## Mais guias

- [Formulário de captação de leads](https://ikiform.com/pt-br/guides/lead-capture.md): Um formulário de leads no seu próprio site que envia e-mail com reply-to, avisa o chat da equipe e alimenta seu CRM, para você responder enquanto o lead ainda está lá.
- [Formulário de pedido de demonstração](https://ikiform.com/pt-br/guides/lead-capture/demo-request.md): Um formulário de agendar demonstração que armazena o lead e depois manda o visitante para o Cal.com ou o Calendly com nome e e-mail já preenchidos.
- [Formulário de consulta de projeto](https://ikiform.com/pt-br/guides/lead-capture/project-inquiry.md): Um formulário de "comece um projeto" para estúdios e freelancers que separa as consultas em quentes, mornas e sem fit por orçamento e prazo antes de alguém ler o briefing.
- [Formulário de pedido de orçamento](https://ikiform.com/pt-br/guides/lead-capture/quote-request.md): Um formulário de pedido de orçamento que recebe briefings como links compartilhados em vez de uploads, coloca um prazo em cada pedido e mantém leads cheios de links fora do spam.
- [Leads no HubSpot ou no Pipedrive](https://ikiform.com/pt-br/guides/lead-capture/leads-to-crm.md): Mapeie cada webhook assinado do Ikiform para o upsert de contatos do HubSpot ou para as chamadas de pessoa e lead do Pipedrive, sem contatos duplicados quando uma entrega é repetida.
- [Formulário de leads em Next.js](https://ikiform.com/pt-br/guides/lead-capture/nextjs.md): Um formulário de leads no App Router do Next.js que valida com Zod numa server action, guarda as UTMs do primeiro toque num cookie e manda leads qualificados para a sua agenda.
