# Connect Next.js to SendBeam

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

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

[← Next.js email marketing integration](https://sendbeam.io/integrations/nextjs)

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