Formulário de captação de leads6 min de leitura
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.
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.
- O formulário pergunta quem é a pessoa: nome e e-mail.
- Ele pergunta como falar com ela: e-mail, telefone ou vídeo.
- Ele pergunta quando: um dia preferido, uma faixa de horário e o fuso horário.
- Ele pergunta do que se trata, como um assunto geral.
- A conversa cobre a situação dela, nas palavras dela.
- Documentos passam por um canal que você escolhe, não pelo formulário.
- Números de conta, documentos de identificação e registros nunca são digitados no formulário.
- A conversa termina com a sua resposta sobre adequação e honorários.
Os campos
Prévia
Roda o validador do Ikiform no seu navegador. Nada é enviado.
| 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.
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.
<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>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.
"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");
}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.
<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 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,cvvouseed. Um campo chamadoseed_roundno formulário de uma consultoria para startups dispara isso. Chame-o defunding_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
morningemAustralia/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
- Escolha Phone call, deixe o telefone vazio e envie. O navegador ou o seu servidor barra o envio com uma mensagem.
- Escolha Email, envie e abra o envio. Ele não tem valor
phonee tem o seu fuso horário, comoAmerica/Chicago. - Tente escolher hoje como dia preferido. O seletor de data não oferece essa opção.
- Confira o e-mail de notificação. A nota aparece inteira, e o Reply escreve para o visitante.
- Liste os ids dos seus campos e confira se nenhum contém
password,ssn,cvvouseed.
Pegadinhas
- Data sem fuso horário.
preferred_dateé um dia do calendário, não um momento. Leia-o junto comtimezoneetime_windowantes 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 decontact_methodcomeç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.
- Formato do telefone. O campo de telefone aceita de 6 a 15 dígitos como digitados. Ligue
e164se 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
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.
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.
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.
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á.
Travou em algum passo ou quer um guia para o seu caso? Envie um e-mail para hello@ikiform.com.