Cloudflare Pages setup
Connect SendBeam to Cloudflare Pages: what you need, the steps, and the code.
Via API
A form handler is the usual reason a Pages site ends up with a Function. With SendBeam the page posts to the form's own endpoint instead, so there is nothing server-side to write or maintain.
Before you start
- An account with Cloudflare Pages
- A SendBeam API key, or a form ID
- Somewhere to paste a snippet
Set it up
- Create the forms in SendBeam. Forms → New form. One Signup form (pick the list it feeds) and one Contact form (set the address messages go to). Copy each form ID from the form page.
- Put the IDs in your environment. The snippets read them from environment variables so the same code serves every site. Form IDs are meant to be public; protection is on the form, not in the ID.
- Add the snippets and deploy. Paste the components below, deploy to Cloudflare Pages, then submit each form once with an address you control and check Contacts and your inbox.
Code
Newsletter signup (plain HTML)
index.html
<form data-sendbeam data-endpoint="https://sendbeam.io/api/forms/YOUR_SIGNUP_FORM_ID">
<label for="nl-email">Email</label>
<input id="nl-email" name="email" type="email" required autocomplete="email" />
<!-- Hidden check field. Each form has its own name: copy it from the form's Embed tab in SendBeam. -->
<div style="position:absolute;left:-9999px" aria-hidden="true">
<input name="YOUR_FORM_CHECK_FIELD" type="text" tabindex="-1" autocomplete="off" />
</div>
<button type="submit">Subscribe</button>
</form>
<p data-status role="status" aria-live="polite" hidden></p>
<script src="/sendbeam-forms.js" defer></script>
Contact form (plain HTML)
contact.html
<form data-sendbeam data-endpoint="https://sendbeam.io/api/forms/YOUR_CONTACT_FORM_ID">
<label for="c-name">Name</label>
<input id="c-name" name="name" type="text" required />
<label for="c-email">Email</label>
<input id="c-email" name="email" type="email" required autocomplete="email" />
<label for="c-subject">Subject</label>
<input id="c-subject" name="subject" type="text" />
<label for="c-message">Message</label>
<textarea id="c-message" name="message" rows="5" required></textarea>
<div style="position:absolute;left:-9999px" aria-hidden="true">
<input name="YOUR_FORM_CHECK_FIELD" type="text" tabindex="-1" autocomplete="off" />
</div>
<!-- Optional: Cloudflare Turnstile. Paste the SECRET key into the form's Protection section in SendBeam. -->
<!-- <div class="cf-turnstile" data-sitekey="YOUR_TURNSTILE_SITE_KEY" data-size="flexible"></div> -->
<button type="submit">Send message</button>
</form>
<p data-status role="status" aria-live="polite" hidden></p>
<script src="/sendbeam-forms.js" defer></script>
<!-- Only when Turnstile is on: -->
<!-- <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> -->
The shared script
sendbeam-forms.js
// sendbeam-forms.js — posts any <form data-sendbeam> as JSON to SendBeam.
document.querySelectorAll('form[data-sendbeam]').forEach((form) => {
const endpoint = form.dataset.endpoint; // https://sendbeam.io/api/forms/<form-id>
const status = form.querySelector('[data-status]');
const button = form.querySelector('button[type="submit"]');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const data = Object.fromEntries(new FormData(form).entries());
const token = form.querySelector('[name="cf-turnstile-response"]');
if (token) data.turnstile_token = token.value;
button.disabled = true;
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const json = await res.json();
if (res.ok && json.success) {
form.hidden = true;
status.hidden = false;
status.textContent = json.message;
if (json.redirect_url) setTimeout(() => location.assign(json.redirect_url), 1500);
} else {
status.hidden = false;
status.textContent = json.error || 'Something went wrong. Please try again.';
if (window.turnstile) window.turnstile.reset();
}
} catch {
status.hidden = false;
status.textContent = 'Could not reach the server. Please try again.';
} finally {
button.disabled = false;
}
});
});
Headers file for CSP and Turnstile
public/_headers
# public/_headers (copied to the site root by your build)
/*
Content-Security-Policy: default-src 'self'; script-src 'self' https://challenges.cloudflare.com; connect-src 'self' https://sendbeam.io https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com; img-src 'self' data:; style-src 'self' 'unsafe-inline'
Only needed if you send a CSP. Pages reads _headers from the build output root; frameworks copy public/ there.
Things to know
- Build settings for the Astro starter: framework preset Astro, build command npm run build, output directory dist. Add the PUBLIC_ variables under Settings → Environment variables.
- No Pages Function, KV or D1 binding is involved; the request goes from the visitor’s browser to sendbeam.io.
- Turnstile widgets are created under Turnstile in the Cloudflare dashboard. The site key goes in your page, the secret key in the form’s Protection section in SendBeam.
- If your DNS is on Cloudflare too, the sending-domain card offers Connect with Cloudflare: approve the CNAME records at Cloudflare’s own login and they are added for you — SendBeam never sees a password or an API token. (Available once Cloudflare has enabled SendBeam’s Domain Connect template; until then, add the two records by hand, DNS-only.)
Stuck, or found a gap? Ask in the community — questions, tips and every release note, with this page as the source of truth.