Guides

Guides15 min read

Build a lead capture form that reaches you fast

A lead capture form on your own site that emails you with reply-to, pings team chat and feeds your CRM, so you answer while the lead is still there.

Preet SutharFounder, Ikiform

Put a short form on your own page that posts name, work email and one qualifying question to a form backend. Turn on notification emails so each lead lands in your inbox with Reply pointed at the lead, and send a signed webhook to your team chat and your CRM. Then measure the minutes from submit to first reply, because that number decides more deals than the form design does.

A lead form is a contact form with a job: start a sales conversation. People who fill it in are comparing you with someone else, often in another tab. The team that answers first, with a useful reply from a real person, usually gets the call. This guide builds the form on your own site, gets each lead to you within seconds, and leaves the CRM optional.

How a lead travels

The form posts to Ikiform. Ikiform checks the post for spam, stores it, and then does three things at once: emails your team with the lead as the reply-to address, sends a signed webhook, and shows the lead in the dashboard. The webhook is how the lead reaches team chat and your CRM.

How a lead gets from your form to the person who answers itlead formyour site, anystack orbuilderIkiformspam check,stores the leadnotificationemailReply goes tothe leaddashboardsearch, filter,exportsignedwebhooksubmission.createdchat andCRMrelay orautomationpostrelayHow a lead gets from your form to the person who answers itlead formyour site, anystack orbuilderIkiformspam check,stores the leadnotificationemailReply goes to theleaddashboardsearch, filter,exportsigned webhooksubmission.createdchat and CRMrelay or automationpostrelay
  1. A visitor fills in the lead form on your site, and it posts to your Ikiform endpoint.
  2. Ikiform checks the honeypot, the spam score and the rate limit, then stores the lead.
  3. Seconds later each notification recipient gets an email, and pressing Reply writes to the lead.
  4. The lead shows up in the dashboard, where you can search, filter and export it.
  5. Ikiform sends a signed submission.created webhook.
  6. A relay or an automation tool posts the lead to team chat and creates the contact in your CRM.
Email is the fastest path with no setup. The webhook adds the places your team already watches.

Four ways to capture leads

Each common setup is good at something. Pick by where you want leads to live and who maintains the form.

ApproachGood atTrade-offs
Your CRM's own form, such as HubSpot forms or Pipedrive's web formsLeads land in the CRM as contacts with no glue. Tracking and lead scoring live in one product.The form is embedded with the CRM's script and styling. Moving CRMs means rebuilding every form. Some features depend on the plan.
Form builder embed, such as Typeform or TallyPolished multi-step forms with logic and no code.An embed that looks like the builder unless you pay to restyle it. Webhooks and response limits depend on the plan.
Your own API route and databaseAny logic you want, such as lead scoring before you store anything.You build spam filtering, rate limits, email alerts, retries and export, and keep a server running for one form.
Form backend, such as IkiformYour own HTML on any site. Spam filtering, email alerts with reply-to, signed webhooks and CSV export with no server.No lead scoring, no CRM records and no email to the lead. Those come from your CRM or email tool, by webhook.

If your whole sales process runs inside one CRM and you're happy with its form, use it. A form backend fits when the form must match your site, when you might change CRM, or when you want the same form to reach email, chat and a CRM without picking one.

Ask less, qualify once

Every field you add costs submissions. Most lead forms need four things: who to reply to, where they work, what they want, and room to explain. One fixed-choice question does the qualifying, because it arrives as one of a few known values your routing can trust. Try it below; it runs Ikiform's validator in your browser and shows what would be stored, or the 422 you'd get back.

Preview

Runs Ikiform's validator in your browser. Nothing is sent.

Paste this definition on the form's Fields tab and Ikiform enforces it on the server. The contact form template has a lighter version with a topic instead of an interest.
FieldWhyKeep in mind
nameA reply that starts with their nameKeep the id name. A link in a name field is a strong spam signal
emailWho Reply writes toKeep the id email. Labelling it Work email nudges people to use one, but doesn't enforce it
companyLook them up before you answerOptional. Requiring it costs submissions from freelancers
interestRouting and priorityA select stores one listed value or refuses the post, so automations can branch on it
messageContext for the first replyOptional. A required essay box is the field people abandon on

Phone numbers, team size and budget belong on forms where they change what you do next. The quote request template is the fuller version, with service, budget and timeline as fixed choices. If a sales rep calls every lead, ask for a phone. If you route by team size, ask for it as a select. Otherwise leave them for the first reply.

The form

Pick your stack. Every version shows the server's message under the field it belongs to and keeps what the visitor typed, because losing a half-written message to one typo is how leads give up. The success state names who replies and when, so nobody wonders if it worked.

The form posts the plain way and redirects to your thank-you page, so it works before any script loads. Each field has an empty slot for its error.

contact-sales.html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST" id="lead-form" novalidate>
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required />
  <p class="error" data-error-for="name" hidden></p>

  <label for="email">Work email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <p class="error" data-error-for="email" hidden></p>

  <label for="company">Company</label>
  <input id="company" name="company" autocomplete="organization" />

  <label for="interest">What can we help with?</label>
  <select id="interest" name="interest" required>
    <option value="pricing">Pricing for my team</option>
    <option value="demo">A live demo</option>
    <option value="custom">A custom plan or contract</option>
    <option value="other">Something else</option>
  </select>

  <label for="message">Anything we should know?</label>
  <textarea id="message" name="message" rows="4"></textarea>
  <p class="error" data-error-for="message" hidden></p>

  <input type="hidden" name="page" value="pricing" />
  <input type="hidden" name="_redirect" value="https://example.com/thanks" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Talk to sales</button>
</form>

The script keeps the visitor on the page. It shows each field's error from the server under that field, and swaps the form for a line that says who replies and when.

lead-form.js
const form = document.getElementById("lead-form");

function showErrors(fields) {
  form.querySelectorAll("[data-error-for]").forEach((slot) => {
    const message = fields[slot.dataset.errorFor]?.[0];
    slot.textContent = message ?? "";
    slot.hidden = !message;
  });
}

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const button = form.querySelector("button");
  button.disabled = true;
  button.textContent = "Sending";
  try {
    const response = await fetch(form.action, {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new FormData(form),
    });
    const result = await response.json();
    if (result.ok) {
      const thanks = document.createElement("p");
      thanks.setAttribute("role", "status");
      thanks.textContent = `Thanks, ${form.elements.name.value.trim().split(" ")[0]}. A person on our team will reply to ${form.elements.email.value} within one working day.`;
      form.replaceWith(thanks);
      return;
    }
    showErrors(result.error.fields ?? {});
    if (!result.error.fields) alert(result.error.message);
  } catch {
    alert("Something went wrong. Email sales@example.com and we'll pick it up.");
  }
  button.disabled = false;
  button.textContent = "Talk to sales";
});
  • Replace frm_xxxxxxxxxxxxxxxx with your form's id from the dashboard.
  • The hidden _gotcha input is the honeypot. People never see it; bots that fill in every input mark themselves as spam.
  • The hidden page input records which page the lead came from: pricing, a feature page, a campaign landing page. If you define fields, add page as an optional text field, or the post is refused with Unknown field.
  • The server versions forward the visitor's Origin header, because a server-side fetch sends none and Ikiform detects free localhost tests from it.
  • The plain HTML form needs Redirect after submit, or a _redirect input on one of your Allowed domains, to land on your own thank-you page.
The server-side versions post with a secret key, so Ikiform skips Allowed domains and its human check. Every post then comes from your server's IP, and the Standard limit of 5 a minute per visitor counts all your visitors together. Choose the Relaxed or Custom rate limit for a form you post through a server, and keep IKIFORM_SECRET_KEY in server-only environment variables.

The docs cover each framework's setup in depth: Next.js, Astro, SvelteKit, Nuxt, React, Vue, posting from a server and plain HTML.

Reach the lead in minutes

The form is the easy part. What separates a lead form that sells from one that collects is the time between submit and a reply from a person. Here is what that path looks like when it works:

The minutes between a lead's submit and your first replysubmit0 sstoredunder a secondemail andchatseconds lateryou replyfrom your inboxqueueReplyThe minutes between a lead's submit and your first replysubmit0 sstoredunder a secondemail and chatseconds lateryou replyfrom your inboxqueueReply
  1. The lead submits the form.
  2. Ikiform stores it and answers the browser in well under a second.
  3. The notification email and the webhook go out from a queue seconds later.
  4. Someone on the team presses Reply in their inbox, or the mailto link in chat, and the reply goes to the lead.
Everything up to the email takes seconds. The last arrow is the one you control, and the one that wins deals.

Reply from your inbox

Each notification email sets its reply-to address to the lead. Ikiform takes it from a _replyto field first, then from a field named email, email_address, your_email, reply_to, contact_email. Keep the field id email and Reply goes to the lead, not to Ikiform. The subject is New submission: plus the form's name, so name the form after what it is, such as Talk to sales, and filter on that subject to label or star leads.

Add up to 10 recipients per form in the form's notification settings: workspace members, or outside addresses that confirm by email first. Send it to the people who reply, not to a shared alias nobody owns: a lead in five inboxes and claimed by no one waits longer than a lead in one.

Notification emails are capped at 50 per form per hour, or 10 while a workspace is younger than 7 days, then one digest for the rest of the hour. A normal lead form never gets close. If you run a campaign that might, route leads to chat by webhook as well.

Ping the team where they are

Email works when someone owns the inbox. A team that lives in chat answers faster when the lead shows up there. Add a webhook to Slack, Teams or Discord following Slack, Microsoft Teams or Discord. Put a mailto: link in the message so the person who claims it can reply in one click.

Route by what they asked for

Because interest only accepts listed values, a small relay can branch on it with confidence. This route sends demo requests and contract questions to their own channels and everything else to the main sales channel:

app/api/leads/route.ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

const CHANNELS: Record<string, string | undefined> = {
  demo: process.env.SLACK_DEMO_WEBHOOK,
  custom: process.env.SLACK_ENTERPRISE_WEBHOOK,
};

export async function POST(request: Request) {
  const payload = await request.text();
  let event;
  try {
    event = await verifyWebhook({ payload, headers: request.headers, secret: process.env.IKIFORM_WEBHOOK_SECRET! });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
  if (event.type !== "submission.created") return new Response(null, { status: 204 });

  const lead = event.data.fields;
  const interest = String(lead.interest ?? "other");
  const target = CHANNELS[interest] ?? process.env.SLACK_SALES_WEBHOOK!;
  const text = [
    `*${lead.name}* from ${lead.company || "an unknown company"} wants: ${interest}`,
    `Reply: mailto:${lead.email}`,
    lead.message ? `> ${String(lead.message).slice(0, 300)}` : "",
  ]
    .filter(Boolean)
    .join("\n");

  const response = await fetch(target, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ text }),
  });
  if (!response.ok) return new Response(`Slack answered ${response.status}`, { status: 502 });
  return new Response(null, { status: 204 });
}
  • verifyWebhook checks the signature and timestamp, so nobody else can post fake leads into your channels.
  • A 502 when Slack is down makes Ikiform retry the same delivery, with the same webhook-id, for about 3 days.
  • Change the label of an option freely. Keep its value stable once a relay or automation depends on it.

Keep junk out of the pipeline

Lead forms attract a particular kind of spam: people selling SEO, link building and outsourced development to you. It wastes the fastest reply you have, so filter it before it reaches anyone.

DefenceWhat it stopsSetup
HoneypotBots that fill in every inputThe hidden _gotcha input
Spam scoringKnown sales-pitch phrases such as seo services (+2), links in the name (+3), three or more links (+2), repeats (+3). At 5 it is spamNone
Rate limitScripts that post in a loopSet the form to Strict, 2 every 10 minutes per visitor. A real lead rarely submits twice in ten minutes
Allowed domainsPosts from other sitesAdd your domain under Allowed domains
Human checkBots in a real browserSet Spam check to Ikiform. The fetch versions then need a token from challenge()

Spam gets the same success answer as a real lead, sends no email or webhook, and doesn't count toward usage. That also means a misjudged lead is silent. Check the Spam filter once a week; marking one as not spam sends its email and webhooks then. The spam docs list every signal.

Know where leads come from

Each submission already records the page's origin and referrer, which shows up in the webhook as meta.origin and meta.referrer. Campaign tags need a little help, because the visitor often lands on one page and submits on another. This snippet copies utm_ parameters into hidden inputs and remembers them for the session:

attribution.html
<input type="hidden" name="utm_source" />
<input type="hidden" name="utm_medium" />
<input type="hidden" name="utm_campaign" />

<script>
  const params = new URLSearchParams(location.search);
  for (const key of ["utm_source", "utm_medium", "utm_campaign"]) {
    let value = params.get(key);
    try {
      if (value) sessionStorage.setItem(key, value);
      else value = sessionStorage.getItem(key);
    } catch {}
    const input = document.querySelector(`#lead-form [name="${key}"]`);
    if (input && value) input.value = value.slice(0, 200);
  }
</script>

In a framework, read the same parameters from the URL on the server and render them as hidden inputs. With fields defined, add each utm_ id as an optional text field. Names that start with _ are never stored, so don't use them for data you want to keep.

What the lead sees next

Ikiform doesn't email the person who fills in the form. Notification emails go to you and your team only, so there is no autoresponder, confirmation email or double opt-in. Send those from your email tool, triggered by a webhook. For a lead form that is often fine: a quick personal reply beats an automated one. If you want an instant acknowledgement anyway, send it from your CRM or email tool when the webhook creates the contact, so replies and unsubscribes are tracked in one place.

Whatever you send, the on-page success message is the promise. Say who replies and by when, and keep it. A form that says one working day and answers in ten minutes earns trust; one that says ten minutes and answers tomorrow loses it.

Leads in your CRM

There are no one-click integrations. Every tool is reached through a signed webhook, directly or through an automation tool such as Zapier or n8n. For a CRM, that means a relay maps Ikiform's fixed webhook body to the CRM's create-contact call. Leads to HubSpot or Pipedrive walks through both, including duplicates and retries. Until you need a CRM, the dashboard and a weekly CSV export are a fine pipeline.

Pick your situation

The setup above works for any lead. These pages cover what changes for a particular kind of lead, and for getting leads into a CRM.

Check it works

Run through this before you send traffic to the page. Ikiform has no free plan. Testing is free: a workspace without a plan accepts 50 test submissions a month from localhost. To go live, pick Pro from $6/mo for 500 submissions, or pay once: 10,000 submissions for $49 or 50,000 submissions for $149, with credits that never expire. See pricing.

  1. Under the form's Allowed domains, add your site's domain, plus localhost while you develop. Leave it empty to accept posts from anywhere.
  2. Fill in the form on your page and submit it. You land on your thank-you page, or Ikiform's hosted one.
  3. Open Submissions in the dashboard. The new submission is there with every field you sent.
  4. Check each recipient's inbox. The subject is New submission: plus the form's name, and Reply opens a message to the address you entered.
  5. If you added a webhook, open the endpoint in Webhooks. The delivery shows succeeded with your server's answer.
  6. Post with the honeypot filled in, _gotcha=bot. It still gets a success answer, but the submission is stored as spam and sends no email or webhook.
  7. Nothing arrived? Switch the dashboard's status filter to Spam. Spam gets the same success answer as a real submission.
  8. Time it: submit from your phone and note how long until the email and the chat message arrive.

Troubleshooting

ProblemWhat to do
Reply goes to Ikiform's address, not the leadThe form has no field Ikiform recognises as the lead's email. Rename the field to email, or send a _replyto field with the address. Accepted names: email, email_address, your_email, reply_to, contact_email.
422 with Unknown field for page or utm_sourceThe form has a field definition and the hidden input isn't in it. Add it as an optional text field, or remove the input.
422 validation_failedThe post doesn't match the form's field definition. error.fields names each field and the reason. Match the input name to the field id.
403 origin_not_allowed, or every post lands in spamThe page's domain isn't in the form's Allowed domains. Add it, plus localhost for local tests. Subdomains of a listed domain are allowed.
429 rate_limitedOne visitor sent more than the form's rate limit allows. Standard allows 5 a minute per visitor. Wait for Retry-After, or choose Relaxed or Custom for forms that post from one shared IP.
403 challenge_requiredThe form's spam check is set to Ikiform and a fetch post had no token. Get one with challenge(formId) from @ikiform/sdk before each submit, or turn the check off.
The submission is in the dashboard but no email or webhook arrivedIt was stored as spam. Spam sends nothing. Open it under the Spam filter to see the reasons, and mark it as not spam to send its email and webhooks.
A recipient gets no notification emailsAn address outside the workspace must confirm first, from the email Ikiform sends it. Check that it was confirmed, then check the recipient's spam folder.

Questions

Stuck on a step, or want a guide for your situation? Email hello@ikiform.com.