# Connect Vercel to SendBeam

How to connect Vercel to SendBeam: what you need, the setup 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

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 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.

[← Vercel email marketing integration](https://sendbeam.io/integrations/vercel)

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