Next.js setup
Connect SendBeam to Next.js: what you need, the steps, and the code.
Via API
A Next.js app can proxy forms through a route handler, but that is one more piece of server code to keep alive. SendBeam's public endpoints take the browser's request directly, so a client component is enough.
Before you start
- An account with Next.js
- 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.local
NEXT_PUBLIC_SENDBEAM_SIGNUP_FORM_ID=your-signup-form-id
NEXT_PUBLIC_SENDBEAM_CONTACT_FORM_ID=your-contact-form-id
NEXT_PUBLIC_TURNSTILE_SITE_KEY=
Newsletter signup component
components/NewsletterForm.js
'use client';
// components/NewsletterForm.js
import { useState } from 'react';
const ENDPOINT = `https://sendbeam.io/api/forms/${process.env.NEXT_PUBLIC_SENDBEAM_SIGNUP_FORM_ID}`;
export default function NewsletterForm() {
const [state, setState] = useState({ status: 'idle', message: '' });
async function onSubmit(e) {
e.preventDefault();
const form = e.currentTarget;
const data = Object.fromEntries(new FormData(form).entries());
setState({ status: 'sending', message: '' });
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) setState({ status: 'done', message: json.message });
else setState({ status: 'error', message: json.error || 'Something went wrong.' });
} catch {
setState({ status: 'error', message: 'Could not reach the server.' });
}
}
if (state.status === 'done') return <p role="status">{state.message}</p>;
return (
<form onSubmit={onSubmit}>
<label htmlFor="nl-email">Email</label>
<input id="nl-email" name="email" type="email" required autoComplete="email" />
<div style={{ position: 'absolute', left: -9999 }} aria-hidden="true">
<input name="YOUR_FORM_CHECK_FIELD" type="text" tabIndex={-1} autoComplete="off" />
</div>
<button type="submit" disabled={state.status === 'sending'}>Subscribe</button>
{state.status === 'error' && <p role="alert">{state.message}</p>}
</form>
);
}
Contact form component
components/ContactForm.js
'use client';
// components/ContactForm.js — same pattern, contact-form fields.
import { useState } from 'react';
const ENDPOINT = `https://sendbeam.io/api/forms/${process.env.NEXT_PUBLIC_SENDBEAM_CONTACT_FORM_ID}`;
export default function ContactForm() {
const [state, setState] = useState({ status: 'idle', message: '' });
async function onSubmit(e) {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget).entries());
const token = e.currentTarget.querySelector('[name="cf-turnstile-response"]');
if (token) data.turnstile_token = token.value; // only when Turnstile is on the page
setState({ status: 'sending', message: '' });
const res = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const json = await res.json().catch(() => ({}));
if (res.ok && json.success) setState({ status: 'done', message: json.message });
else setState({ status: 'error', message: json.error || 'Something went wrong.' });
}
if (state.status === 'done') return <p role="status">{state.message}</p>;
return (
<form onSubmit={onSubmit}>
<label htmlFor="c-name">Name</label>
<input id="c-name" name="name" type="text" required />
<label htmlFor="c-email">Email</label>
<input id="c-email" name="email" type="email" required autoComplete="email" />
<label htmlFor="c-message">Message</label>
<textarea id="c-message" name="message" rows={5} required />
<div style={{ position: 'absolute', left: -9999 }} aria-hidden="true">
<input name="YOUR_FORM_CHECK_FIELD" type="text" tabIndex={-1} autoComplete="off" />
</div>
<button type="submit" disabled={state.status === 'sending'}>Send message</button>
{state.status === 'error' && <p role="alert">{state.message}</p>}
</form>
);
}
To add Turnstile, render
inside the form and load https://challenges.cloudflare.com/turnstile/v0/api.js with next/script (strategy=“afterInteractive”).Things to know
- If you would rather keep the form ID out of the client, a route handler can forward the JSON to the same endpoint; send the visitor’s Origin along or leave Allowed sites empty, because server-side calls carry no Origin header.
- On Vercel add the NEXT_PUBLIC_ variables under Project → Settings → Environment Variables and redeploy; they are inlined at build time.
- Turbopack and the React Compiler are fine: the components have no dependencies beyond React.
Stuck, or found a gap? Ask in the community — questions, tips and every release note, with this page as the source of truth.