# 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/`. 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](https://sendbeam.io/docs/getting-started/sending-domain).
- A static site of any kind, hosted anywhere that serves HTML and JavaScript.

## Steps

1. **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](https://sendbeam.io/docs/forms/embedding); the rest
  of this guide is for when you want your own HTML.
2. **Write the page.** The form needs an `email` and a
  `message`; `name` and `subject` are optional. The
  `data-endpoint` attribute is where the script posts to. Replace
  `YOUR_FORM_ID` and the fallback mailto address.
  `
  
  
  
  
  Contact
  
  
  Contact
  
  
  
  Your name
  
  
  
  Your email
  
  
  
  Message
  
  
  
  
  
  Leave this empty
  
  
  Send
  
  Or email [hello@example.com](mailto:hello@example.com).
  
  
  
  
  `
3. **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-endpoint` attribute 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; });
  });
  });
  })();`
4. **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.
  `
  Or email [hello@example.com](mailto:hello@example.com).`
5. **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.
  `
  
  Email
  
  
  
  
  Leave this empty
  
  Subscribe
  
  `
6. **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`; add `https://www.example.com` too if both resolve).
  If your host sends a Content Security Policy header, add `https://sendbeam.io` to
  `connect-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 `` 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](https://sendbeam.io/docs/guides/contact-form-cloudflare-pages) 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](https://sendbeam.io/docs/guides/contact-form-cloudflare-pages).
- Framework versions of the form: the [Astro](https://sendbeam.io/integrations/astro), [Next.js](https://sendbeam.io/integrations/nextjs), [Hugo](https://sendbeam.io/integrations/hugo) and [Eleventy](https://sendbeam.io/integrations/eleventy) integration pages, and the [starter kits](https://github.com/sendbeam-io/sendbeam-starters).
- Send notifications from your own domain instead of the shared address: [Verify a sending domain with Cloudflare DNS](https://sendbeam.io/docs/guides/sending-domain-cloudflare-dns).
- The full contract for the endpoint is under Forms in the [API reference](https://sendbeam.io/docs/api).

## 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](https://github.com/sendbeam-io/sendbeam-starters/tree/main/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](https://github.com/sendbeam-io/sendbeam-starters/tree/main/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](https://github.com/sendbeam-io/sendbeam-starters/tree/main/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](https://github.com/sendbeam-io/sendbeam-starters/tree/main/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](https://github.com/sendbeam-io/sendbeam-starters/tree/main/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](https://github.com/sendbeam-io/sendbeam-starters).

---
Source: https://sendbeam.io/docs/guides/form-to-email-without-backend
