# Framer form to email: webhook or code component

> Send Framer form submissions to your inbox from your own address: Framer’s webhook destination or a code component form, and the trade-offs of each one.

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

## What Framer forms do on their own

Framer’s native forms can send submissions to email, Google Sheets or a custom webhook, and Framer adds its own spam protection and rate limiting. For anything else, Framer offers an **Embed** component for third-party HTML and **Code Components**: React components you write in Framer’s code editor that run on your published site.

The webhook destination is the useful one here. Framer sends each submission to your URL as a JSON `POST`, using the input names as keys. Point it at FormSMTP and the submission becomes an email sent through your own SMTP account.

## Webhook or code component?

|  | Send To → Webhook | Code component |
| --- | --- | --- |
| **Setup** | No code. Keeps Framer’s form design and states. | One code file, then drag it onto the page. |
| **Who sends the request** | Framer’s servers | The visitor’s browser |
| **Per-visitor limits** | All visitors share one limit, because every request comes from Framer | Each visitor is counted on their own |
| **Honeypot and time trap** | Not available. Framer’s own spam protection applies instead. | Included |
| **Allowed websites** | Not checked (no browser Origin) | Checked |

For a low-traffic portfolio or small business site, the webhook is fine in a project of its own (below). For a busy form, or one that attracts bots, use the code component.

## 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*. Any mailbox with SMTP access works: [Gmail or Google Workspace](https://formsmtp.com/guides/gmail-smtp-contact-form), [Microsoft 365](https://formsmtp.com/guides/outlook-smtp-contact-form), [Zoho](https://formsmtp.com/guides/zoho-smtp-contact-form), your web host’s mailbox, or a sending service. Personal Outlook.com and Hotmail addresses can’t be used, because Microsoft only lets them sign in with OAuth.

## 2. Lock it to your site

In *Settings → Security*, set **Allowed websites** to your published Framer address and your custom domain, each written out in full (for example `https://www.yourdomain.com`), and **Allowed recipients** to your own address. Then copy the **form key** from *API & code*. It starts with `fk_`, it only works on the form endpoint, and it can only email you, so it’s safe in your page.

Allowed websites only matters for the code component; the webhook route doesn’t send a browser Origin. Allowed recipients protects both. For the webhook route, use a project just for this form and keep its form key only in Framer’s webhook settings, never in page code: without an Origin, the key itself is what keeps others out.

## 3. Option A: Send To → Webhook

1. Select the form on the canvas.
2. In the right sidebar, click **Add…** next to *Send To* and choose **Webhook**.
3. Enter `https://app.formsmtp.com/api/v1/form/YOUR_FORM_KEY` as the URL.

Name your inputs `name`, `email` and `message`. The input called `email` becomes the reply-to address, so you can answer from your inbox. To set the subject line, add one more input, turn on its **Hidden** property, name it `_subject` and give it a value. Use *Hidden*, not the *Visible* toggle: Framer leaves invisible inputs out of the submission entirely.

Framer then sends something like this, which FormSMTP emails to you:

```http
POST https://app.formsmtp.com/api/v1/form/fk_…
Content-Type: application/json

{
  "name": "Ana Silva",
  "email": "ana@example.com",
  "message": "Do you have availability in March?",
  "_subject": "New message from my Framer site"
}
```

Framer also lets you set a signing secret for the webhook. You can leave it empty: FormSMTP identifies your project by the form key, not by Framer’s signature header.

Every webhook request comes from Framer’s servers, so FormSMTP sees all your visitors as one. Its per-visitor limits are 5 submissions a minute and, by default, 10 an hour. Raise **Form submissions per visitor per hour** in *Settings → Spam protection* only as far as your real traffic needs, for example 30 an hour for a small site, and keep **Max API emails per hour** modest too: it caps everything the project sends, so a leaked key can’t flood your inbox or use up your mailbox’s daily sending limit. The 5-a-minute cap stays. Over the limit, FormSMTP answers with an error and Framer retries the request, up to 5 times.

FormSMTP only checks Allowed websites when a request carries the Origin header that browsers add, and server-to-server webhooks normally don’t, so this route relies on the form key and your recipient lock. The spam filter for link-stuffed messages and duplicates still runs.

## 4. Option B: A code component form

In the Assets panel, select **Code**, click **Create Code File**, and replace its contents with this component. It posts from the visitor’s browser, so each visitor gets their own limit, and the honeypot and time trap work.

```tsx
import { useRef, useState } from "react"
import { addPropertyControls, ControlType } from "framer"

const ENDPOINT = "https://app.formsmtp.com/api/v1/form/"

/**
 * @framerSupportedLayoutWidth any
 * @framerSupportedLayoutHeight auto
 */
export default function ContactForm(props) {
    const { formKey, subject, buttonLabel, style } = props
    const [status, setStatus] = useState("idle") // idle | sending | sent | error
    const [error, setError] = useState("")
    const loadedAt = useRef(Date.now())

    async function onSubmit(e) {
        e.preventDefault()
        const fields = Object.fromEntries(new FormData(e.currentTarget))
        setStatus("sending")
        try {
            const res = await fetch(ENDPOINT + formKey, {
                method: "POST",
                headers: { "Content-Type": "application/json", Accept: "application/json" },
                body: JSON.stringify({ ...fields, _subject: subject, _t: loadedAt.current }),
            })
            const data = await res.json()
            if (!data.success) throw new Error(data.error)
            setStatus("sent")
        } catch (err) {
            setError(err.message)
            setStatus("error")
        }
    }

    if (status === "sent") return <p style={style}>Thanks! Your message was sent.</p>

    const field = { font: "inherit", padding: "10px 12px", border: "1px solid #d6d3cc", borderRadius: 10 }
    return (
        <form onSubmit={onSubmit} style={{ display: "grid", gap: 12, ...style }}>
            <input name="name" placeholder="Name" autoComplete="name" required style={field} />
            <input name="email" type="email" placeholder="Email" autoComplete="email" required style={field} />
            <textarea name="message" placeholder="Message" rows={5} required style={field} />
            <input name="_gotcha" tabIndex={-1} autoComplete="off" style={{ display: "none" }} />
            <button disabled={status === "sending"} style={{ ...field, background: "#141413", color: "#fff", cursor: "pointer" }}>
                {status === "sending" ? "Sending…" : buttonLabel}
            </button>
            {status === "error" && <p role="alert">{error}</p>}
        </form>
    )
}

ContactForm.defaultProps = {
    formKey: "YOUR_FORM_KEY",
    subject: "New message from my Framer site",
    buttonLabel: "Send message",
}

addPropertyControls(ContactForm, {
    formKey: { type: ControlType.String, title: "Form key" },
    subject: { type: ControlType.String, title: "Subject" },
    buttonLabel: { type: ControlType.String, title: "Button" },
})
```

Drag *ContactForm* from the Assets panel onto your page and paste your form key into the *Form key* control. Change the inline styles to match your site.

## 5. Publish and test

Publish, open the live site and send a message. It arrives from your own address and shows up under *Logs* in FormSMTP. For the code component, test on the published address you added to Allowed websites; the editor preview runs elsewhere.

## Troubleshooting

- **Webhook route: emails stop after a few in an hour.** All submissions share one visitor limit. Raise *Form submissions per visitor per hour*, and look under *Logs → Blocked* for the ones that were held back.
- **The subject says “New form submission from” your project name.** The `_subject` input is missing, or it was hidden with *Visible* instead of *Hidden*.
- **Replies go to you instead of the visitor.** The email input isn’t named `email`.
- **Code component: “This form isn’t allowed on …”.** Add the exact address from the error to *Allowed websites*. If it says `null`, the form is running inside a sandboxed frame; use the code component, which runs in your page.
- **Code component: nothing happens on the canvas.** Test on the published site, where the request comes from your real domain.

## FAQ

### Which option should I choose?

The webhook if you want to keep Framer’s own form and your traffic is modest. The code component if you get a lot of submissions or bot spam, because each visitor is limited on their own and the honeypot works.

### Do I need to verify Framer’s webhook signature?

No. The signing secret is optional in Framer, and FormSMTP doesn’t read it. Your project is identified by the form key, and it can only email the recipients you allowed.

### Why not paste an HTML form into an Embed?

You can, but Framer’s docs don’t say whether HTML embeds run in a frame. If one does, the browser sends a `null` origin and Allowed websites rejects it. A code component runs in your page, so it doesn’t have that problem.

### Can the visitor get a confirmation email?

Not from the form endpoint: it only emails you, so a copied form key can’t be used to spam other people. To email the visitor, call the send API with your API key from server code.

### Is the form key safe in my page?

Yes, it’s made for that. The form key (`fk_`) only works on the form endpoint and always delivers to your own inbox, with per-visitor limits and a spam filter on top. Someone who copies it can’t email anyone else. The API key (`fs_`) can, so keep that one in server code or a secrets store.

## More guides

- [Webflow form to email: custom action with your SMTP](https://formsmtp.com/guides/webflow-contact-form)
- [Carrd contact form that emails you from your address](https://formsmtp.com/guides/carrd-contact-form)
- [Lovable contact form that sends email via your SMTP](https://formsmtp.com/guides/lovable-contact-form)
- [Bolt.new contact form that sends email (no backend)](https://formsmtp.com/guides/bolt-contact-form)
