Guides

Guides14 min read

Send HTML form submissions to your email

Get every form submission in your inbox without a mail server: why mailto: and PHP mail() fail, and a setup that filters spam and lets you reply.

Preet SutharFounder, Ikiform

An HTML form can't send email by itself, because a browser has no way to talk to a mail server. Point the form's action at a form backend, which receives the post, filters spam, stores it and emails you and your team. Name the email field email and pressing Reply in your inbox writes straight back to the person who filled in the form.

Most sites need one thing from a contact form: the message in an inbox someone reads. That turns out to be harder than it looks. HTML has a <form> element but no way to send mail, and the old shortcuts either hand the job back to the visitor or land in spam. This guide explains why, compares the options honestly, and sets up a form that emails you on any site and any stack.

Why a form can't send email on its own

When a visitor presses Send, the browser makes one HTTP request to the URL in the form's action. That's all a form can do. Sending email needs an SMTP conversation with a mail server, from a machine the receiving server trusts. A browser can't open that connection, and even if it could, mail from a random visitor's laptop would be rejected. So something on a server always sits between the form and your inbox: your own code, a script on your host, or a service.

With a form backend, that something is a URL. The form posts to https://ikiform.com/f/<id>, and Ikiform does the rest:

How a form post becomes an email in your inboxyour formany site orstackIkiformvalidates,scores spam,storesone emailperrecipientreply-to isthe senderyour inboxReply writesto thevisitorstored asspamno email, nowebhookpostnot spamsecondsspam score reachedHow a form post becomes an email in your inboxyour formany site or stackIkiformvalidates, scoresspam, storesone email perrecipientreply-to is the senderyour inboxReply writes to thevisitorstored asspamno email, nowebhookpostnot spamsecondsspam score reached
  1. The visitor submits your form, and the browser or your server posts it to your Ikiform endpoint.
  2. Ikiform checks the fields against the form's definition, scores the post for spam, and stores it.
  3. A post that reaches the spam score is stored as spam. It gets the same success answer, but sends no email and no webhook.
  4. Every other post sends one email to each recipient, with the visitor's address as the reply-to.
  5. The email arrives in your inbox seconds later. Pressing Reply writes to the visitor.
The visitor never sees an email address and never opens a mail app. The only thing on your page is a form.

Six ways to get a form into your inbox

Each of these works for someone. The table is about what each one costs you once real visitors, and real bots, start using the form.

ApproachGood atTrade-offs
action="mailto:you@example.com"No server, no account, one attribute.Nothing is sent. The browser opens the visitor's mail app with a draft, or does nothing if no mail app is set up. Your address sits in the page for scrapers. See the Gmail guide.
A PHP script with mail()Runs on almost any shared PHP host, with no third party.Depends on the host's mail setup, and mail that claims to come from your domain often fails SPF or DMARC and lands in spam. No spam filtering, no storage, and no PHP at all on static hosts. See without PHP.
A browser email SDK, such as EmailJSNo server. Email templates you design in their dashboard.The keys and template ids are in your page's JavaScript, so anyone can send your templates with their own text. See the EmailJS alternative.
Your own route plus an email API, such as Resend, Postmark, Amazon SES or Nodemailer over SMTPFull control: your subject, your HTML, a confirmation to the visitor.You build validation, spam filtering, rate limits, storage and retries, verify a sending domain, and need a server even for a static site. See the Next.js guide.
Google Apps Script as a web appRuns on a Google account you already have.Script quotas cap how many recipients it can email a day, the URL has no spam protection, and you maintain the script.
A form backend, such as Ikiform, Formspree or Web3FormsOne URL in action. Spam filtering, storage and email with no server.The email's layout and subject are the service's. With Ikiform, nothing is ever emailed to the visitor.

If you need a fully branded email, or a confirmation to the visitor, write your own route with an email API. If you want the message in your inbox and a way to reply, with spam kept out, a form backend is the least code. The rest of this guide uses Ikiform.

The form

Three fields cover most contact forms: who they are, where to reply, and what they want. Pick your stack below; the choice carries over to every code sample in these guides.

This form works with no JavaScript at all: the browser posts it, Ikiform emails you, and the visitor lands on the page in _redirect. Each field has an empty error slot under it for the script below.

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

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

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="6" required minlength="10"></textarea>
  <p data-error="message"></p>

  <input type="hidden" name="page" value="/contact" />
  <input type="hidden" name="_redirect" value="https://example.com/contact/sent" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Send message</button>
  <p id="contact-status" role="status"></p>
</form>

The script keeps the visitor on the page. It posts with fetch, writes each server error under its field, and swaps the form for a line that repeats the address you'll reply to, so a typo is easy to spot.

contact.js
const form = document.getElementById("contact");
const status = document.getElementById("contact-status");
const button = form.querySelector("button");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  for (const slot of form.querySelectorAll("[data-error]")) slot.textContent = "";
  button.disabled = true;
  status.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 email = form.elements.namedItem("email").value;
      const done = document.createElement("p");
      done.setAttribute("role", "status");
      done.textContent = `Sent. We'll reply to ${email}.`;
      form.replaceWith(done);
      return;
    }
    for (const [field, messages] of Object.entries(result.error.fields ?? {})) {
      const slot = form.querySelector(`[data-error="${field}"]`);
      if (slot) slot.textContent = messages[0];
    }
    status.textContent = result.error.fields ? "Check the fields above." : result.error.message;
  } catch {
    status.textContent = "Couldn't send. Check your connection and try again.";
  }
  button.disabled = false;
});
  • Replace frm_xxxxxxxxxxxxxxxx with your form's id from the dashboard.
  • name="email" makes that field the reply-to address of the notification email. Keep the name, even if the label says "Your email".
  • page is an ordinary hidden field. It shows up in the email, so you know which page the message came from.
  • _gotcha is the honeypot. People never see it; a bot that fills it in is stored as spam and emails nobody.
  • _redirect only works for an https: URL on one of the form's Allowed domains. Leave it out and set Redirect after submit in the form's settings instead, or Ikiform shows its own thank-you page.

The framework guides go further: Next.js, Astro, SvelteKit, Nuxt, React, Vue and plain HTML. Site builders such as Webflow and Carrd have their own pages, and the contact form template adds a topic field.

Define the fields in the dashboard too, and Ikiform refuses a post that doesn't match with 422 validation_failed and a message per field, which the code above shows next to each input. Try it here; this runs the same validator in your browser:

Preview

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

Paste this definition on the form's Fields tab. Without a definition, Ikiform accepts any field names and emails whatever arrives.

What the email looks like

Every accepted submission sends a plain, readable email. Here's the text version of one:

Notification email
From: Ikiform <notifications@ikiform.com>
To: you@yourcompany.com
Reply-To: ada@example.com
Subject: New submission: Contact

Contact got a new submission on Oct 25, 2026, 9:14 AM UTC.

name: Ada Lovelace
email: ada@example.com
message: Do you ship to Canada? I'd like to order 40 units before December.
page: /contact

View it in Ikiform:
https://ikiform.com/dashboard/submissions?form=frm_xxxxxxxxxxxxxxxx&submission=sub_...
  • It comes from notifications@ikiform.com. You don't set up DNS records or a sending domain, because the mail is sent from Ikiform's domain.
  • The subject is always New submission: <form name>. _subject doesn't change the subject of notification emails. It is passed on to webhooks and the API as meta.subject. To tell forms apart, give each form a clear name in the dashboard, such as "Contact, pricing page".
  • Fields appear in the order they were posted, labelled by their name. Fields starting with _ are left out.
  • A very long form or answer is shortened, and the email says how many more fields are in the dashboard. The limits page has the numbers.
  • Fields that look like passwords or card numbers are left out, and the email for a post that contains them is held for review. Don't collect credentials with a form backend.
  • Workspace members get a View submission button. People outside the workspace get the fields and an unsubscribe link.

Who gets the email

Recipients are set per form in the dashboard, never by the form itself. A field that named a recipient would let anyone use your form to email anyone, so Ikiform doesn't have one.

WhoHow they're addedNotes
The defaultNothing to doThe member chosen as the workspace's Notification email, or every workspace owner when none is chosen
Chosen membersTick them under the form's Notifications settingsOnly members with a verified email address
People outside the workspaceAdd someone outside the workspace, then Send linkThey get email only after they confirm. Up to 5 per form, and each can unsubscribe

Each form emails at most 10 addresses. Every recipient gets their own copy rather than a shared CC, so nobody sees the others' addresses. If a recipient's mailbox bounces, Ikiform pauses email to that address for a while and the dashboard shows Emails bounced next to it. Multiple recipients covers routing by topic, which recipients alone can't do.

Reply to the person, not to Ikiform

Ikiform sets the reply-to address from a _replyto field first, then from a field named email, email_address, your_email, reply_to, contact_email. The value must be one valid email address, or it is ignored. With a reply-to set, pressing Reply in Gmail, Outlook or Apple Mail opens a reply to the visitor, and your answer comes from your own address.

Without one, a reply goes to notifications@ikiform.com, which nobody reads. That's the most common surprise, and it's usually a field called user_email or Email Address with a space. Rename it, or add _replyto. The reply-to guide covers forms with two email fields and builders that rename fields for you.

When a lot arrives at once

Each form sends up to 50 notification emails an hour, or 10 while the workspace is younger than 7 days. Past that, the rest of the hour's submissions are stored as usual and arrive as one digest email after the hour ends. The cap protects your inbox, and your mail provider's opinion of Ikiform's mail, when a form is posted somewhere big or a bot gets past the filters.

What happens to notification emails in a busy hourone form, one clock hour in UTCsubmissions 1to 50one email each,seconds latersubmission 51onwardstored, saved forthe digesthour endsone digest per formcap reachednext hourWhat happens to notification emails in a busy hourone form, one clock hour in UTCsubmissions 1 to 50one email each, seconds latersubmission 51 onwardstored, saved for the digesthour endsone digest per formcap reachednext hour
  1. The first 50 submissions to a form in a clock hour each send an email as usual.
  2. Later submissions in the same hour are stored and shown in the dashboard straight away, without an email each.
  3. After the hour ends, each recipient gets one digest that counts them and links to the dashboard.
The cap counts per form, so a burst on one form doesn't slow another. Webhooks aren't capped and still fire for every submission.

The digest's subject is the count and the form name, and it doesn't list the fields or set a reply-to, so answer those messages from the dashboard. If you regularly go past the cap, email isn't the right channel for that form: send it to Slack or a shared inbox through a webhook instead.

Keep spam out of your inbox

Email makes spam expensive: every bot post is a notification someone opens. Ikiform stores spam with a normal success answer, so the bot learns nothing, and sends no email for it. Four things decide what counts as spam:

DefenceWhat it catchesSetup
HoneypotBots that fill in every inputThe hidden _gotcha input
Spam scoringLinks in the name field, three or more links, known spam phrases, disposable addresses and identical repeats. At 5 the post is spamNone
Allowed domainsPosts from other sitesAdd your domain in the form's settings
Human checkBots that run a real browserSet Spam check to Ikiform

Check the Spam filter in the dashboard now and then. If a real message lands there, mark it as not spam and its email goes out then. The spam docs explain each signal, including when to turn on the human check.

What email can't do

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. If your site promises "we've received your message", show that on the page after submit, where the code above already does.

  • A shared team inbox. Add the team address as a recipient, or forward from your mail provider.
  • A chat channel. Send each submission to Slack, Discord or Microsoft Teams by webhook. Chat suits forms that get more than a few messages a day.
  • A spreadsheet or CRM. Google Sheets, or any tool through Zapier or n8n.
  • Attachments. File uploads aren't supported yet. A post with a file gets 422 files_not_supported. Ask for a link to a shared file instead.

Turning email off

Some forms are better without email, such as a busy newsletter signup. Switch off Email new submissions in the form's Notifications settings. Submissions are still stored, and webhooks still fire.

The cap starts lower on new workspaces: 10 emails per form an hour for the first 7 days. If you're testing with a script, expect a digest sooner than you would later.

Pick your situation

The setup above is the same everywhere. These pages cover a particular inbox, a particular stack, or the tool you're moving away from.

Check it works

Run through this before you put the form live. 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. Under the form's Notifications settings, check that Email new submissions is on and the right people are ticked.
  3. Fill in the form on your page and submit it. You land on your thank-you page, or Ikiform's hosted one.
  4. Open Submissions in the dashboard. The new submission is there with every field you sent.
  5. Open the notification email. The subject is New submission: and the form's name, and every field you sent is listed.
  6. Press Reply. The To line shows the address you typed in the form, not Ikiform's.
  7. 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.
  8. Nothing arrived? Switch the dashboard's status filter to Spam. Spam gets the same success answer as a real submission.

Troubleshooting

ProblemWhat to do
The submission is in the dashboard but no email arrivedLook in your spam or promotions folder first, and add notifications@ikiform.com to your contacts. Then check the form's Notifications settings: email may be off, an outside address may still say Waiting for confirmation, or it may show Emails bounced.
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.
Reply goes to notifications@ikiform.comNo reply-to was found. Name the email field one of email, email_address, your_email, reply_to, contact_email, or add a _replyto field with the visitor's address.
Emails stopped and a digest arrived insteadThe form went past 50 emails in an hour (10 on a new workspace). Everything is in the dashboard. Look for a bot burst under Spam, or use a webhook for busy forms.
_subject doesn't change the subject_subject doesn't change the subject of notification emails. It is passed on to webhooks and the API as meta.subject.
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.
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.
402 test_limit_reachedThe workspace has no plan and used its 50 free localhost tests this month. Pick a plan, or use Send test in the dashboard.

Questions

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