Guides

Framer form to email: webhook or code component

Framer forms can post to a webhook, and FormSMTP turns that post into an email from your own address. Here’s that no-code route, a code component route with full spam protection, and how to choose.

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 → WebhookCode component
SetupNo code. Keeps Framer’s form design and states.One code file, then drag it onto the page.
Who sends the requestFramer’s serversThe visitor’s browser
Per-visitor limitsAll visitors share one limit, because every request comes from FramerEach visitor is counted on their own
Honeypot and time trapNot available. Framer’s own spam protection applies instead.Included
Allowed websitesNot 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.

1Create a FormSMTP project

Sign up free 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, Microsoft 365, Zoho, 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.

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

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

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

5Publish 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

Get a Founding Pass

10,000 emails a month, free for your first year. Pass No. 0008 is next: 1,993 of 2,000 left, no card.