# Gatsby contact form that sends email (no functions)

> A Gatsby contact page that emails you through your own SMTP: the key in a GATSBY_ env variable, fetch() with loading states, a thank-you page, no functions.

Source: https://formsmtp.com/guides/gatsby-contact-form · Last updated November 3, 2026

## 1. Create a FormSMTP project

[Sign up free](https://app.formsmtp.com/#/signup) and add a project. Pick your email provider so the SMTP host and port fill in, enter your login, and click *Test connection*. Not sure which provider? Your existing mailbox works: see the [Gmail](https://formsmtp.com/guides/gmail-smtp-contact-form) or [Zoho](https://formsmtp.com/guides/zoho-smtp-contact-form) guides.

## 2. Lock the project to your site

In *Settings → Security*, set **Allowed websites** to `https://yoursite.com` and **Allowed recipients** to your own address. Now the key in your page can only email you, only from your site, and only up to your hourly limit.

`gatsby develop` serves on `http://localhost:8000` by default. Add it to Allowed websites while you test, alongside your live domain.

## 3. Add the key as a GATSBY_ variable

Gatsby only exposes environment variables to browser code when their names start with `GATSBY_`. It loads `.env.development` for `gatsby develop` and `.env.production` for `gatsby build`:

```bash
# .env.development and .env.production
GATSBY_FORMSMTP_FORM_KEY=fk_your_key
```

The value is compiled into your JavaScript, so restart `gatsby develop` or rebuild after changing it. On Netlify or another host, set the same variable in the site’s build settings. The form key is meant to be public; never give the `GATSBY_` prefix to your API key (`fs_`).

## 4. Create the contact page

```jsx
// src/pages/contact.js
import * as React from "react";
import { navigate } from "gatsby";

const ENDPOINT = "https://app.formsmtp.com/api/v1/form/" + process.env.GATSBY_FORMSMTP_FORM_KEY;

export default function ContactPage() {
  const [status, setStatus] = React.useState("idle"); // idle | sending | error
  const [error, setError] = React.useState("");
  const loadedAt = React.useRef(Date.now());

  async function handleSubmit(event) {
    event.preventDefault();
    setStatus("sending");
    const fields = Object.fromEntries(new FormData(event.currentTarget));
    try {
      const res = await fetch(ENDPOINT, {
        method: "POST",
        headers: { "Content-Type": "application/json", Accept: "application/json" },
        body: JSON.stringify({ ...fields, _subject: "New message from my Gatsby site", _t: loadedAt.current }),
      });
      const data = await res.json();
      if (!data.success) throw new Error(data.error);
      navigate("/thanks/");
    } catch (err) {
      setError(err.message);
      setStatus("error");
    }
  }

  return (
    <main>
      <h1>Contact</h1>
      <form method="POST" action={ENDPOINT} onSubmit={handleSubmit}>
        <label>Name <input name="name" autoComplete="name" required /></label>
        <label>Email <input name="email" type="email" autoComplete="email" required /></label>
        <label>Message <textarea name="message" rows={5} required /></label>
        <input type="hidden" name="_next" value="/thanks/" />
        <input name="_gotcha" style={{ display: "none" }} tabIndex={-1} autoComplete="off" />
        <button disabled={status === "sending"}>{status === "sending" ? "Sending…" : "Send"}</button>
        {status === "error" && <p role="alert">{error}</p>}
      </form>
    </main>
  );
}

export const Head = () => <title>Contact</title>;
```

Because Gatsby renders this page to HTML at build time, the `action` and `method` attributes make the form work before React has loaded, or with JavaScript off: it posts normally and FormSMTP redirects to `_next`. Once React is running, `handleSubmit` takes over, sends JSON and uses Gatsby’s `navigate`. `_t` is the time the page loaded; FormSMTP drops submissions that arrive within two seconds of it.

## 5. Add the thank-you page

```jsx
// src/pages/thanks.js
import * as React from "react";
import { Link } from "gatsby";

export default function ThanksPage() {
  return (
    <main>
      <h1>Thanks!</h1>
      <p>Your message is on its way. We'll reply within a day or two.</p>
      <Link to="/">Back to the homepage</Link>
    </main>
  );
}

export const Head = () => (
  <>
    <title>Thanks</title>
    <meta name="robots" content="noindex" />
  </>
);
```

Run `gatsby build`, deploy, and send a test message from your live site. It arrives from your own address and appears under *Logs*.

## What about Gatsby Functions?

If your host runs Gatsby Functions, you can keep a key on the server instead: files in `src/api` become endpoints, and environment variables without the `GATSBY_` prefix never reach the browser. That route uses the secret API key and the full send API:

```js
// src/api/contact.js
export default async function handler(req, res) {
  if (req.method !== "POST") return res.status(405).end();
  const { name, email, message } = req.body;
  const r = await fetch("https://app.formsmtp.com/api/v1/send", {
    method: "POST",
    headers: {
      Authorization: "Bearer " + process.env.FORMSMTP_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      to: "you@yourdomain.com",
      replyTo: email,
      subject: "New message from " + name,
      text: String(message),
    }),
  });
  const data = await r.json();
  if (!data.success) {
    console.error("FormSMTP:", data.error); // the details stay in your logs
    return res.status(502).json({ ok: false, error: "Sorry, your message couldn't be sent. Please try again later." });
  }
  res.json({ ok: true });
}
```

Functions need a host that runs them. On a purely static host such as GitHub Pages or an S3 bucket, use the page component above: it needs nothing but static files.

## Troubleshooting

- **The request goes to `…/form/undefined`:** the variable is missing the `GATSBY_` prefix, isn’t in the `.env` file for the command you ran, or you didn’t restart after adding it.
- **“This form isn’t allowed on http://localhost:8000”:** add the dev origin to Allowed websites, or test the production build on your domain.
- **“The form has no fields”:** the inputs have no `name` attributes, so `FormData` is empty.
- **Works locally, fails after deploy:** the host’s build didn’t have `GATSBY_FORMSMTP_FORM_KEY` set, or your live domain isn’t in Allowed websites.

## FAQ

### Why does the variable need the GATSBY_ prefix?

Gatsby keeps other environment variables out of browser code so secrets don’t leak by accident. The form key is safe to expose; your API key is not, so never prefix that one.

### Can I use Formik or React Hook Form?

Yes. Keep your form library for state and validation, and make the same fetch() call in its submit handler.

### Does it work on GitHub Pages, Netlify or S3?

Yes. The page posts directly to FormSMTP from the browser, so any host that serves Gatsby’s build output works.

### Why did my message land in spam?

Usually the From address and the SMTP account don’t match, or the domain has no SPF/DKIM records. Send from the address you log in with, and check the Deliverability panel in FormSMTP’s Settings.

## More guides

- [Next.js contact form with email (App Router)](https://formsmtp.com/guides/nextjs-contact-form)
- [Astro contact form that sends email](https://formsmtp.com/guides/astro-contact-form)
- [React contact form that sends email (Vite, CRA, any React)](https://formsmtp.com/guides/react-contact-form)
- [Vue contact form that sends email (Vue 3 & Nuxt)](https://formsmtp.com/guides/vue-contact-form)
