# Lovable Contact Form Not Sending Email: Why and How to Fix It

Why Lovable contact forms often show a success message without sending an email, and how to fix it with Lovable Emails, Resend or a form backend on any plan.

Published 2026-10-30.

**Short answer:** Lovable's submit handler usually only shows a "Message sent!" toast and sends nothing. Ask Lovable to post the form to a form backend endpoint instead. It takes one prompt and works on Lovable's Free plan. Lovable Emails and the Resend connector also work, but both need a domain verified with DNS records, and Lovable Emails needs a paid plan.

```text title="Prompt for Lovable"
My contact form shows a success message but doesn't send anything. Change it so it sends every submission to https://api.nisuform.com/s/YOUR_FORM_KEY.

- Send the form from the browser with fetch, as a POST with the header Accept: application/json. Use FormData and don't set Content-Type.
- Keep the field names name, email and message.
- Add a text input named _gotcha with tabIndex -1 and autoComplete off, hidden with the Tailwind class hidden, not type="hidden". Leave it empty.
- Add a field named _elapsed with the milliseconds between the form appearing and the submit.
- Read the JSON response. Show the success toast only when ok is true. When ok is false, show the message from the response as an error toast and keep what the visitor typed.
- Disable the submit button while the request is in flight.
- Don't add a database table, an edge function, a server function or an email API, and don't change the design.

Follow the Nisuform docs at https://nisuform.com/llms-full.txt.
```

Replace `YOUR_FORM_KEY` with the key from a free [Nisuform](https://nisuform.com) form, paste the prompt into Lovable's chat, and send yourself a test from the preview. Every message then lands in your inbox and your email.

## Why your Lovable contact form isn't sending email

Lovable writes a React app, and a form in it only sends what people type if you ask for that. Prompt "add a contact page" and the submit handler often looks like this:

```tsx title="What a Lovable contact form often does"
const handleSubmit = (e: React.FormEvent) => {
  e.preventDefault()
  toast({
    title: 'Message sent!',
    description: "We'll get back to you soon.",
  })
  setFormData({ name: '', email: '', message: '' })
}
```

The toast appears, the fields clear and the message is gone. Lovable's docs don't describe a form feature, so there is no endpoint, inbox or notification email behind that toast.

### Find out which problem you have

Open your published site, or the preview in a new tab, and go to the **Network** tab in your browser's developer tools. Send the form and see what appears:

| What you see                                   | What it means                                                                                           |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| No new request                                 | The submit handler only shows a toast. Nothing was sent.                                                |
| A request to your app's database that succeeds | The message was saved, but nothing emails you about it.                                                 |
| A request to a function that fails             | There is an email step, and it breaks. Usually a secret is missing or the sender domain isn't verified. |
| A request that succeeds, but no email arrives  | The email left from an address that can't reach you, or it went to spam.                                |

### Why the email step fails

If Lovable already tried to send email, one of these is usually the cause:

* **Saved, never sent.** Lovable stores the message in a table on its built-in backend, but nothing sends an email after the save.
* **Sending from `onboarding@resend.dev`.** That is Resend's shared test address, and it only delivers to the email address of the Resend account owner. A notification to any other address never arrives.
* **No verified domain.** Lovable Emails and Resend both send only from a domain you verified with DNS records. Lovable's Resend connector doesn't set up DNS or verify domains for you.
* **A missing secret.** If Lovable wrote a function that reads `RESEND_API_KEY`, it fails on every submit until that secret is set.
* **The Free plan.** Lovable Emails needs a paid plan. On Free, the only emails Lovable sends are sign-in emails, from a generic Lovable sender.
* **An older React + Vite project without the backend.** In those projects, app emails run on Lovable's built-in backend, so it has to be turned on.

## Three ways to make a Lovable form send email

|                     | Form backend                                    | Lovable Emails                                        | Resend connector                                 |
| ------------------- | ----------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------ |
| What you need       | A free Nisuform account                         | A paid Lovable plan and a domain whose DNS you manage | A Resend account and a domain verified in Resend |
| Server code         | None, the browser posts the form                | An app email Lovable writes                           | A function Lovable writes to call Resend         |
| Keeps every message | Yes, in an inbox                                | Only if Lovable also saves it to a table              | Only if Lovable also saves it to a table         |
| Spam protection     | Built in                                        | Whatever Lovable writes                               | Whatever Lovable writes                          |
| Cost                | Free for 100 a month, Pro $10 a month for 5,000 | 50,000 emails a month included with paid workspaces   | Billed by Resend                                 |

### 1. Post the form to a form backend

This is the prompt above. The browser sends the form straight to an endpoint, and the form backend stores it, filters spam and emails you. Lovable only changes the submit handler, so there is no server code, table or secret. It works on the Free plan, in the preview, and in TanStack Start and React + Vite projects. The form key is public by design, so it can sit in your app's code.

Nisuform, Formspree and Web3Forms all work this way, and the Lovable code is the same for any of them. If you are still choosing, see [the best form backends compared](/blog/best-form-backend).

### 2. Lovable Emails

Lovable's built-in emails go out from a subdomain of a domain you own, `notify` by default, and Lovable handles the SPF, DKIM and DMARC records and the delivery. You need a paid plan and access to the domain's DNS. On Pro, a workspace can send up to 100 app emails an hour.

Lovable's docs describe app emails as transactional, like order confirmations, and don't mention contact forms. If you go this way, ask Lovable to save each message to a table as well as email it, or a failed email loses the message. It makes sense when you already pay for Lovable and want other emails from the same domain, like receipts or sign-in links.

### 3. The Resend connector

The connector sends through your own Resend account, from a domain you verified in Resend, and Resend bills you for the email. Lovable writes the function that calls it on each submit, and you maintain the function, the key, the domain, the spam checks and the rate limits. The function has a public URL, so anyone who finds it can call it as often as they like, and every call sends an email.



## Fix it with one prompt

### Create a form and copy its endpoint

Sign in at [app.nisuform.com](https://app.nisuform.com), choose **Create a form**, then **Endpoint only**. The endpoint is on the form's **Setup** tab and looks like `https://api.nisuform.com/s/YOUR_FORM_KEY`. Under &#x2A;*How do you build your site?**, choose **AI tool**, then **Your own styles**, to get a prompt with your endpoint and field names already filled in.

### Paste the prompt into Lovable

Use the prompt at the top of this guide or the one from the dashboard. Only the submit handler changes, and the design stays. If your form has more fields, like a phone number or a budget, list their names in the prompt. Nisuform keeps every field the form sends.

### Check what Lovable wrote

Compare the form's code with [the component below](#the-code-lovable-should-end-up-with). Look for a `POST` to your endpoint, a `name` on every input, the hidden `_gotcha` input, and a success toast that only shows when the response says `ok`.

### Send a test from the preview

Fill in the form, wait at least 2 seconds and press send, because faster submissions are dropped as bots. The message should show up in your Nisuform inbox and your email within seconds. Keep to 5 test submissions a minute, the rate limit for one IP address on one form.

### Publish, then lock the form to your site

Publish the update, because your live site doesn't change until you do. Then add your live address to the form's [allowed websites](/docs/protection/access-rules), so other sites can't post to your endpoint. [Test in the preview, then publish](#test-in-the-preview-then-publish) has the details.

## The code Lovable should end up with

Lovable projects usually come with shadcn/ui components in `src/components/ui` and a toast library. A working version of the form, whether Lovable writes it or you paste it yourself, looks like this:

```tsx title="src/components/ContactForm.tsx"
import { useEffect, useRef, useState, type FormEvent } from 'react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'

const endpoint = 'https://api.nisuform.com/s/YOUR_FORM_KEY'

export function ContactForm() {
  const shownAt = useRef(0)
  const [sending, setSending] = useState(false)

  useEffect(() => {
    shownAt.current = Date.now()
  }, [])

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    const form = event.currentTarget
    const formData = new FormData(form)
    formData.set('_elapsed', String(Date.now() - shownAt.current))
    setSending(true)
    try {
      const response = await fetch(endpoint, {
        method: 'POST',
        headers: { Accept: 'application/json' },
        body: formData,
      })
      const result = await response.json()
      if (result.ok) {
        toast.success('Thanks! We got your message.')
        form.reset()
      } else {
        toast.error(result.message)
      }
    } catch {
      toast.error('Something went wrong. Please try again.')
    } finally {
      setSending(false)
    }
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <div className="space-y-2">
        <Label htmlFor="name">Name</Label>
        <Input id="name" name="name" autoComplete="name" required />
      </div>
      <div className="space-y-2">
        <Label htmlFor="email">Email</Label>
        <Input id="email" name="email" type="email" autoComplete="email" required />
      </div>
      <div className="space-y-2">
        <Label htmlFor="message">Message</Label>
        <Textarea id="message" name="message" rows={5} required />
      </div>
      <input type="text" name="_gotcha" tabIndex={-1} autoComplete="off" className="hidden" />
      <Button type="submit" disabled={sending}>
        {sending ? 'Sending...' : 'Send message'}
      </Button>
    </form>
  )
}
```

What each part does:

* **It posts from the browser.** Event handlers only run in the browser, so the form works the same in TanStack Start, which renders the page on the server first, and in older React + Vite projects. Your app's server never sees the request.
* **`Accept: application/json`** asks Nisuform for JSON instead of its thank-you page. Leave out `Content-Type` when the body is `FormData`, because the browser sets it with the boundary it needs.
* **The response decides the toast.** A stored submission answers `{ "ok": true, "submissionId": "..." }`. A rejected one answers with a code and a message, like `{ "ok": false, "error": "rate_limited", "message": "Too many requests. Try again shortly" }`. `message` is written for people, so the error toast shows it as is. Every code is listed in [Errors](/docs/reference/errors).
* **`event.currentTarget` is read first.** It is `null` once the event finishes, so the form is saved to a variable before the first `await`.
* **Errors keep what the visitor typed.** With `onSubmit` and `preventDefault`, React doesn't clear the fields for you. Only a successful send calls `form.reset()`.

If your project shows toasts with the `useToast` hook instead of sonner, keep the one it already has. The success and error branches stay the same.

## If your form uses react-hook-form and zod

Lovable often builds forms with shadcn/ui's `Form` components, react-hook-form and zod. Keep them for validation and only replace the function you pass to `handleSubmit`:

```tsx title="src/components/ContactForm.tsx"
async function onSubmit(values: ContactValues, event?: React.BaseSyntheticEvent) {
  const formData = new FormData(event?.target)
  formData.set('_elapsed', String(Date.now() - shownAt.current))
  try {
    const response = await fetch(endpoint, {
      method: 'POST',
      headers: { Accept: 'application/json' },
      body: formData,
    })
    const result = await response.json()
    if (!result.ok) {
      toast.error(result.message)
      return
    }
    toast.success('Thanks! We got your message.')
    form.reset()
  } catch {
    toast.error('Something went wrong. Please try again.')
  }
}
```

react-hook-form calls `onSubmit` only after zod has accepted the fields, and passes the submit event second. The body is built from the form element rather than from `values`, because zod drops fields that aren't in its schema, and the honeypot has to go along. Keep the plain `_gotcha` input inside the `<form>`, and define `shownAt` and `endpoint` as in the component above.

## Test in the preview, then publish

The preview is a real browser page, so submissions from it are real.

* **Leave allowed websites empty while you build.** Each draft's preview runs at its own address, so a list with only your live site would reject posts from the preview.
* **Add your live addresses after you publish.** Lovable publishes to an address like `https://your-app.lovable.app`. Add it to the form's allowed websites, plus your custom domain if you have one. `https://example.com` and `https://www.example.com` are different sites, so add both if your site answers on both.
* **Publish again after every fix.** The live site keeps serving your last published version, so a form that works in the preview can still be the old one on your domain.
* **Local dev servers count too.** If you synced the project to GitHub and run it on your machine, add the address your dev server prints, like `http://localhost:8080`.

## Stop spam without a CAPTCHA

The component already sends two invisible checks:

* **Honeypot.** The `_gotcha` input is hidden with the `hidden` class, which sets `display: none`. People leave it empty, and bots that fill in every input get dropped. Don't use `type="hidden"`, because bots skip hidden inputs.
* **Fill time.** `_elapsed` is the number of milliseconds between the form appearing and the submit. Anything under 2 seconds is dropped.

Bots caught by either check still get `{ "ok": true }`, so they can't tell what gave them away. Nisuform also rate limits each visitor, sends submissions stuffed with links to a spam folder, and can reject posts from any site that isn't on your form's [allowed websites](/docs/protection/access-rules). If a form still gets hit, add [Cloudflare Turnstile](/docs/protection/turnstile).

[How to add a contact form to a static website without a backend](/blog/html-forms-without-backend) goes deeper on every check and when each one is worth it.

## Send messages to Slack, Discord or Google Sheets

Email is only the default. On Pro, the same Lovable form can post each submission to [Slack, Discord, Telegram or Google Sheets](/docs/integrations), or to your own code with a [webhook](/docs/integrations/webhooks), and send the visitor an [auto-reply](/docs/submissions/auto-replies). None of it needs another prompt in Lovable.

## Checklist before you publish

1. `YOUR_FORM_KEY` is replaced with your form's key.
2. Pressing send shows a `POST` to `api.nisuform.com` in the Network tab.
3. The success toast only appears when the response has `"ok": true`.
4. Every input has a `name`, and the names match the fields in your form builder.
5. The honeypot is hidden with CSS, not `type="hidden"`.
6. Errors keep what the visitor typed. Test it by going offline in DevTools and pressing send.
7. A test from the preview reached your inbox and your email.
8. You published the update, and your live address is on the form's allowed websites.

For the same fix outside Lovable, see [the React contact form guide](/blog/react-contact-form), [the Next.js contact form guide](/blog/nextjs-contact-form) and the [AI coding agents docs](/docs/ai-agents), which have prompts for Cursor, Claude Code, v0 and Bolt.

## Frequently asked questions

### Why is my Lovable contact form not sending email?

Usually the form never sends anything. The submit handler Lovable writes shows a toast like 'Message sent!' and clears the fields, but no request leaves the browser, and Lovable has no inbox for form messages. Open your browser's Network tab and press send. If no request appears, ask Lovable to post the form to a form backend endpoint, or to add an email step with Lovable Emails or Resend.

### Can Lovable send emails?

Yes, with some setup. Lovable Emails sends transactional emails, like order confirmations, from a domain you own once you verify it with DNS records, and it needs a paid plan. The Resend connector sends through your own Resend account, from a domain you verified in Resend. On the Free plan, sign-in emails come from a generic Lovable sender. A contact form needs neither, because a form backend emails you every submission on any Lovable plan.

### Does Lovable have a built-in contact form backend?

No. Lovable builds the form you see, and its built-in backend can save each message to a database table, but Lovable has no form inbox, spam filter or notification email for form messages. You get those by asking Lovable to set up Lovable Emails or Resend, or by posting the form to a form backend, which stores, filters and emails every submission for you.

### Why does Resend only send emails to me from my Lovable app?

You are sending from onboarding@resend.dev, Resend's shared test address. It only delivers to the email address of the Resend account owner. To email anyone else, verify your own domain in Resend, add the DNS records it gives you, and send from an address on that domain. Lovable's Resend connector doesn't verify domains for you.

### Do I need Lovable Cloud or Supabase for a contact form?

No. If the form posts straight from the browser to a form backend endpoint, your app doesn't need a database table, an edge function, a server function or a secret. The form backend stores each message and emails you. You only need Lovable's built-in backend if you also want messages in your own database or code that runs on each submit.

### Is it safe to put a form endpoint in my Lovable app's code?

Yes. A Nisuform endpoint only accepts new submissions. Nobody can read your messages or change your form with it, so it doesn't need to be a secret. An email API key like RESEND_API_KEY is different, because anyone who has it can send email as you, so it must stay in server code. To stop other sites from posting to your endpoint, add your published Lovable address to the form's allowed websites.

### Does the fix work with TanStack Start and older React + Vite projects?

Yes. New Lovable projects have used TanStack Start since May 13, 2026, and older ones use React + Vite. Both are React, and the form posts from the browser, so the same prompt and the same component work in either. Nothing runs on your app's server when someone presses send.

### Why does my Lovable form work in the preview but not on my live site?

Your live site only changes when you publish again, so publish after Lovable fixes the form. If the live form shows an origin_not_allowed error, the form's allowed websites don't include the live address. Add your lovable.app address and any custom domain, with and without www if your site answers on both.
