# Connect Astro to SendBeam

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

Via API

Astro sites are static, and static sites have nowhere to put a form handler. SendBeam's public form endpoints fill that gap: a component posts straight to SendBeam, with no server code to keep alive.

## Before you start

- An account with Astro
- 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 your host, then submit each form once with an address you control and check Contacts and your inbox.

## Code

### Environment variables

`.env`

```
PUBLIC_SENDBEAM_SIGNUP_FORM_ID=your-signup-form-id
PUBLIC_SENDBEAM_CONTACT_FORM_ID=your-contact-form-id
# optional, only if Turnstile is on in the form's Protection section
PUBLIC_TURNSTILE_SITE_KEY=
```

Form IDs are public by design (they are in the page for every visitor); protection comes from the form’s allowed sites, rate limits and Turnstile, never from hiding the ID.

### Newsletter signup component

`src/components/NewsletterForm.astro`

```
---
// src/components/NewsletterForm.astro
const formId = import.meta.env.PUBLIC_SENDBEAM_SIGNUP_FORM_ID;
const endpoint = `https://sendbeam.io/api/forms/${formId}`;
---
<form data-sendbeam data-endpoint={endpoint} class="sb-form">
  <label for="nl-email">Email</label>
  <input id="nl-email" name="email" type="email" required autocomplete="email" />
  <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>
  // Astro bundles this once per page.
  document.querySelectorAll<HTMLFormElement>('form[data-sendbeam]').forEach((form) => {
    const status = form.nextElementSibling as HTMLElement;
    form.addEventListener('submit', async (e) => {
      e.preventDefault();
      const data: Record<string, unknown> = Object.fromEntries(new FormData(form).entries());
        const res = await fetch(form.dataset.endpoint!, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });
      const json = await res.json();
      status.hidden = false;
      status.textContent = res.ok && json.success ? json.message : (json.error || 'Something went wrong.');
      if (res.ok && json.success) form.hidden = true;
    });
  });
</script>
```

### Contact form component

`src/components/ContactForm.astro`

```
---
// src/components/ContactForm.astro
const formId = import.meta.env.PUBLIC_SENDBEAM_CONTACT_FORM_ID;
const siteKey = import.meta.env.PUBLIC_TURNSTILE_SITE_KEY; // optional
const endpoint = `https://sendbeam.io/api/forms/${formId}`;
---
<form data-sendbeam data-endpoint={endpoint} class="sb-form">
  <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-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>
  {siteKey && <div class="cf-turnstile" data-sitekey={siteKey} data-size="flexible"></div>}
  <button type="submit">Send message</button>
</form>
<p data-status role="status" aria-live="polite" hidden></p>
{siteKey && <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>}

<script>
  document.querySelectorAll<HTMLFormElement>('form[data-sendbeam]').forEach((form) => {
    const status = form.nextElementSibling as HTMLElement;
    form.addEventListener('submit', async (e) => {
      e.preventDefault();
      const data: Record<string, unknown> = Object.fromEntries(new FormData(form).entries());
        const token = form.querySelector<HTMLInputElement>('[name="cf-turnstile-response"]');
      if (token) data.turnstile_token = token.value;
      const res = await fetch(form.dataset.endpoint!, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });
      const json = await res.json();
      status.hidden = false;
      status.textContent = res.ok && json.success ? json.message : (json.error || 'Something went wrong.');
      if (res.ok && json.success) form.hidden = true;
      else (window as any).turnstile?.reset();
    });
  });
</script>
```

The Turnstile script is an ordinary external script tag, which Astro leaves untouched. The bundled script posts the form as JSON; SendBeam applies its own submission checks server-side.

## Things to know

- Works with static output and with SSR adapters alike; nothing runs on your server.
- On Cloudflare Pages set the PUBLIC_ variables under Settings → Environment variables, then add public/_headers if you send a Content-Security-Policy (see the Cloudflare Pages page).
- On Netlify or Vercel the same variables go in the site’s environment settings; PUBLIC_ prefixed values are inlined at build time.
- Put the site’s origin ([https://www.example.com](https://www.example.com)) under the form’s Allowed sites so the endpoint only answers your pages.

[← Astro email marketing integration](https://sendbeam.io/integrations/astro)

---
Source: https://sendbeam.io/docs/integrations/astro
