Astro setup
Connect SendBeam to Astro: what you need, the 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
- 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 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) under the form’s Allowed sites so the endpoint only answers your pages.
Stuck, or found a gap? Ask in the community — questions, tips and every release note, with this page as the source of truth.