Form to email without a backend
Make a plain HTML form on any static host send its contents to your inbox: one endpoint, one small script, no server, no API key in the page.
"Form to email" is the oldest job on the web and it is still awkward on a static site. There is
no PHP mail() to call, and a mailto: action opens the visitor's mail
client, which half of them do not have configured. What you want is for the browser to hand the
fields to something that will email them to you. SendBeam's public form endpoint is that
something. This guide is the framework-free version: an HTML page, a 40-line script, and a form
in SendBeam that knows where to send the message. It works on GitHub Pages, Cloudflare Pages,
Netlify, Vercel, S3, a shared host, or a folder served by nginx.
How it works
You create a contact form in SendBeam and give it a destination address. That form has
a public URL of the shape https://sendbeam.io/api/forms/<form-id>. Your page
posts a JSON object to it. SendBeam records the submission, emails it to you from your workspace's
sending address with the visitor as Reply-To, and answers with a thank-you message for the page
to display. Nothing is added to your contacts, because writing in is not subscribing. The form ID
is public by nature (anyone who can submit the form can see it), so there is nothing to protect
in the page; abuse is handled at the endpoint, as described further down.
Prerequisites
- A SendBeam workspace. The Free plan covers a contact form.
- A destination address that is either a member of the workspace or on a verified sending domain.
- A static site of any kind, hosted anywhere that serves HTML and JavaScript.
Steps
- Create the form. In SendBeam, open Forms, add a form, choose type Contact, and enter the receiving address under Send messages to. Set a thank-you message. Open the saved form and copy the ID from the API Endpoint line. If you prefer to skip the hand-written markup entirely, the same page offers a ready-made embed snippet; the rest of this guide is for when you want your own HTML.
- Write the page. The form needs an
emailand amessage;nameandsubjectare optional. Thedata-endpointattribute is where the script posts to. ReplaceYOUR_FORM_IDand the fallback mailto address.<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Contact</title> </head> <body> <h1>Contact</h1> <form id="contact" data-endpoint="https://sendbeam.io/api/forms/YOUR_FORM_ID" novalidate> <p> <label for="name">Your name</label><br /> <input id="name" name="name" type="text" autocomplete="name" /> </p> <p> <label for="email">Your email</label><br /> <input id="email" name="email" type="email" autocomplete="email" required /> </p> <p> <label for="message">Message</label><br /> <textarea id="message" name="message" rows="6" required></textarea> </p> <!-- Hidden check field. Each form has its own name: copy it from the form's Embed tab. --> <div style="position:absolute;left:-9999px" aria-hidden="true"> <label>Leave this empty <input type="text" name="YOUR_FORM_CHECK_FIELD" tabindex="-1" autocomplete="off" /></label> </div> <p><button type="submit">Send</button></p> <p id="contact-status" role="status" aria-live="polite"></p> <p><small>Or email <a href="mailto:hello@example.com">hello@example.com</a>.</small></p> </form> <script src="/sendbeam-form.js" defer></script> </body> </html> - Add the script. It collects every named field into JSON, adds the render-time
stamp, posts, and shows the result. It is generic: any form on the page with a
data-endpointattribute is wired up, so the newsletter form in the next step reuses it. The:has()selector used to hide the inputs on success is supported by every current browser; on an older one the inputs simply stay visible under the thank-you message.// sendbeam-form.js — works for any form with a data-endpoint attribute (function () { document.querySelectorAll('form[data-endpoint]').forEach(function (form) { var status = form.querySelector('[role="status"]'); form.addEventListener('submit', function (event) { event.preventDefault(); // Every named field becomes a JSON key; SendBeam ignores keys the form does not declare. var payload = {}; new FormData(form).forEach(function (value, key) { payload[key] = value; }); // Turnstile, when present on the page, adds this hidden field; SendBeam reads it as the token. if (payload['cf-turnstile-response']) payload.turnstile_token = payload['cf-turnstile-response']; var button = form.querySelector('button[type="submit"]'); button.disabled = true; if (status) status.textContent = 'Sending…'; fetch(form.dataset.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) .then(function (res) { return res.json().then(function (json) { return { ok: res.ok, json: json }; }); }) .then(function (res) { if (res.ok && res.json.success) { form.querySelectorAll('p:not(:has([role="status"]))').forEach(function (p) { p.hidden = true; }); if (status) status.textContent = res.json.message; if (res.json.redirect_url) setTimeout(function () { location.assign(res.json.redirect_url); }, 1500); } else { if (status) status.textContent = res.json.error || 'Something went wrong.'; if (window.turnstile) window.turnstile.reset(); } }) .catch(function () { if (status) status.textContent = 'Could not reach the server. Please use the email link below.'; }) .finally(function () { button.disabled = false; }); }); }); })(); - Keep a fallback. The endpoint accepts JSON only, so a form cannot post to it
without JavaScript. A
mailto:link beside the form costs nothing and covers the rare visitor with scripts off, as well as the rare 503 when a notification cannot be sent.<!-- Progressive enhancement is not available: the endpoint accepts JSON only. Keep a mailto: link near the form so a visitor without JavaScript still has a route to you. --> <p>Or email <a href="mailto:hello@example.com">hello@example.com</a>.</p> - Optionally, add a signup form with the same script. Create a second form in
SendBeam of type Signup, pointed at a list, and use its ID. The script does not change.
<form data-endpoint="https://sendbeam.io/api/forms/YOUR_SIGNUP_FORM_ID" novalidate> <p> <label for="sub-email">Email</label><br /> <input id="sub-email" name="email" type="email" autocomplete="email" required /> </p> <!-- Hidden check field. Each form has its own name: copy it from the form's Embed tab. --> <div style="position:absolute;left:-9999px" aria-hidden="true"> <label>Leave this empty <input type="text" name="YOUR_FORM_CHECK_FIELD" tabindex="-1" autocomplete="off" /></label> </div> <p><button type="submit">Subscribe</button></p> <p role="status" aria-live="polite"></p> </form> - Deploy and set Allowed sites. Upload the files. Then, in the form's
Protection section in SendBeam, add your site's origin (for example
https://example.com; addhttps://www.example.comtoo if both resolve). If your host sends a Content Security Policy header, addhttps://sendbeam.iotoconnect-src.
Test it
Open the deployed page and send yourself a message. You should see the thank-you text replace the form, and an email arrive at the destination address whose subject starts with the form's name and whose Reply-To is the address you typed. If you want to see the raw responses, curl is quicker than the browser:
curl -s -X POST https://sendbeam.io/api/forms/YOUR_FORM_ID \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"you@example.com","message":"Testing the form"}'
# {"success":true,"message":"Thanks — your message has been sent. We'll reply by email.","redirect_url":null} The errors you are most likely to meet while setting up, and what they mean:
# Missing message on a contact form
# {"error":"A message is required"} HTTP 400
# Wrong origin when Allowed sites is set
# {"error":"This form does not accept submissions from this site."} HTTP 403
# Too many submissions from one visitor in a short window
# {"error":"Too many submissions. Please try again later."} HTTP 429
# Notification could not be delivered
# {"error":"Your message could not be sent right now. Please try again or email us directly."} HTTP 503 Each submission is also listed against the form in SendBeam, with whether the notification was delivered, so you can check there if an email seems to have gone missing.
Dealing with spam
Because there is no secret in the page, the endpoint protects itself in layers. The hidden field
in the markup above is one of them: a submission carrying the signs of a script rather than a
person is discarded. On top of that, every form is
rate-limited per visitor and capped per form per day, with the caps shown and adjustable on the
form's settings page. For anything more you switch on two settings in the
form's Protection section. Allowed sites makes SendBeam check the browser's
Origin header against the origins you list; browsers cannot forge it, so it
ends drive-by embedding, though a script can still fake it. Cloudflare Turnstile
closes that gap: add Cloudflare's script tag and a <div class="cf-turnstile"
data-sitekey="…"> inside the form, paste the site key and secret into SendBeam, and
every submission must carry a valid token or it is refused with a 403 before the message is
read. The script above already forwards the token when the widget is present. Turnstile needs
https://challenges.cloudflare.com allowed in script-src,
frame-src and connect-src if you have a CSP. See the
Cloudflare Pages guide for a complete
Turnstile setup with a header file.
Why not Formspree, Netlify Forms or Web3Forms?
All three do form-to-email well and you would not be wrong to pick one. Netlify Forms is the least effort if you host on Netlify and stay under 100 submissions a month; it does not exist anywhere else and detects forms at build time from the HTML, which occasionally surprises people using client-side frameworks. Formspree is host-independent and mature, priced per form and per submission. Web3Forms is the closest in spirit to this guide: an access key in the page, JSON or form-encoded posts, and a generous free tier.
What none of them do is anything after the email. If the same site also has a newsletter, or you want a welcome sequence, or you run four sites and want their forms, subscribers and sending domains in one account, that is the case for SendBeam. If a message in your inbox is the whole requirement, a forms-only service is the smaller and simpler tool. SendBeam's own constraints are the ones above: a JSON-only endpoint, the destination address rule, and the rate limits.
Next steps
- Do the same on a specific host, with Turnstile and a CSP header: Contact form backend for Cloudflare Pages.
- Framework versions of the form: the Astro, Next.js, Hugo and Eleventy integration pages, and the starter kits.
- Send notifications from your own domain instead of the shared address: Verify a sending domain with Cloudflare DNS.
- The full contract for the endpoint is under Forms in the API reference.
Starter kits
If you would rather start from something that already runs, the starter kits have this form wired up. The plain HTML one is this guide with nothing added to it:
- HTML and vanilla JS Plain HTML No API key needed · Any static host One page, one script, no build step: paste the files onto any host and both forms work. sendbeam-starters/plain-html
- Hugo Hugo No API key needed · Any static host Two partials and one script in static/, with the form IDs read from site params in hugo.toml. sendbeam-starters/hugo
- Eleventy Eleventy on Netlify No API key needed · Netlify Nunjucks includes and a global data file, with a netlify.toml that sets the Content-Security-Policy for you. sendbeam-starters/eleventy-netlify
- Astro Astro on Cloudflare Pages No API key needed · Cloudflare Pages Signup and contact components, form IDs in PUBLIC_ environment variables, and a _headers file that lets Turnstile load. sendbeam-starters/astro-cloudflare-pages
- Next.js (App Router) Next.js on Vercel No API key needed · Vercel Client components that post straight to the form endpoint, so no route handler and no secret sit in the middle. sendbeam-starters/nextjs-vercel
Every starter covers the same five things — newsletter signup, contact form, double opt-in, unsubscribe and spam protection — reads its form IDs from environment variables or site config, and is MIT licensed. Browse them all in sendbeam-starters.
Stuck, or found a gap? Ask in the community — questions, tips and every release note, with this page as the source of truth.