# Connect Cloudflare Pages to SendBeam

How to connect Cloudflare Pages to SendBeam: what you need, the setup steps, and the code.

CP

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

1. **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.
2. **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.
3. **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.)

[← Cloudflare Pages email marketing integration](https://sendbeam.io/integrations/cloudflare-pages)

---
Source: https://sendbeam.io/docs/integrations/cloudflare-pages
