Vercel setup
Connect SendBeam to Vercel: what you need, the steps, and the code.
Via API
A contact form is often the first thing that tempts a Vercel project into a serverless function and an email API key. SendBeam's public endpoints let the browser post directly, so there is neither.
Before you start
- An account with Vercel
- 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 Vercel, then submit each form once with an address you control and check Contacts and your inbox.
Code
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>
);
}
vercel.json with CSP
vercel.json
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Content-Security-Policy", "value": "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'" }
]
}
]
}
If you set headers in next.config.js instead, use the same Content-Security-Policy value there.
Things to know
- Set NEXT_PUBLIC_SENDBEAM_SIGNUP_FORM_ID and NEXT_PUBLIC_SENDBEAM_CONTACT_FORM_ID under Project → Settings → Environment Variables for Production and Preview, then redeploy: they are inlined at build time.
- Preview deployments have their own *.vercel.app origins; allow them on the form while testing or keep Allowed sites empty until launch.
- Static exports (output: “export”) work the same way, because nothing depends on the Node runtime.
Stuck, or found a gap? Ask in the community — questions, tips and every release note, with this page as the source of truth.