--- name: netlify-forms description: Serverless form handling on Netlify-hosted sites — detects HTML forms at deploy time, stores submissions, filters spam, and sends notifications. Use when adding a contact form, lead-capture form, file-upload form, or newsletter signup to a Netlify site; wiring AJAX form submission; setting up a custom thank-you page; adding a honeypot or reCAPTCHA to a form; getting forms working in Next.js, Nuxt, SvelteKit, Astro, or Gatsby; reading form submissions via the Netlify API; or debugging missing submissions and forms that silently fail to register. --- # Netlify Forms Mark a form for detection with `data-netlify="true"` (or the bare `netlify` attribute — equivalent) on the `
` tag. Forms are detected by **parsing the final built HTML at deploy time** — there is no runtime API call or backend code. Client-side/JS-rendered/SSR forms are NOT in the built HTML and are never detected on their own; they require a static skeleton file (see below). Prerequisite: form detection must be enabled once in the Netlify UI (Forms > **Enable form detection**). Takes effect on the next deploy. ## Static HTML form ```html

``` - `name` sets the form name in the UI and **must be unique per site**. - At deploy, Netlify strips the `data-netlify`/`netlify` attribute and injects ``. - Add an `` so the notification email's `Reply-to` is set to the submitter. ## JS-rendered / SSR / framework forms (Next.js, Nuxt, SvelteKit, Astro, Gatsby) Two required pieces: **1. Static skeleton file `public/__forms.html`** — a hidden copy of each form with `data-netlify="true"`, a hidden `form-name` input, and every field the component submits, with names matching **exactly** (Netlify validates field names against the registered form). Without this file, submissions silently fail. ```html ``` **2. The rendered form** carries a matching hidden `form-name` input: ```jsx
``` **⚠️ SSR POST target:** In SSR apps, `fetch("/")` is intercepted by the SSR catch-all function and never reaches form processing. POST to the static skeleton file itself — `/__forms.html` — not `/` or an arbitrary path. **⚠️ Astro on-demand routes:** Routes with `export const prerender = false` or `output: "server"` are never scanned at build time, so their forms are never registered. Put the form on a prerendered page, or rely on the static skeleton file. **Next.js Runtime v5 (Next.js 13.5+):** extract form definitions to the static skeleton file and submit via AJAX rather than full-page navigation. See https://docs.netlify.com/build/frameworks/framework-setup-guides/nextjs/overview#v5-breaking-changes ## AJAX submission ```js const handleSubmit = event => { event.preventDefault(); const formData = new FormData(event.target); fetch("/__forms.html", { // static sites may POST to "/"; SSR must target the skeleton file method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams(formData).toString() }) .then(() => alert("Thank you for your submission")) // or navigate("/thank-you") .catch(error => alert(error)); }; document.querySelector("form").addEventListener("submit", handleSubmit); ``` - **Body MUST be URL-encoded. JSON is NOT supported.** - If the rendered form has no hidden `form-name` input, you MUST include a `form-name` field in the POST body. - The honeypot field name and `g-recaptcha-response` (if used) must be in the body — automatic with `FormData()`. ## File uploads Add `type="file"`; optionally `enctype="multipart/form-data"` on the `
`. For AJAX file uploads, **do NOT set a `Content-Type` header** — let the browser set it (with the multipart boundary). ```js document.forms.fileForm.addEventListener("submit", event => { event.preventDefault(); fetch("/", { body: new FormData(event.target), method: "POST" }) // no headers .then(() => { /* success */ }); }); ``` Limits: one file per field (use multiple fields for multiple files) · 8 MB max request size · 30 s upload timeout · after form deletion, uploaded files stay at their direct URL for 24 h. PII uploads need extra security (Very Good Security integration). ## Custom success page Add an `action` path relative to site root, starting with `/`. **Use extensionless paths** — Netlify serves `thank-you.html` at `/thank-you`; the `.html` path returns 404. ```html
``` Custom success *alert* is only possible via AJAX (substitute the redirect with your own logic). ## Spam prevention All submissions are filtered by Akismet. Passed → **Verified submissions**; flagged → **Spam submissions**. Honeypot/reCAPTCHA failures are rejected and appear in neither list. **Honeypot:** add `netlify-honeypot="bot-field"` to the `
` and include a CSS-hidden field of that name. Any value entered → submission quietly rejected. ```html
``` **Netlify reCAPTCHA 2:** add `data-netlify-recaptcha="true"` to the `
` AND an empty `
` where it renders. Only ONE Netlify-provided challenge per page — for multiple, use custom reCAPTCHA. For JS-rendered forms, also add the `div` to the static skeleton file. **Custom reCAPTCHA 2:** your own reCAPTCHA snippet + `data-netlify-recaptcha="true"` on the ``, plus env vars: - `SITE_RECAPTCHA_KEY` — site key (scopes: Builds + Runtime) - `SITE_RECAPTCHA_SECRET` — secret (scope: Runtime) ## Email notifications & subject line Default sender: `formresponses@netlify.com`. Set subject via a hidden `subject` input **or** the Netlify UI (Configuration > Notifications) — **not both; the HTML value always overrides the UI.** ```html ``` Variables: `%{formName}`, `%{siteName}`, `%{submissionId}`. Forms created before **May 5, 2023** carry a `[Netlify]` subject prefix — remove it by adding the `data-remove-prefix` attribute to the `subject` input. Set up notifications (email/webhook/Slack) in the UI: Configuration > Notifications > Form submission notifications > **Add notification**. ## Reading submissions via the API Use only documented surfaces. Do NOT invent `api.netlify.com` endpoints or read tokens from local CLI config files. Reference: https://open-api.netlify.com/#tag/submission/operation/listFormSubmissions - **Page through results using the `Link` header** — code that reads only the first response silently drops the rest. - `listFormSubmissions` returns data from old/removed fields no longer shown in the UI. - Query spam with `?state=spam`. ## Submission summary (field order matters) The UI summary is derived from field **type**, not name: - **Title**: first non-hidden text `` that isn't email-like (`type="email"`, or name matching `email`/`mail`/`from`/`twitter`/`sender`); falls back to a field named `title` or `subject`. - **Body**: first `