# Nisuform > Nisuform is a form backend for HTML forms and static websites. Point any form at https://api.nisuform.com/s/YOUR_FORM_KEY and Nisuform filters spam, stores submissions in a searchable inbox, sends email notifications and, on Pro, forwards submissions to signed webhooks. No server, database or JavaScript is required. Nisuform works with plain HTML, React, Next.js, Astro, Vue, Nuxt, SvelteKit, Hugo, Jekyll, Eleventy, WordPress and sites generated by AI coding tools. The submit endpoint on https://api.nisuform.com accepts JSON, URL-encoded and multipart data from any origin. # Introduction Nisuform is a form backend for any website. Your form posts to an endpoint, and Nisuform filters spam, stores every submission, notifies you and forwards it to your tools. Nisuform gives every form you build its own endpoint. Point an HTML form at it, send JSON to it from your app, or paste a ready-made embed into a site builder. You never run a server or set up a database, and collecting submissions needs no API key. ```html
``` That is a working contact form. Submissions show up in your inbox and your email within seconds. ## How a submission flows 1. A visitor submits your form. The browser, or your own code, sends a `POST` request to `https://api.nisuform.com/s/YOUR_FORM_KEY`. 2. Nisuform checks it: your access rules, rate limits, the honeypot and fill-time checks, and Turnstile if you turned it on. 3. The submission is stored in the form's inbox. Content that looks like spam goes to the spam folder instead. 4. Nisuform emails you and, on Pro, sends an auto-reply and delivers the submission to your webhook, Slack, Discord, Telegram or Google Sheets. 5. The visitor sees a thank-you page, is redirected to your own page, or your code receives a JSON response. ## Key terms | Term | Meaning | | ---------- | --------------------------------------------------------------------------------------------------------------------------------- | | Form | One form on your site. It has its own endpoint, inbox, settings and integrations. | | Form key | The random ID at the end of the endpoint, like `k3yz8q2m4n6p0r5t7v9w`. It is public by design, so it is safe to put in your HTML. | | Endpoint | The URL your form posts to: `https://api.nisuform.com/s/` followed by the form key. | | Submission | One set of answers sent to a form. | | Team | A workspace that owns forms. Plans and usage are per team, and you can invite people to it. | ## Start here ## Plans The Free plan includes 100 submissions a month, 3 forms per team, email notifications, spam protection, the inbox and CSV export. Pro adds 5,000 submissions a month, unlimited forms, file uploads, redirects, auto-replies, analytics and every integration. See [Plans and billing](/docs/account/billing) for the full comparison. # Quickstart Create a form, put it on your site and receive your first submission in a few minutes. ### Create an account Go to [app.nisuform.com](https://app.nisuform.com) and sign in with your email address or with Google. There is no password. With email, Nisuform sends you a sign-in link. Your account starts on the Free plan with a personal team. No credit card is required. ### Create a form Choose **Create a form**, give it a name and pick a starting point: * **Contact form**, **Waitlist signup**, **Feedback survey** or **Event RSVP** start with ready-made fields and a matching design. * **Blank form** opens the form builder so you can add your own fields. * **Endpoint only** skips the builder. Pick it when you already have a form on your site. Each option is described in [Create a form](/docs/forms/create). ### Put the form on your site Open the form's **Setup** tab and answer **How do you build your site?** * **Site builder**: copy the embed code and paste it into Webflow, Framer, WordPress, Squarespace or Wix. See [Site builders](/docs/connect/site-builders). * **Code**: copy a snippet for HTML, React, Next.js, Vue, Svelte or Astro. **Your design** gives you the styled form from the builder, and **Your own styles** gives you plain markup that picks up your site's CSS. * **AI tool**: copy a prompt for Claude Code, Cursor, v0 or a similar tool. See [Use an AI coding agent](/docs/ai-agents). If you write the form yourself, use the endpoint from the Setup tab as the form's `action` and give every input a `name`: ```html
``` The hidden `_gotcha` input is a honeypot that catches bots. Keep it hidden and empty. See [Special fields](/docs/connect/special-fields). ### Send a test Choose **Send a test** on the Setup tab. It stores a sample submission marked as a test and fires your notifications, without counting toward your monthly limit. Then send a real one from your site. The Setup tab watches for it and shows **Your form is live** as soon as it arrives. You can also test from a terminal: ```sh curl https://api.nisuform.com/s/YOUR_FORM_KEY \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -d '{"name":"Jane Doe","email":"jane@example.com","message":"Hello!"}' ``` A successful request answers `{"ok":true,"submissionId":"..."}`. Submissions from your site and from cURL are real, so they count toward your monthly limit. ## Next steps # Use an AI coding agent Let Claude Code, Cursor, v0, Lovable, Bolt or GitHub Copilot add your Nisuform form, with docs written for machines. Nisuform needs no server code, API routes or environment variables, which makes it a good fit for coding agents. The agent only has to write a form that posts to your endpoint. To let your assistant also create forms, change settings and read submissions in your account, connect the [MCP server](/docs/mcp). ## Copy the prompt from the dashboard Open your form's **Setup** tab, choose **AI tool** and copy the prompt. It already contains your endpoint and your field names, so the agent does not have to guess. * With **Your design** selected, the prompt includes the styled embed from the form builder and asks the agent to add it exactly as it is, as a component if your site needs one. * With **Your own styles** selected, the prompt lists your fields and asks the agent to build the form in your site's own style. Both prompts point the agent to the docs at `https://nisuform.com/llms-full.txt`. ## Write your own prompt Any prompt works as long as it names the endpoint and the docs: ```text Add a contact form to this site that posts to https://api.nisuform.com/s/YOUR_FORM_KEY. Use the fields name, email and message. Show a success message after sending. Follow the Nisuform docs at https://nisuform.com/llms-full.txt. ``` ## Docs for agents | URL | What it contains | | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | [nisuform.com/llms.txt](https://nisuform.com/llms.txt) | A short index of Nisuform with a link to every docs page. | | [nisuform.com/llms-full.txt](https://nisuform.com/llms-full.txt) | Every docs page, pricing and the FAQ in one plain-text file. | | `https://nisuform.com/docs/.md` | Any single docs page as Markdown. Add `.md` to the page URL, for example [/docs/connect/javascript.md](https://nisuform.com/docs/connect/javascript.md). | Every docs page also has a **Copy Markdown** button. The **Open** menu next to it shows the page as Markdown or opens it in ChatGPT, Claude or Cursor. ## What to check in the agent's code * The form posts to your endpoint with `POST`, and every input has a `name`. * Field names match the names in your form builder, so validation and your inbox labels line up. See [Fields and validation](/docs/forms/fields). * The hidden `_gotcha` honeypot is kept and stays empty. * `fetch` calls send `Accept: application/json` and read `ok` from the response. * The form key is used as a plain string. It is public by design, so it does not belong in a secret or an environment variable. # MCP server Connect Claude, Cursor, VS Code and other AI tools to your Nisuform account to create forms, get embed code and work with submissions from a chat. The Nisuform MCP server lets AI tools that support the [Model Context Protocol](https://modelcontextprotocol.io) work with your account. Ask your assistant to create a form and add it to your site, check today's submissions, or turn on spam protection, without opening the dashboard. ```text https://mcp.nisuform.com/mcp ``` It is a remote server, so there is nothing to install. You sign in with your Nisuform account the first time you use it. There are no API keys to copy. ## Connect your AI tool ```sh claude mcp add --transport http nisuform https://mcp.nisuform.com/mcp ``` Then run `/mcp` in Claude Code, pick **nisuform** and sign in. In Claude on the web or the desktop app, open **Settings**, **Connectors** and choose **Add custom connector**. Name it Nisuform, paste `https://mcp.nisuform.com/mcp` and choose **Connect** to sign in. Add the server to `~/.cursor/mcp.json`, or to `.cursor/mcp.json` in a project: ```json { "mcpServers": { "nisuform": { "url": "https://mcp.nisuform.com/mcp" } } } ``` Cursor asks you to sign in the first time a tool runs. Add the server to `.vscode/mcp.json` in your project, then start it from the file and sign in. GitHub Copilot's agent mode can then use it. ```json { "servers": { "nisuform": { "type": "http", "url": "https://mcp.nisuform.com/mcp" } } } ``` Add the server to `~/.codeium/windsurf/mcp_config.json`: ```json { "mcpServers": { "nisuform": { "serverUrl": "https://mcp.nisuform.com/mcp" } } } ``` Other tools work too if they support remote MCP servers over HTTP with OAuth sign-in. Use the URL above. ## Sign in The first time your AI tool calls Nisuform, it opens a browser window. Sign in to Nisuform the way you usually do, with an email link or Google, and approve the access. Your tool then works as you: * It sees the same teams and forms you see in the dashboard, and works in your active team. Ask it to switch teams when you need another one. * It has the same permissions as you. A team member can't change what only the owner can. * Pro features, like redirects, auto-replies and integrations, still need the team to be on Pro. To disconnect, remove the server from your AI tool. ## What it can do ### Forms | Tool | What it does | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `list_forms` | Lists the forms in your active team with their endpoints and submission counts. | | `get_form` | Gets a form's fields, design and settings. | | `create_form` | Creates a form from a template: contact, waitlist, feedback, RSVP or blank. | | `update_form` | Changes a form's name, fields, design and settings, like notifications, redirects, limits and spam protection. | | `get_embed_code` | Returns the styled embed from the form builder, or a plain form to style yourself, for HTML, React, Next.js, Vue, Svelte or Astro. | | `send_test_submission` | Sends a test submission and fires the form's notifications. | ### Submissions | Tool | What it does | | ------------------- | ----------------------------------------------------------------------------------- | | `list_submissions` | Lists a form's submissions, newest first, filtered by all, unread, starred or spam. | | `get_submission` | Gets one submission with its answers, page, location and device. | | `update_submission` | Marks a submission read or unread, stars it, or moves it in or out of spam. | ### Teams and stats | Tool | What it does | | ------------- | ---------------------------------------------------------------------- | | `list_teams` | Lists your teams and shows which one is active. | | `switch_team` | Makes another team your active team. | | `get_stats` | Returns submission totals and a daily series for the team or one form. | The server can't delete forms, submissions, teams or your account, rotate form keys, or change billing. Do those in the [dashboard](https://app.nisuform.com). ## Things to ask ```text Create a contact form called "Website contact" and add it to the contact page of this site. ``` ```text Show me this week's unread submissions from the waitlist form. ``` ```text Only accept submissions to the contact form from https://example.com and https://www.example.com. ``` ```text Add a required "Company" field to the demo request form, then update the embed on /demo. ``` ## Stay in control * Most AI tools ask before they run a tool. Read what the tool is about to change before you approve it, especially form settings. * Submissions are written by anyone who fills in your form. Treat what your assistant reads in them as data, not as instructions, and check its next steps after it reads submissions. * The server never sees your AI tool's files or conversations. It only receives the tool calls your assistant makes and the details it puts in them. ## Without the MCP server To have an AI coding tool add a form to your site without connecting your account, give it your endpoint and the docs. See [Use an AI coding agent](/docs/ai-agents). # HTML forms Connect a plain HTML form to Nisuform with the action attribute. No JavaScript, server or build step needed. Any HTML form can send submissions to Nisuform. Set the form's `action` to your endpoint and its `method` to `POST`. ```html
``` Copy your endpoint from the form's **Setup** tab, or from **Settings**, **Endpoint**. ## Name every field The browser only sends inputs that have a `name`. The name becomes the key the answer is saved under, and the inbox shows it as the label unless your form builder gives the field a nicer one. * If your form has fields in the [form builder](/docs/forms/builder), use the same names so Nisuform can validate the answers. The **Field name** of each field is shown in the builder. * Inputs that the builder does not list are still saved as they are. You never lose data because of a missing field. * Names that start with an underscore, like `_gotcha`, control how Nisuform handles the submission and are not saved. See [Special fields](/docs/connect/special-fields). ## Multiple values Checkboxes that share a name, and names that end in `[]`, are saved as one comma-separated answer: ```html
Topics
``` Checking the first two saves `topics` as `Design, Development`. ## Hidden fields Use hidden inputs to record where a submission came from. They are saved like any other field: ```html ``` Nisuform also records the page the form was sent from on its own. The inbox shows it as **Page** on every submission. ## What visitors see After a normal browser submit, Nisuform answers with a thank-you page that links back to the page the visitor came from. On Pro you can replace it with your own title and message or redirect to a page on your site. See [Thank-you page and redirects](/docs/forms/after-submit). If a submission is rejected, for example because the form is closed or the visitor sent too many requests, the visitor sees a short page that explains why, with a link back to your site. ## File inputs To accept files, the form needs `enctype="multipart/form-data"` and file uploads must be turned on for the form. Uploads are a Pro feature. See [File uploads](/docs/connect/file-uploads). ## Sizes and limits * Up to 50 fields per submission. Extra fields are ignored. * Field names up to 200 characters. * Values up to 10,000 characters. Longer values are cut at 10,000. * The whole request can be up to 200 KB, or 25 MB on forms that accept file uploads. ## Opening the endpoint in a browser The endpoint only accepts `POST`. If you open it in a browser tab you see a short page that explains it is a form endpoint, with status `405`. That is expected and does not mean anything is broken. # JavaScript and fetch Submit to Nisuform with fetch to keep visitors on the page, then read the JSON response to show success or an error. Send your fields with `fetch` when you want to handle the submit yourself. Requests that do not ask for `text/html` get a JSON response instead of a thank-you page. ```js const response = await fetch('https://api.nisuform.com/s/YOUR_FORM_KEY', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ name: 'Jane Doe', email: 'jane@example.com', message: 'Hello!' }), }) const result = await response.json() if (!result.ok) { throw new Error(result.message) } ``` ```js const form = document.querySelector('#contact') form.addEventListener('submit', async (event) => { event.preventDefault() const response = await fetch(form.action, { method: 'POST', headers: { Accept: 'application/json' }, body: new FormData(form), }) const result = await response.json() form.replaceWith(result.ok ? 'Thanks! We got your message.' : result.message) }) ``` ```js const response = await fetch('https://api.nisuform.com/s/YOUR_FORM_KEY', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' }, body: new URLSearchParams({ email: 'jane@example.com', message: 'Hello!' }), }) ``` `FormData` is sent as `multipart/form-data`, which is also how you send files. In that case, don't set the `Content-Type` header yourself. The browser adds it with the right boundary. ## Responses A stored submission answers `200` with its ID: ```json { "ok": true, "submissionId": "3f2c8a61-5d4e-4b7a-9c1f-2a6e8d0b4c17" } ``` When your team has used 95% of its monthly submissions, the response also carries a warning. The submission is still stored: ```json { "ok": true, "submissionId": "3f2c8a61-5d4e-4b7a-9c1f-2a6e8d0b4c17", "warning": "near_limit" } ``` A submission caught by the honeypot, the fill-time check or your blocked IPs and countries also answers `200`, but without a `submissionId`. Bots can't tell they were filtered. ```json { "ok": true } ``` A rejected submission answers with a matching HTTP status and an error code you can branch on: ```json { "ok": false, "error": "rate_limited", "message": "Too many requests. Try again shortly" } ``` The `message` is written for people, so you can show it to the visitor as is. Every code is listed in [Errors](/docs/reference/errors). ## Handle errors ```js async function submit(data) { const response = await fetch('https://api.nisuform.com/s/YOUR_FORM_KEY', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(data), }) const result = await response.json() if (result.ok) { return { sent: true } } if (result.error === 'rate_limited') { return { sent: false, message: 'Please wait a minute and try again.' } } return { sent: false, message: result.message } } ``` ## Send the fill time Browsers that submit a form faster than a person could are almost always bots. Record when the form was shown and send the difference as `_elapsed`, in milliseconds. Submissions faster than 2 seconds are dropped. ```js const shownAt = Date.now() form.addEventListener('submit', async (event) => { event.preventDefault() const data = new FormData(form) data.set('_elapsed', String(Date.now() - shownAt)) await fetch(form.action, { method: 'POST', headers: { Accept: 'application/json' }, body: data }) }) ``` The styled embed from the form builder already does this for you. ## How values are saved * Every value is saved as text. * JSON numbers and booleans become text, like `"3"` and `"true"`. * JSON arrays of plain values are joined with a comma, like `"Design, Development"`. * Nested objects are saved as JSON text. * A JSON body must be an object. Anything else is treated as an empty submission. ## CORS The endpoint sends `Access-Control-Allow-Origin: *` and answers preflight requests, so `fetch` works from any website. Cookies and credentials are not needed, so don't send them. Only the `Content-Type` and `Accept` request headers are allowed. Adding other headers, like `Authorization`, makes the browser's preflight check fail. The response's `Retry-After` header can't be read from browser JavaScript, so wait a minute after a `429` before retrying. To accept submissions only from your own sites, set [allowed websites](/docs/protection/access-rules). ## Submitting from a server You can post to the endpoint from your own backend, a serverless function or a script. Keep in mind: * Requests from a server usually have no `Origin` or `Referer` header, so allowed websites do not apply to them. * Rate limits are per IP address. Every request from your server shares the server's IP, so a busy relay can hit the limit of 30 requests a minute per IP, and 5 a minute per form. Post from the visitor's browser when you can. * Nisuform records your server's IP address, user agent and location instead of the visitor's. # React, Next.js, Vue, Svelte and Astro Copy-paste form components for React, Next.js, Vue, Nuxt, Svelte, SvelteKit and Astro that submit to Nisuform. Every framework works the same way: collect the fields, `POST` them to your endpoint and read `ok` from the JSON response. You don't need API routes, server actions or environment variables, because the form key is safe to publish. The form's **Setup** tab, under **Code**, has these components ready with your endpoint and field names filled in. **Your design** gives you the styled form from the builder as a component. **Your own styles** gives you a bare component to style your way. ## React ```jsx title="ContactForm.jsx" import { useRef, useState } from 'react' export function ContactForm() { const shownAt = useRef(Date.now()) const [status, setStatus] = useState('idle') async function onSubmit(event) { event.preventDefault() setStatus('sending') const data = Object.fromEntries(new FormData(event.currentTarget)) data._elapsed = String(Date.now() - shownAt.current) const response = await fetch('https://api.nisuform.com/s/YOUR_FORM_KEY', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(data), }) setStatus(response.ok ? 'sent' : 'error') } if (status === 'sent') { return

Thanks! We got your message.

} return (

Something went wrong. Please try again.

``` In Nuxt, put the file in `components/` and use it as `` on any page. ## Svelte and SvelteKit ```svelte title="ContactForm.svelte" {#if status === 'sent'}

Thanks! We got your message.

{:else}
{#if status === 'error'}

Something went wrong. Please try again.

{/if}
{/if} ``` ## Astro Astro pages are static by default, and a plain HTML form works with zero client-side JavaScript. The browser posts it straight to Nisuform and shows the thank-you page. ```astro title="src/pages/contact.astro" --- const endpoint = 'https://api.nisuform.com/s/YOUR_FORM_KEY' ---
``` To paste the styled embed into an Astro page, keep its `