Send email from an HTML form without a backend

By Nick Peplow ·

An HTML form can’t send email. The <form> tag collects fields and POSTs them somewhere; turning that POST into a message in your inbox is a server’s job, and on a static site there is no server. Every technique for “sending email from HTML” is really an answer to one question: what receives the POST?

Get that question wrong and the failure is silent. The form looks fine, visitors submit, and nothing arrives. Here’s how each answer holds up.

Why doesn’t mailto: work?

The oldest answer tries to skip the receiving end entirely:

<form action="mailto:you@example.com" method="POST" enctype="text/plain">

This sends nothing. It asks the browser to hand the form data to the visitor’s operating system as a compose request, and everything after that depends on the visitor’s machine:

  • Most visitors have no configured mail client. Anyone who lives in Gmail or Outlook in a browser tab gets an OS app picker, a blank window, or nothing at all. The browser can’t compose email; it can only delegate.
  • The visitor has to press Send themselves. Even on a machine with a working client, submitting your form opens a draft. Some visitors send it. Many close it and assume they’re done.
  • The data arrives mangled. Without enctype="text/plain" the draft body is one URL-encoded string (name=Jane&message=Hello%20there). With it, you get bare key=value lines and lose any structure beyond that.
  • You have no record. A closed compose window is a lead that never existed, and nothing tells you it happened.
  • Your address ships in the markup, where scrapers harvest it.

mailto: is a compose shortcut, not a delivery mechanism. Any tutorial recommending it for a contact form is a signal to close the tab.

What are the real options?

Three, and they differ mostly in what you end up maintaining.

OptionWhat receives the POSTYou maintainIt breaks when
Serverless functionYour function + an email APICode, keys, spam, deliverabilityThe email call fails after your function already said 200
Host-captured formsYour hosting platformThe vendor couplingYou change hosts or outgrow the quota
Form backendA dedicated endpoint serviceOne hidden fieldThe service’s quota — not your code

A serverless function you babysit

Wire a Netlify, Vercel, or Cloudflare function to an email API — Resend, SendGrid, SES — and you have a working form in an afternoon. What you also have: an API key to rotate, a bare endpoint that bots will find and flood, and a sending domain whose SPF and DKIM records are now your problem.

The failure mode that actually costs you leads is subtler. The obvious implementation returns 200 as soon as the function accepts the request, then calls the email API — and when that call fails on a timeout, a suspended key, or a rate limit, the submission is gone with no trace. Fixing it properly means adding a queue and durable storage, at which point you’ve built a small product you now operate.

Host-captured forms

Netlify Forms is the canonical version: add an attribute to your form, the platform parses your HTML at deploy time and captures matching POSTs into a dashboard. Genuinely low-friction — there’s nothing to sign up for beyond the host you already use.

The costs are structural rather than technical. The form pipeline is a feature of Netlify’s build system, so moving to Cloudflare Pages or Vercel — neither offers an equivalent — means rebuilding form handling from scratch. Netlify’s pricing page lists 100 form submissions per month on the free tier, per site, with a paid add-on above that. And framework-rendered forms need a static HTML mirror so build-time parsing can find them, a workaround Netlify’s own docs walk through.

A form backend

The third category exists because receiving form POSTs is a complete product on its own. A form backend gives you an endpoint URL; you point your form’s action at it, and every submission becomes an email in your inbox plus a stored record you can search and export later. Your markup, your styling, your host — the service only sees the POST. Formspree, Web3Forms, Getform, and FormWire are all this category.

What does the working form look like?

Using FormWire as the worked example, this is the entire integration:

<form action="https://api.formwire.com/submit" method="POST">
  <input type="hidden" name="access_key" value="YOUR-ACCESS-KEY">
  <input type="text" name="name" required>
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>

What each part does:

  • Plain HTML POST, zero JavaScript. This works on any host that serves files, which is all of them.
  • The access key is public by design. Delivery is locked to email addresses the account owner has verified, so the only thing anyone can do with a key scraped from your markup is send you a submission.
  • Field names become the email’s line items and the stored record’s keys. message reads better in your inbox than field_47, so name fields for the human reading them.
  • Spam is filtered before it reaches you. A server-side honeypot runs by default; filtered spam is never emailed.
  • Redirect after submit by adding a hidden field: <input type="hidden" name="_redirect" value="https://yoursite.com/thanks">. Underscore-prefixed names are reserved for controls like this — don’t invent your own.

If you’re submitting with fetch, POST JSON to the same endpoint with access_key in the body and an Accept: application/json header. Success is a 200 with {"success": true}; validation failures return a 4xx with a message you can show the user. CORS is handled server-side.

And the silent-loss problem from the serverless option is handled where it belongs: a submission is stored before the POST is even acknowledged, so an email hiccup delays the notification instead of deleting the lead — with per-submission delivery status visible in the dashboard.

The question was never about email

“Make an HTML form send email” is backwards phrasing, and backwards phrasing is why mailto: tutorials still rank. The form was never going to send anything; it needed something competent on the other end of the POST. That end is a solved category now — a form backend for static sites is one hidden field’s worth of integration — so spend your effort on the form people fill in, not the plumbing behind it.