# FormParcel
> FormParcel is a form backend for web agencies and freelance developers. An HTML or JavaScript form posts to https://f.formparcel.live/f/{formId}. Each accepted lead is stored before the answer is sent, delivered to a Google Sheet, and gets a receipt whose ID starts with sub_.
AI coding agents: to add a FormParcel form to a site, read https://formparcel.live/docs/guides/ai.md first. Every link below is a Markdown page; the HTML page is the same address without .md. All pages in one file: https://formparcel.live/llms-full.txt. The form address as OpenAPI 3.1: https://formparcel.live/openapi.json.
- Only the public form address is documented. There is no management API, CLI or MCP server.
- Post from the visitor's browser and load the bot check script (/f/formparcel.js on the form address host). A post from a server, a webhook or curl gets a 200 but is held in the owner's Spam folder. Test with "Send a test lead" in the dashboard.
- A 200 answer means Accepted (stored). Delivery is a separate state. Never resend a lead whose answer said "pending".
- File uploads, passwords, one-time codes and card details are refused.
This file holds every FormParcel docs page in full, in reading order. Each page starts with its front matter.
---
title: "FormParcel docs"
description: "FormParcel gives a website form a public address. Each lead it accepts is stored, delivered to your client's Google Sheet, and gets a receipt you can check."
url: "https://formparcel.live/docs"
---
# FormParcel docs
FormParcel gives a website form a public address. Each lead it accepts is stored, delivered to your client's Google Sheet, and gets a receipt you can check.
[**Quickstart →** Form address to first receipt.](https://formparcel.live/docs/quickstart)[**Choose your platform →** WordPress, Webflow, Framer, Wix, Squarespace, Shopify or code.](https://formparcel.live/docs/setup)[**Use an AI builder →** One block of instructions to paste into your AI.](https://formparcel.live/docs/guides/ai)[**Answers and error codes →** What the form address sends back, and every refusal.](https://formparcel.live/docs/reference/responses)[**Read a receipt →** What each delivery state means.](https://formparcel.live/docs/reference/receipts)[**Fix a problem →** Start from the code or the state you see.](https://formparcel.live/docs/help/troubleshooting)
## What FormParcel does not do
- It does not take file uploads. A file in a form post is refused.
- It does not take passwords, one-time codes or card details. A field named like one is refused.
- It is not a form builder. Your form keeps the markup you wrote.
- It does not take posts from a server yet: a post without the browser’s bot check waits in Spam. See [Bot check](https://formparcel.live/docs/reference/bot-check).
- It has no management API, command line or MCP server. Only the form address is documented here.
## For AI agents
Every page has a Markdown copy at the same address plus `.md`. The whole set is in [llms.txt](https://formparcel.live/llms.txt) and [llms-full.txt](https://formparcel.live/llms-full.txt), and the form address is described in [openapi.json](https://formparcel.live/openapi.json).
## All pages
Get started
- [Overview](https://formparcel.live/docs)
- [Quickstart](https://formparcel.live/docs/quickstart)
- [How it works](https://formparcel.live/docs/how-it-works)
Set up
- [Choose your platform](https://formparcel.live/docs/setup)
- [Google Sheets](https://formparcel.live/docs/setup/google-sheets)
- [Email notifications](https://formparcel.live/docs/setup/email)
- [Spam protection](https://formparcel.live/docs/setup/spam-protection)
- [Auto-reply](https://formparcel.live/docs/setup/autoresponder)
Guides
- [Plain HTML form](https://formparcel.live/docs/guides/html)
- [JavaScript fetch](https://formparcel.live/docs/guides/fetch)
- [React](https://formparcel.live/docs/guides/react)
- [Next.js](https://formparcel.live/docs/guides/nextjs)
- [Astro](https://formparcel.live/docs/guides/astro)
- [WordPress](https://formparcel.live/docs/guides/wordpress)
- [Webflow](https://formparcel.live/docs/guides/webflow)
- [Framer](https://formparcel.live/docs/guides/framer)
- [Wix](https://formparcel.live/docs/guides/wix)
- [Squarespace](https://formparcel.live/docs/guides/squarespace)
- [Shopify](https://formparcel.live/docs/guides/shopify)
- [AI builders and agents](https://formparcel.live/docs/guides/ai)
Reference
- [`POST /f/{formId}`](https://formparcel.live/docs/reference/endpoint)
- [Responses and errors](https://formparcel.live/docs/reference/responses)
- [Special fields](https://formparcel.live/docs/reference/fields)
- [Bot check](https://formparcel.live/docs/reference/bot-check)
- [Limits and retention](https://formparcel.live/docs/reference/limits)
- [Receipts and delivery states](https://formparcel.live/docs/reference/receipts)
Help
- [Troubleshooting](https://formparcel.live/docs/help/troubleshooting)
---
---
title: "Quickstart"
description: "Sign in, copy your form address, point a form at it, send a test lead and read its receipt."
url: "https://formparcel.live/docs/quickstart"
---
# Quickstart
Sign in, copy your form address, point a form at it, send a test lead and read its receipt.
## Sign in and copy your form address
Enter your email address and open the link in the email, or sign in with Google. Your first sign-in creates a first form for you, with a form address ready to copy.
Add a client site, and a form on it, for each client after that. The address looks like `https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE`, and your own ID replaces the last part. It does not change, so you can paste it into a client’s site once.
A form accepts leads once it has somewhere to deliver them: a Google Sheet or an email recipient. Until then, a post gets a 400 answer ([form not ready](https://formparcel.live/docs/reference/responses#form-not-ready)). Connecting a Google Sheet is a separate step that asks for a narrower permission: only the files FormParcel creates.
## Point a form at it
Set the form’s `action` to your address and its `method` to `POST`. Keep your own markup. The dashboard gives you this snippet with your address filled in. Using a site builder or an AI builder? Start from [Choose your platform](https://formparcel.live/docs/setup).
Example form address. Use yours from the dashboard.
```html title="contact.html"
```
- Every field needs a `name`. A field without one is not sent.
- The `_gotcha` field is a spam trap. Keep it hidden from people.
- The script line after the form is FormParcel’s bot check. It sets no cookies and shows nothing. Without it, leads wait in your Spam folder instead of being delivered. See [Spam protection](https://formparcel.live/docs/setup/spam-protection#the-bot-check).
- `_next` is optional. It sends visitors to a page on the client’s own site.
To stay on the page and read the answer yourself, send the post from JavaScript: [the fetch guide](https://formparcel.live/docs/guides/fetch).
## Send a test lead
Before you touch the client’s site, use **Send a test lead** on the form’s page in the dashboard. It goes through the real pipeline, so it gets a real receipt, and its name in the Sheet row starts with `[TEST]`. A test lead skips the bot check, so post the form once from the client’s page too. Do not test with `curl` or from a server: those posts carry no bot-check proof and wait in your Spam folder. Real leads from the site show up in the same list.
## Read the receipt
Open the receipt in **Leads** to see each attempt and where the lead stands. A lead is Accepted as soon as it is stored. It is Delivered once the Google Sheet has the row. [The receipts reference](https://formparcel.live/docs/reference/receipts) explains every state.
## What it refuses
A post is limited to 16 KB of text. Files are not accepted. Do not collect passwords, payment card numbers or highly sensitive regulated information: a post with a field named like one is refused, and so is a post with nothing readable in it.
Every refusal and its code is on [Responses and error codes](https://formparcel.live/docs/reference/responses).
## If nothing arrives
- Check that every field has a `name` and the form uses `method="POST"`.
- If you set allowed origins on the form, the page the form is on must match one of them. A request that carries neither an Origin nor a Referer header is refused ([origin not allowed](https://formparcel.live/docs/reference/responses#origin-not-allowed)).
- A post with nothing readable is [`EMPTY_SUBMISSION`](https://formparcel.live/docs/reference/responses#empty-submission), and a field named like a password or card detail is [`PROHIBITED_FIELD`](https://formparcel.live/docs/reference/responses#prohibited-field). Neither creates a receipt.
- If the receipt says **Held as spam** and the reason is the bot check, the page is missing the script line, or its Content-Security-Policy blocks it. See [Troubleshooting](https://formparcel.live/docs/help/troubleshooting#leads-in-the-spam-folder).
- If the receipt says **New fields to review**, the form sent a field your Sheet has no column for. Review it in the dashboard.
- A 429 answer means you are sending too fast: wait for the seconds in `Retry-After`. [`PLAN_LIMIT_REACHED`](https://formparcel.live/docs/reference/responses#plan-limit-reached) is different: the workspace is far over its plan’s monthly limit.
- A 403 with [`SPAM_CHECK_FAILED`](https://formparcel.live/docs/reference/responses#spam-check-failed) means the site has the spam check on and the visitor did not pass it.
---
---
title: "How it works"
description: "A lead is stored before FormParcel answers, then delivered to your Sheet. Each lead gets a receipt, and a lead without the bot check waits in Spam."
url: "https://formparcel.live/docs/how-it-works"
---
# How it works
A lead is stored before FormParcel answers, then delivered to your Sheet. Each lead gets a receipt, and a lead without the bot check waits in Spam.
## Accepted, then delivered
Your form posts to its form address. FormParcel stores the lead first, then answers, then delivers it. **Accepted** means stored; **Delivered** means your Google Sheet has the row. [Learn more](https://formparcel.live/docs/reference/responses#what-a-200-means)
## Checks before a lead is stored
A post that breaks a rule, such as a file, a password field or too much text, is refused with a code and no receipt. [Learn more](https://formparcel.live/docs/reference/limits)
## The bot check and the Spam folder
A post without the bot check’s proof, or one that looks like spam, is stored in your Spam folder, not delivered and not counted. Release a real one with **Not spam**. [Learn more](https://formparcel.live/docs/reference/bot-check)
## When delivery fails
If Google is down or access was removed, the lead waits and FormParcel tries again. Each row carries the lead’s receipt ID, and FormParcel looks for it in the Sheet before it writes again, so a retry does not add a second row. [Learn more](https://formparcel.live/docs/reference/receipts)
## The receipt
Every accepted lead gets a receipt ID that starts with `sub_`. It is in the answer, in column A of the Sheet row, and in **Leads**, where you can see each attempt. [Learn more](https://formparcel.live/docs/reference/receipts)
---
---
title: "Choose your platform"
description: "Where is your form? Pick your site builder, your code or your AI builder, and follow one short guide with one block to copy."
url: "https://formparcel.live/docs/setup"
---
# Choose your platform
Where is your form? Pick your site builder, your code or your AI builder, and follow one short guide with one block to copy.
## Where is your form?
[**I write code** Plain HTML, fetch, React, Next.js or Astro.](https://formparcel.live/docs/guides/html)[**AI builder or coding agent** Lovable, Bolt, v0, Replit, Cursor, Claude Code, Codex.](https://formparcel.live/docs/guides/ai)[**WordPress** A Custom HTML block or Elementor’s HTML widget.](https://formparcel.live/docs/guides/wordpress)[**Webflow** A custom form action plus one script line.](https://formparcel.live/docs/guides/webflow)[**Framer** An Embed, not Framer’s own form.](https://formparcel.live/docs/guides/framer)[**Wix** An Embed HTML element.](https://formparcel.live/docs/guides/wix)[**Squarespace** A Code block.](https://formparcel.live/docs/guides/squarespace)[**Shopify** A Custom Liquid section.](https://formparcel.live/docs/guides/shopify)
Something else? Send [the instructions for your AI](https://formparcel.live/docs/guides/ai#copy-the-instructions) to your developer: they work for people too.
## One rule for every platform
The form must post from the visitor’s browser and load FormParcel’s bot check. A builder that forwards the form from its own server, through a webhook or a plugin, sends leads that wait in your Spam folder. [Learn more](https://formparcel.live/docs/reference/bot-check)
## Then set up the form
- [Google Sheets](https://formparcel.live/docs/setup/google-sheets): where leads are delivered.
- [Spam protection](https://formparcel.live/docs/setup/spam-protection): the bot check, the Spam folder and the optional Turnstile check.
- [Auto-reply](https://formparcel.live/docs/setup/autoresponder): an optional thank-you email to the visitor.
---
---
title: "Google Sheets"
description: "Connect a Google account with a narrow permission, create the Sheet for a form, and know what FormParcel writes into it and what to leave alone."
url: "https://formparcel.live/docs/setup/google-sheets"
---
# Google Sheets
Connect a Google account with a narrow permission, create the Sheet for a form, and know what FormParcel writes into it and what to leave alone.
## What FormParcel does
Each lead is added as a new row in a Google Sheet that you connect to the form. The Sheet lives in the Google account you choose for that form: your agency’s, or your client’s if they should own their leads. FormParcel can open only the Sheets it creates, not the rest of the Drive.
## Connect Google
Signing in to FormParcel shares only your name and email. Sheet access is a separate, narrower permission, asked for when you connect Google on a form: the `drive.file` scope, which covers only files FormParcel creates.
On the form’s page in the dashboard, open the Sheet step and choose **Connect Google**. Google shows the permission screen, and you are sent back to the form. A workspace can connect more than one Google account. When there is more than one, the form asks which account the Sheet is created in. **Use a different Google account** on the Sheet step, or **Connect another account** in Settings, connects another. **Disconnect** in Settings removes one.
## Create the Sheet and verify
Name the Sheet (you can rename it in Google Sheets later) and choose **Create Sheet and verify**. FormParcel creates the Sheet in the chosen account’s Drive and writes one verification row, so you know delivery works before you put the form on a site. **Open Sheet** takes you to it. If the verification row was not written, the form says so and offers **Run verification**.
On the Free plan a workspace has 1 Sheet. Archiving a form frees its Sheet’s place.
## What is in the Sheet
The header row starts with four columns FormParcel writes for every lead. Your form’s fields follow, one column per field name.
| Column | What it holds |
| --- | --- |
| `_submission_id` | The lead’s receipt ID, in column A of every row. It is how you find a lead’s row from its receipt, and a receipt from its row. |
| `_received_at` | When FormParcel received the lead. |
| `_source_site` | The client site’s name, as set in FormParcel. |
| `_source_form` | The form’s name, as set in FormParcel. |
A form field with one of these four names is written to a column named `custom_` followed by the name, so it never overwrites a system column. A test lead’s name starts with `[TEST]`.
Text that a spreadsheet would read as a formula is neutralised before it is written, so a lead cannot run a formula in your Sheet.
## New fields
If the form starts sending a field the Sheet has no column for, the lead is not written and not dropped. Its receipt says New fields to review and waits. Open it in the dashboard, then add a column for the field, or ignore the field, and the lead is delivered. The state is explained on [Receipts and delivery states](https://formparcel.live/docs/reference/receipts#new-fields-to-review).
## Editing the Sheet
- You can add your own columns to the right of FormParcel’s.
- Do not rename, move or delete a header cell. FormParcel checks the header row before it writes. If it no longer matches, delivery to that Sheet pauses and the receipt says Sheet columns changed, rather than writing values into the wrong columns. Put the header back and re-check the Sheet, or switch the form to a new Sheet.
- If the Sheet goes to Trash, is deleted, or can no longer be edited, the receipt says Sheet needs repair. Restore it and re-check it, or replace it. Waiting leads are then sent again automatically.
- If Google access is revoked or expires, the receipt says Reconnect Google. Reconnect Google and waiting leads are sent again automatically.
Every fix is on [Troubleshooting](https://formparcel.live/docs/help/troubleshooting).
## A row and its receipt
Column A holds the receipt ID, so a retry can check the Sheet for a lead before it writes again. When Google’s answer is unclear, FormParcel looks for the lead’s receipt ID in the Sheet first. If it is there, nothing more is written.
---
---
title: "Email notifications"
description: "Add recipients to a form so each new lead is also emailed. Covers confirmation, the reply address, the subject suffix, sending limits and paused addresses."
url: "https://formparcel.live/docs/setup/email"
---
# Email notifications
Add recipients to a form so each new lead is also emailed. Covers confirmation, the reply address, the subject suffix, sending limits and paused addresses.
## Recipients and confirmation
A form can email each new lead to the recipients you add, so a lead still reaches someone when Google is down. A form can have 1 notification recipient on Free, 3 on Starter and 10 on Agency. An address that bounced takes no place.
The account owner’s own address is confirmed already. Any other address receives a confirmation email with a link first, and gets no leads until the link is used. Each recipient gets their own message, so recipients do not see each other’s addresses. You can add a client and yourself to the same form without either seeing the other’s address.
Emails come from FormParcel’s own notify address, not from your client’s domain.
## The reply address
The first field called `email, email_address, your_email, mail, contact_email, _replyto, replyto, reply_to, reply-to` that holds exactly one plain address becomes the Reply-To, so replying goes to the visitor. Without such a field, no reply address is set and replying will not reach the visitor. Name the field accordingly if you want replies to work.
## The subject
A hidden `_subject` field adds a single-line suffix to the email subject, up to 80 characters. It is an ordinary field too, so it also appears in the Sheet.
## Sending limits
Each email destination sends at most 60 messages an hour and 300 a day. Above that, leads wait and are sent later. Nothing is dropped. Limits set by the email provider work the same way.
## Paused addresses
When an address bounces permanently or reports a message as spam, FormParcel stops emailing that one address, for every customer, and tells the form’s owner. Other recipients of the same form are not affected. A paused address is not retried automatically. The owner can retry an address that bounced on one of their own messages once the problem is fixed.
## What Delivered means for email
For email, Delivered means the email provider accepted the message for the confirmed recipients. It does not mean the message reached an inbox. The receipt shows one state for each destination, and a lead is called delivered only when both its Sheet row and its email are. If an email does not arrive, go through [Troubleshooting](https://formparcel.live/docs/help/troubleshooting#lead-email-did-not-arrive).
---
---
title: "Spam protection"
description: "The FormParcel bot check runs on every form: one script line, no cookies, no widget. Plus the Spam folder, a trap field, rate limits and optional Turnstile."
url: "https://formparcel.live/docs/setup/spam-protection"
---
# Spam protection
The FormParcel bot check runs on every form: one script line, no cookies, no widget. Plus the Spam folder, a trap field, rate limits and optional Turnstile.
## What it does
Every form has these, with nothing to turn on:
- **FormParcel’s bot check.** A small script, `formparcel.js`, does a short piece of work in the visitor’s browser and adds a proof to the form. A submission without a valid proof is not delivered. It waits in the Spam folder.
- **The Spam folder.** Submissions that look automated or repeated are stored there instead of being delivered. You can release a real lead with **Not spam**.
- **The trap field.** A hidden field that people never see and bots fill in. A post that fills it is dropped.
- **Rate limits and sender rules.** Limits on how fast one sender can post, and rules for repeated and duplicate messages.
- **A flood alert.** When a form gets far more submissions than usual, the bot check gets harder and the dashboard tells you.
On top of that, a client site can turn on Cloudflare Turnstile with that client’s own free keys. It is optional: see [Turnstile](https://formparcel.live/docs/setup/spam-protection#turnstile).
## The bot check
The bot check is on for every form. The only setup is one script line on the page with the form. The code in your dashboard already includes it, and so does every example in these docs.
Example form address. Use yours from the dashboard.
```html title="After the form"
```
The script is served from the same host as your form address, at `/f/formparcel.js`. It sets no cookies, stores nothing in the browser, shows no widget and loads nothing from a third party. Its only request is for a challenge, to the host the form already posts to.
### How it works
1. The script finds each form on the page whose `action` is a FormParcel form address on that host.
2. It asks FormParcel for a signed challenge for that form.
3. In the background, it finds a number that makes the challenge’s SHA-256 hash start with 16 zero bits. That takes about 65,000 hashes, well under a second on a phone. The page stays responsive while it works.
4. It adds the result to the form as the hidden field `_fp_proof`. FormParcel checks it and never saves it with the lead.
FormParcel accepts a proof only when all of these hold:
- It was made for this form, and the challenge was signed by FormParcel.
- The challenge is less than 30 minutes old. The script fetches a fresh one before that, so a page left open keeps working.
- At least 2 seconds have passed since the challenge was issued. A person filling in a form takes longer than that. If a visitor sends sooner, the script waits until the proof is old enough, then sends.
- The challenge has not been used before. Each challenge works for one submission.
### Sending with fetch
A page that sends leads with `fetch()` loads the same script and asks it for a proof before each send:
```js title="send-lead.js"
const _fp_proof = await window.FormParcel?.proof("https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE").catch(() => undefined);
// Send it with the other fields: JSON.stringify({ ...data, _fp_proof })
```
If the script did not load, `proof` is not there and the lead is sent without a proof. It is still saved, and it waits in the Spam folder. The full sample is in [the fetch guide](https://formparcel.live/docs/guides/fetch).
### Without a valid proof
A submission without a valid proof is not refused, because a real visitor whose browser blocked the script should not lose their message. It is stored in the Spam folder: not delivered and not counted toward your plan. The receipt says Held as spam and gives the reason:
- **Sent without FormParcel's bot check (formparcel.js).** The post carried no proof. Usually the page is missing the script line.
- **Failed FormParcel's bot check.** The proof was there but did not pass: it was for another form, expired, made too fast or did not have enough work.
Release a real lead with **Not spam**. It is then counted and delivered. The visitor and the sending script get the same answer a lead gets, so a bot learns nothing it could tune against. The dashboard shows **Bot check not installed** for the form under Needs action.
Only a visitor’s browser can make a proof. A post from your server, a site builder’s webhook or `curl` never carries one, so its leads always wait here. See [Bot check](https://formparcel.live/docs/reference/bot-check#who-cannot-send-the-proof).
A proof used a second time is a replay. That post is held in the Spam folder too, never dropped: a visitor who sends again after an error sends the same proof, and their lead must not be lost.
### Is it installed
The form’s **Spam & security** page shows the bot check’s state from the last 7 days of submissions:
- **Bot check working.** Submissions came with the bot check. Nothing to do.
- **Bot check not installed.** Submissions came without it. The page shows the script line to add.
- **Bot check missing on some submissions.** Some came with it and some without. A page with the form may be missing the script, or a visitor’s browser blocked it.
- **No recent submissions.** Nothing to tell yet.
### Sites with a Content-Security-Policy
If the client’s site sends a `Content-Security-Policy` header, it must allow the form endpoint host, `https://f.formparcel.live` in this example, in both `script-src` (to load the script) and `connect-src` (to fetch the challenge). Otherwise the browser blocks the script and every lead waits in the Spam folder.
### Test leads and Turnstile
A lead sent with **Send a test lead** from the dashboard skips the bot check. A post that passed the site’s Turnstile check does not need a proof either. Keep the script on a Turnstile site anyway: when Cloudflare cannot answer, the lead is accepted without the Turnstile check, and then the proof decides whether it is delivered or held.
## The Spam folder
Besides a missing or failed bot check, these put a submission in the Spam folder: a third submission from one sender to a form in 24 hours, the same message twice in 24 hours, 3 or more links, or link markup. A held submission is stored encrypted like a lead, is never delivered and never counted, and is deleted after 30 days. Spam is not counted toward your limit.
More than 10 submissions from one sender to your workspace in a day are held as well.
Choose **Not spam** to release a real lead. Choose **Mark as spam** on a lead that got through: nothing more is delivered for it, and for 30 days every submission from that sender to your workspace goes to the Spam folder instead of being delivered. It is held, not refused, because many real people can share one network address, on a mobile network or in an office. A lead that was already delivered still counts toward your plan; one that was not gives its count back.
The Spam folder holds up to ten times your monthly lead limit each month. Once it is full, a submission that would be held is refused instead, with status `429` and the code [`SPAM_FOLDER_FULL`](https://formparcel.live/docs/reference/responses#spam-folder-full), and the visitor is told the form cannot take more messages. The dashboard tells you. Leads that pass every check are still accepted.
## The trap field
Every form has a hidden trap field, `_gotcha` by default. Keep it in the form, hidden from people. A post that fills it is dropped: nothing is saved and no receipt is created. How each kind of request is answered is on [special fields](https://formparcel.live/docs/reference/fields#trap-field).
## Rate limits
Each form has limits on how fast one sender can post, and on how many leads a form and a workspace accept. Past a limit, a submission is refused with 429, except one: more than 10 submissions from one sender to a workspace in a day are held in the Spam folder, not refused, because an office or a mobile network can put many people on one address. The numbers are on [Limits and retention](https://formparcel.live/docs/reference/limits#rate-limits). The challenge the script fetches is limited too: 30 a minute per sender, far more than a page needs.
## Flood alert
A form counts as flooded when, within an hour, it holds or drops 30 submissions, or accepts more than 5 times its usual hourly leads (checked once it has 50 or more in that hour). For the next hour:
- The bot check gets harder: 20 zero bits, about a million hashes, a few seconds in a browser. Real visitors can still send.
- The dashboard shows “*form name* is being flooded.” with a link to the form, where you can pause it if the flood goes on.
No email is sent for a flood yet. You see it when you open the dashboard.
## What this does not stop
- **The allowed-origins list does not stop scripts.** A browser sends an honest `Origin` header, but a script can send any value it likes. The list keeps other websites from posting your form from their pages. It is not a bot check.
- **A determined attacker can get through.** Someone who runs real browsers, solves every check and sends slowly can still post. The bot check makes each submission cost them computing time. The Spam folder, the sender and duplicate rules, and **Mark as spam** handle what remains.
- **Junk already in a Sheet stays there.** FormParcel does not delete rows it has written. Every row carries its submission ID in column A, so you can find a junk lead’s row by its receipt ID and delete it.
## Turnstile: an optional extra layer
For a stronger check on top of the bot check, you can turn on Cloudflare Turnstile for a client site. A visitor passes a quick check before the form sends. A visitor who does not pass is refused, never becomes a lead and never gets a receipt.
- It is **optional and set per client site**, so it covers every form on that site. It is off until you turn it on.
- It uses **the client’s own Cloudflare keys**, not a shared FormParcel widget. The secret key is saved encrypted and is never shown again.
- [The auto-reply](https://formparcel.live/docs/setup/autoresponder) needs it, because that email goes to an address a stranger typed.
### Create a free widget for the client’s domain
1. In the Cloudflare dashboard, open **Turnstile** and choose **Add widget**. A free Cloudflare account is enough.
2. Add the client’s domain as the hostname, for example `client.example`. A hostname also covers its subdomains.
3. Choose the **Managed** widget mode and create the widget.
4. In FormParcel, open the client site and choose **Edit site**. Paste the **site key** and the **secret key**, turn on **Check visitors with Turnstile** and save.
5. Copy the updated code from the form’s **Setup** tab into the client’s site. It now holds the widget as well as the bot check.
**Why one widget per client.** A free Turnstile widget covers up to 10 hostnames and takes no wildcards, so a single widget cannot serve many clients. Making one per client site also keeps each client’s keys, and their Cloudflare account, their own.
**Which sites are accepted.** A check passes only when the widget was served from the site’s domain or from a host in the form’s [allowed origins](https://formparcel.live/docs/reference/limits#allowed-origins), including their subdomains. If you test from another host, such as a development address, add it to the allowed origins.
### What the form needs
The widget adds a field named `cf-turnstile-response` to the form. FormParcel reads it, checks it with Cloudflare and never saves it with the lead. The code in your dashboard already includes the widget while the check is on. This is the idea in a plain HTML form:
```html title="contact.html"
```
From a script, send the token as the `cf-turnstile-response` field in the JSON body, and ask the widget for a fresh one after every send, because a token works once:
```js title="send-lead-with-spam-check.js"
// Load https://challenges.cloudflare.com/turnstile/v0/api.js once, and put
// on the page with your form.
// Keep FormParcel's bot check too (no cookies, no widget):
//
async function submitLead(data) {
const fp = window.FormParcel || await new Promise((ok) => { addEventListener("formparcel:ready", () => ok(window.FormParcel), { once: true }); setTimeout(ok, 10000); });
const _fp_proof = await fp?.proof("https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE").catch(() => undefined);
const response = await fetch("https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ ...data, _fp_proof, "cf-turnstile-response": window.turnstile?.getResponse() }),
});
window.turnstile?.reset(); // a token works once: ask the widget for a fresh check
const result = await response.json();
if (response.status === 403 && result.code === "SPAM_CHECK_FAILED") {
// The visitor did not pass the check. Ask them to complete it and send again.
}
return result;
}
```
### What visitors see
A visitor who has not passed the check (no token, or a token that is invalid, expired, already used or made on another site) is refused with status 403 and the code [`SPAM_CHECK_FAILED`](https://formparcel.live/docs/reference/responses#spam-check-failed). A script gets this JSON:
```json title="Refusal"
{
"success": false,
"error": "Please complete the spam check and try again.",
"code": "SPAM_CHECK_FAILED"
}
```
A browser form post gets a short page titled “Please complete the spam check and try again”, which says the message was not sent and to go back, complete the check and send it again. Nothing was saved, so there is no receipt. Spam is not counted toward your limit. FormParcel counts these refusals per form and shows the count in the form’s settings.
### If Cloudflare is down or the key is wrong
A real lead is never lost to a spam check. When the check itself cannot be made, the lead is accepted without it:
- Cloudflare does not answer within 3 seconds, or answers with a server error.
- Cloudflare rejects the site’s secret key, or FormParcel can no longer read the key it saved.
The lead’s receipt then says the spam check could not be made, and the bot check applies to it as it does on a site without Turnstile. If the cause is the secret key, the dashboard shows **The spam check is not working** on the site and the form, and says to paste the secret key again. While the check is not working, the auto-reply sends nothing, because it only goes to visitors who passed a real check.
### Test leads
A lead sent with **Send a test lead** from the dashboard skips the Turnstile check and the bot check, so you can test a form without the widget or the script.
---
---
title: "Auto-reply"
description: "An optional thank-you email to the person who filled in a form. It is off until you turn it on, needs the site's spam check, and has daily limits."
url: "https://formparcel.live/docs/setup/autoresponder"
---
# Auto-reply
An optional thank-you email to the person who filled in a form. It is off until you turn it on, needs the site's spam check, and has daily limits.
## What it is
The auto-reply is **optional and set per form**. It is off unless you turn it on. While it is off, only the site owner and the recipients you add are notified, and the visitor gets no email.
It is an acknowledgement to the visitor, not a delivery channel. It never changes whether a lead counts as delivered, and a problem with it never touches the lead.
## Before you turn it on
- **The site’s spam check must be on and working.** The reply goes to an address a stranger typed, so it goes only to visitors who passed the site’s own Turnstile check. Set it up first: [spam protection](https://formparcel.live/docs/setup/spam-protection).
- **Write a subject and a message.** The subject can be up to 120 characters and the message up to 2,000 characters, in plain text.
- **The form needs an email field.** The reply goes to the address in the first field called `email, email_address, your_email, mail, contact_email, _replyto, replyto, reply_to, reply-to` that holds exactly one plain address.
- **A reply address.** A visitor who replies writes to the form's first confirmed recipient, or to your own sign-in address when the form has none. It is never the visitor’s own address.
## Writing the message
The only placeholder is `{{name}}`, which becomes the visitor’s name from a field called `name, full name, your name, contact name, first name, fname, given name`. The name is cleaned first: markup, links, digits and symbols are removed and it is cut to 40 characters. When the form has no usable name, `{{name}}` and the space before it are left out, so write `Hi {{name}},`, which then reads “Hi,”, rather than a greeting that needs a name. Any other text in double braces is refused when you save.
The dashboard shows a preview built by the same code that builds the email, with a sample name.
## Links
**A link in your subject or message may only point to the client’s own domain** (the site’s domain or one of its subdomains). Any other web address, any email address and any bare domain such as `another-site.example` is refused when you save. The rule errs on the side of refusing, so it can also catch text such as `Mon.Fri`; add a space. A site with no domain set cannot have links at all. The text is checked again before every send.
## What every email carries
- A line saying why the person got it: “You are getting this because your email address was entered in a form on your site”, and that they can ignore it if it was not them.
- On the Free plan every auto-reply ends with a “Sent via FormParcel” line. Starter and Agency auto-replies do not.
- A sender address that is separate from the one that sends owner emails.
## Limits
An auto-reply emails strangers, so it has limits that protect them and the sending reputation of every customer. They count over a rolling day, and these are the current defaults.
| Limit | Value |
| --- | --- |
| One address, one form | 1 |
| One address, across all of FormParcel | 3 |
| One email domain, one form | 10 |
| One email domain, across all of FormParcel | 20 |
| One form | 100 |
| One workspace | 30 |
| All of FormParcel | 40 |
The domain limits do not apply to free mailbox providers such as Gmail, which share one domain between many people. An address is counted in a canonical form, so a `+tag` on any address, or dots in a Gmail address, do not get around the limit. An auto-reply over a limit is **skipped, never retried and never saved for tomorrow**, and the lead is unaffected.
## When a reply is skipped
The receipt of every lead on a form with the auto-reply on has an auto-reply line: Sent, Skipped (with the reason), Failed or Queued. A reply is skipped when:
- the lead was a test lead from the dashboard;
- the lead has no usable email address, or that address bounced or reported spam before;
- the site’s spam check is off or not working, or could not be made for this lead (the lead was accepted unchecked);
- a limit above was reached;
- the form was paused or archived, the account is being deleted, or you turned the auto-reply off before it was sent;
- the lead’s content was deleted before the reply went out;
- the text has a link that does not lead to the client’s site;
- the lead was counted past the plan's monthly limit, which pauses its auto-reply;
- it could not be sent within 12 hours, after which it is not sent at all.
The receipt does not name another customer’s activity: a limit counted across FormParcel only says that a daily limit was reached.
## What FormParcel keeps
While the email waits to be sent, the visitor’s address and greeting name are kept encrypted. Once it has been sent or skipped they are deleted, along with the text. The keyed hashes that count an address and its domain against the limits are cleared 48 hours later. Deleting a lead’s content cancels a reply that has not been sent. See the [privacy policy](https://formparcel.live/privacy).
---
---
title: "Plain HTML form"
description: "Post a plain HTML form to your form address, with one script line for the bot check. A hidden _next field sends visitors to your own page."
url: "https://formparcel.live/docs/guides/html"
---
# Plain HTML form
Post a plain HTML form to your form address, with one script line for the bot check. A hidden _next field sends visitors to your own page.
Untested: Not yet tested on a live site. The steps follow how the form address works today; if one differs, [tell us](https://formparcel.live/contact).
## The form
Set the form’s `action` to your form address and its `method` to `POST`. Keep your own markup and styling. The dashboard gives you this snippet with your address filled in.
Example form address. Use yours from the dashboard.
```html title="contact.html"
```
| Attribute | Value | Notes |
| --- | --- | --- |
| `action` | Your form address | Copy it from the form’s page in the dashboard. It does not change. |
| `method` | `POST` | Only POST is accepted. A GET request is answered with [405](https://formparcel.live/docs/reference/responses#method-not-allowed). |
| `enctype` | Leave it unset, or `multipart/form-data` | The default encoding works. `multipart/form-data` is accepted for text fields only. |
## Field names
A field is sent only if it has a `name`. Each name becomes a column in the Sheet. If a form starts sending a name the Sheet has no column for, the lead is saved and waits as New fields to review. Nothing is dropped, and nothing is written to the Sheet until you review the new field in the dashboard. There you can add it as a column, or ignore it, for example a field a spam bot made up. An ignored field is left out of the Sheet, the lead keeps it, and you can stop ignoring it later in the form’s settings.
When several fields share a name, for example a group of checkboxes, their values are stored together as one value.
## The spam trap
`_gotcha` is a field people should never see. Bots tend to fill in every field they find. If it arrives with any value, FormParcel drops the post: nothing is saved and no receipt is created. Hide the field by moving it off-screen, as the snippet does, and keep `tabindex="-1"` and `autocomplete="off"` on it so keyboards and password managers skip it.
## The bot check
The script line after the form is FormParcel’s bot check:
```html title="After the form"
```
It does a short piece of work in the visitor’s browser and adds a hidden `_fp_proof` field to the form. It sets no cookies and shows nothing. Keep it on every page that has the form. Without it, or with JavaScript turned off, the form still posts and the lead is saved, but it waits in your Spam folder until you release it with **Not spam**. If the site has a Content-Security-Policy, allow the form endpoint host in `script-src` and `connect-src`. How it works is on [Spam protection](https://formparcel.live/docs/setup/spam-protection#the-bot-check).
For a stronger check, a client site can turn on Cloudflare Turnstile: [spam protection](https://formparcel.live/docs/setup/spam-protection). The trap’s name, and what a script or a browser gets back when it is filled, are on [special fields](https://formparcel.live/docs/reference/fields#trap-field).
> **Bot check.** A lead sent without FormParcel's bot check is saved but held in your Spam folder, not delivered. That is every post from a server, a webhook or `curl`, and every post from a page that does not load the script. The dashboard then shows **Bot check not installed** under Needs action. Fix: keep the script line on every page that has the form. [Learn more](https://formparcel.live/docs/reference/bot-check)
## Your own thank-you page
Add a hidden `_next` field with the full address of a page on the client’s own site. After the lead is saved, FormParcel redirects the visitor there. Without it, the visitor sees a short FormParcel confirmation page. FormParcel only follows an address it can trust, and the rule is on [special fields](https://formparcel.live/docs/reference/fields#redirects).
## What the visitor sees
A browser form post gets one of two things: a redirect to a trusted `_next`, or a short, neutral FormParcel page such as “Your message was sent”, or a plain explanation when something went wrong. Neither shows the receipt ID, and each page has a “Report abuse” link to our [contact page](https://formparcel.live/contact). Anything that is not a browser form post gets JSON instead: [the fetch guide](https://formparcel.live/docs/guides/fetch). Every answer is on [Responses and error codes](https://formparcel.live/docs/reference/responses).
## What is not accepted
- File inputs. A file in the post is refused ([`FILE_UPLOAD_NOT_SUPPORTED`](https://formparcel.live/docs/reference/responses#file-upload-not-supported)), so leave them out of the form.
- Fields named like a password or a card detail. They are refused ([`PROHIBITED_FIELD`](https://formparcel.live/docs/reference/responses#prohibited-field)).
- A post with no readable value in any field ([`EMPTY_SUBMISSION`](https://formparcel.live/docs/reference/responses#empty-submission)).
The limits on size and fields are on [Limits and retention](https://formparcel.live/docs/reference/limits).
## Check it
Send a test lead from the form’s page in the dashboard, then post the form once from the client’s site and open its receipt under **Leads**. If nothing arrives, go through [Troubleshooting](https://formparcel.live/docs/help/troubleshooting).
---
---
title: "JavaScript fetch"
description: "Send a lead with fetch and keep the visitor on the page. A 200 means the lead is stored. Read deliveryStatus, and never resend a pending lead."
url: "https://formparcel.live/docs/guides/fetch"
---
# JavaScript fetch
Send a lead with fetch and keep the visitor on the page. A 200 means the lead is stored. Read deliveryStatus, and never resend a pending lead.
Untested: Not yet tested on a live site. The steps follow how the form address works today; if one differs, [tell us](https://formparcel.live/contact).
## Send the lead
`POST` a JSON object to your form address. Anything that is not a browser page navigation (fetch, XHR, curl, a server) gets JSON back, for success and for errors.
Example form address. Use yours from the dashboard.
```js title="send-lead.js"
// FormParcel's bot check: load this once on the page (no cookies, no widget). Without it, submissions wait in your Spam folder.
//
async function submitLead(data) {
// One proof per submission. The bot check may still be loading on a slow phone, so wait for it (10 seconds at most). If it cannot load, the lead is still sent and waits in your Spam folder.
const fp = window.FormParcel || await new Promise((ok) => { addEventListener("formparcel:ready", () => ok(window.FormParcel), { once: true }); setTimeout(ok, 10000); });
const _fp_proof = await fp?.proof("https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE").catch(() => undefined);
const response = await fetch("https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ ...data, _fp_proof }),
});
// 204: the post was dropped as spam (the trap field was filled). Nothing was saved.
if (response.status === 204) return { accepted: false };
const result = await response.json();
// Accepted: { accepted: true, deliveryStatus: "delivered" | "pending", submissionId: "sub_..." }
// "pending" means the lead is saved and delivery is still in progress. Do not send it again.
// Refused: { success: false, error: "...", code: "..." } with a 4xx status. A 429 or 503 says when to retry (Retry-After).
return result;
}
```
Load FormParcel’s bot check once on the page, and ask it for a proof before every send:
```html title="On the page"
```
`window.FormParcel.proof(address)` gives one proof for one submission. Send it as the `_fp_proof` field of the JSON body. It resolves once the proof is ready and at least 2 seconds old, so the first send after the page loads can wait a moment. A lead sent without a valid proof is still saved, and it waits in your Spam folder instead of being delivered. A proof sent a second time is held in your Spam folder too, so ask for a new one before every send. See [Spam protection](https://formparcel.live/docs/setup/spam-protection#the-bot-check).
> **Note** A 200 means Accepted, not Delivered. Delivery runs after the answer. The receipt ID finds the lead.
A browser form post gets a page or a redirect instead, and a request counts as one only when it accepts `text/html`, its `Sec-Fetch-Mode` is missing or `navigate`, and it did not ask for JSON. A `fetch` call never does, even with a form-encoded body that sets `_next`.
> **Bot check.** A lead sent without FormParcel's bot check is saved but held in your Spam folder, not delivered. That is every post from a server, a webhook or `curl`, and every post from a page that does not load the script. The dashboard then shows **Bot check not installed** under Needs action. Fix: load the script on the page and send the proof from `FormParcel.proof()` with each `fetch`. [Learn more](https://formparcel.live/docs/reference/bot-check)
## Read the answer
An accepted lead comes back with status 200. The lead is saved before this answer is sent.
```json title="200 OK application/json"
{
"accepted": true,
"deliveryStatus": "pending",
"submissionId": "sub_m1x8k2q4_Zk3v..."
}
```
- **accepted boolean**: Always `true` on a 200.
- **deliveryStatus"delivered" or "pending"**: How far delivery got in the 2 seconds FormParcel waits before it answers. `delivered`: every destination confirmed in time and no Sheet column was waiting for review. `pending`: the lead is stored and delivery carries on.
- **submissionId string, starts with sub_**: The receipt ID. Quote it when you ask about a lead.
Both values are success. Show your thank-you message either way. **Do not send a `pending` lead again**: it is stored and FormParcel keeps delivering it, and a second post makes a second lead with its own receipt ID.
## Handle a refusal
A refused request has a 4xx or 5xx status and a JSON body with `code`, `error` and `"success": false`. A few refusals send no `code`. The `error` text is written for people and can change: branch on the HTTP status, then on `code`. No refused request creates a receipt.
```json title="429 Too Many Requests application/json"
{
"success": false,
"error": "This form cannot take more messages this month. Please contact the business another way.",
"code": "PLAN_LIMIT_REACHED"
}
```
A 204 is not an error. It means the post was dropped as spam because the trap field was filled. Nothing was stored, so show no success message. A bot check proof sent a second time is different: that lead is stored and held in the Spam folder, and the answer is the usual 200. A real visitor never fills the trap field in, so leave it out of your JSON unless you fill it on purpose, and get a new proof for every send.
Every status and code, with what to do about it, is on [Responses and error codes](https://formparcel.live/docs/reference/responses#codes).
## When to retry
- **200**: never send the lead again, whatever `deliveryStatus` says.
- **429**: wait the seconds in `Retry-After`, then try once more. Not for [`PLAN_LIMIT_REACHED`](https://formparcel.live/docs/reference/responses#plan-limit-reached), which has no `Retry-After`.
- **500** or **503**: try once more. A 503 asks you to wait 5 seconds.
- **403** with [`SPAM_CHECK_FAILED`](https://formparcel.live/docs/reference/responses#spam-check-failed): ask the visitor to complete the spam check, then send the post with the fresh token.
- Any other **4xx**: do not retry. The same request fails the same way. Fix the request or the form’s settings.
## Spam check
When the client site has the Cloudflare Turnstile check on, the form must send the widget’s token in the `cf-turnstile-response` field. A token works once, so ask the widget for a fresh one after every post. Keep sending the bot check’s proof too: it decides whether the lead is delivered when Cloudflare cannot answer.
```js title="send-lead-with-spam-check.js"
// Load https://challenges.cloudflare.com/turnstile/v0/api.js once, and put
// on the page with your form.
// Keep FormParcel's bot check too (no cookies, no widget):
//
async function submitLead(data) {
const fp = window.FormParcel || await new Promise((ok) => { addEventListener("formparcel:ready", () => ok(window.FormParcel), { once: true }); setTimeout(ok, 10000); });
const _fp_proof = await fp?.proof("https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE").catch(() => undefined);
const response = await fetch("https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ ...data, _fp_proof, "cf-turnstile-response": window.turnstile?.getResponse() }),
});
window.turnstile?.reset(); // a token works once: ask the widget for a fresh check
const result = await response.json();
if (response.status === 403 && result.code === "SPAM_CHECK_FAILED") {
// The visitor did not pass the check. Ask them to complete it and send again.
}
return result;
}
```
## When you get no answer
A timeout or a dropped connection does not tell you whether the lead was accepted. FormParcel has no caller-supplied idempotency key yet, so sending the same lead again can create a second receipt. For a contact form, the safer choice is to tell the visitor it may not have gone through and let them decide. A 503 is different: it says the lead was not accepted.
## Calling from a browser
FormParcel answers the CORS preflight: it allows `POST, OPTIONS` and the headers `Content-Type, Accept, Authorization`, so `fetch` from your site works without a proxy. If you set allowed origins on the form, a request from any other origin is refused ([origin not allowed](https://formparcel.live/docs/reference/responses#origin-not-allowed)). Keep each post to 16 KB or less.
---
---
title: "React"
description: "A React contact form for a single-page app, such as one built with Vite. It loads the bot check, sends JSON with fetch and shows the answer."
url: "https://formparcel.live/docs/guides/react"
---
# React
A React contact form for a single-page app, such as one built with Vite. It loads the bot check, sends JSON with fetch and shows the answer.
Untested: Not yet tested on a live site. The steps follow how the form address works today; if one differs, [tell us](https://formparcel.live/contact).
## The component
Paste this component and render it where the form goes. For Next.js, use [the Next.js guide](https://formparcel.live/docs/guides/nextjs).
Example form address. Use yours from the dashboard.
```jsx title="ContactForm.jsx"
import { useEffect, useState } from "react";
const ENDPOINT = "https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE";
// FormParcel's bot check: no cookies, no widget. Without it, submissions wait in your Spam folder.
const BOT_CHECK_SCRIPT = "https://f.formparcel.live/f/formparcel.js";
export function ContactForm() {
const [status, setStatus] = useState("idle");
useEffect(() => {
if (document.querySelector('script[src="' + BOT_CHECK_SCRIPT + '"]')) return;
const script = document.createElement("script");
script.src = BOT_CHECK_SCRIPT;
script.async = true;
document.head.append(script);
}, []);
async function onSubmit(event) {
event.preventDefault();
setStatus("sending");
const data = Object.fromEntries(new FormData(event.currentTarget));
try {
// One proof per submission. The bot check may still be loading on a slow phone, so wait for it (10 seconds at most). If it cannot load, the lead is still sent and waits in your Spam folder.
const fp = window.FormParcel || await new Promise((ok) => { addEventListener("formparcel:ready", () => ok(window.FormParcel), { once: true }); setTimeout(ok, 10000); });
const _fp_proof = await fp?.proof(ENDPOINT).catch(() => undefined);
const response = await fetch(ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ ...data, _fp_proof }),
});
// 204: the post was dropped as spam (the trap field was filled). Nothing was saved.
const result = response.status === 204 ? null : await response.json();
// "pending" is success too: the lead is stored. Never send it again.
setStatus(result?.accepted ? "sent" : "error");
} catch {
setStatus("error");
}
}
if (status === "sent") return
Thanks, we got your message.
;
return (
);
}
```
- A `200` with `accepted: true` is success, whether `deliveryStatus` is `"delivered"` or `"pending"`. Never send a pending lead again.
- A `204` means the hidden trap field was filled: nothing was saved.
- Every refusal and its code is on [Responses and errors](https://formparcel.live/docs/reference/responses).
> **Bot check.** A lead sent without FormParcel's bot check is saved but held in your Spam folder, not delivered. That is every post from a server, a webhook or `curl`, and every post from a page that does not load the script. The dashboard then shows **Bot check not installed** under Needs action. Fix: load the script on the page and send the proof from `FormParcel.proof()` with each `fetch`. [Learn more](https://formparcel.live/docs/reference/bot-check)
## Check it
Send a test lead from the dashboard, then submit the form once from the deployed site and open its receipt under **Leads**.
---
---
title: "Next.js"
description: "Two ways to send a Next.js contact form to FormParcel: a plain form with no client JavaScript, or a client component that calls fetch and reads the answer."
url: "https://formparcel.live/docs/guides/nextjs"
---
# Next.js
Two ways to send a Next.js contact form to FormParcel: a plain form with no client JavaScript, or a client component that calls fetch and reads the answer.
Untested: Not yet tested on a live site. The steps follow how the form address works today; if one differs, [tell us](https://formparcel.live/contact).
## Before you start
You need a form address from the dashboard. It looks like `https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE`, and your own ID replaces the last part. If you have not made one yet, start with [the quickstart](https://formparcel.live/docs/quickstart). This guide is for the App Router.
## Option 1: a plain form
A Server Component can render a normal HTML form that posts straight to FormParcel. No React code runs in the browser, so it also works before the page has hydrated. The visitor is sent to the page in `_next` afterwards. The one `
```
- `_next` must be a page on the client’s own site, and the site’s domain must be set in FormParcel. Otherwise the visitor sees a short FormParcel confirmation page. The trust rule is on [special fields](https://formparcel.live/docs/reference/fields#redirects).
- Keep the `_gotcha` field hidden. It is the spam trap: a real visitor never fills it in.
- Keep the script line after the form. It is FormParcel’s bot check, and `is:inline` makes Astro leave it as it is. Without it, leads wait in your Spam folder. See [Spam protection](https://formparcel.live/docs/setup/spam-protection#the-bot-check).
- Every field needs a `name`. Each name becomes a column in the Sheet.
> **Bot check.** A lead sent without FormParcel's bot check is saved but held in your Spam folder, not delivered. That is every post from a server, a webhook or `curl`, and every post from a page that does not load the script. The dashboard then shows **Bot check not installed** under Needs action. Fix: keep the script line on every page that has the form. [Learn more](https://formparcel.live/docs/reference/bot-check)
## Optional: submit with fetch
If you would rather keep the visitor on the page, add a small script that submits with fetch and shows a message. The form still works without it. Keep the bot check’s script line: the script asks it for a proof before each send.
```html title="In the same file"
```
- With fetch the answer is JSON. `deliveryStatus: "pending"` still means the lead was accepted: do not send it again. Every status is on [the fetch guide](https://formparcel.live/docs/guides/fetch).
- If you set allowed origins on the form, add your site’s host, and `localhost` while you test locally.
## Check it
Submit the form once and open its receipt in the dashboard under **Leads**. If nothing arrives, go through [Troubleshooting](https://formparcel.live/docs/help/troubleshooting).
---
---
title: "WordPress"
description: "Paste a FormParcel form into a Custom HTML block or Elementor's HTML widget. Contact Form 7, WPForms and webhooks post from a server and are held in Spam."
url: "https://formparcel.live/docs/guides/wordpress"
---
# WordPress
Paste a FormParcel form into a Custom HTML block or Elementor's HTML widget. Contact Form 7, WPForms and webhooks post from a server and are held in Spam.
Untested: Not yet tested on a live site. The steps follow how the form address works today; if one differs, [tell us](https://formparcel.live/contact).
## Paste the form into a Custom HTML block
In the block editor, add a **Custom HTML** block where the form goes and paste this. The dashboard has the same code with your address filled in.
Example form address. Use yours from the dashboard.
```html title="Custom HTML block"
```
- Set `_next` to the full address of a thank-you page on the same site, or delete that line to show FormParcel’s short confirmation.
- Keep the hidden `_gotcha` field and the script line after the form.
- In Elementor, paste the same code into an **HTML** widget.
> **Bot check.** A lead sent without FormParcel's bot check is saved but held in your Spam folder, not delivered. That is every post from a server, a webhook or `curl`, and every post from a page that does not load the script. The dashboard then shows **Bot check not installed** under Needs action. Fix: keep the script line on every page that has the form. [Learn more](https://formparcel.live/docs/reference/bot-check)
## Form plugins
A plugin form sends its data to WordPress first. Some plugins can then forward it to a webhook, but that post comes from your server, so it carries no bot-check proof and FormParcel holds it in Spam.
| Plugin | Posts to an outside address from the browser | Use instead |
| --- | --- | --- |
| Contact Form 7 | No. A webhook plugin can forward it, from the server. | A Custom HTML block |
| WPForms | No. The Webhooks addon forwards it, from the server. | A Custom HTML block |
| Elementor Pro Form | No. Its Webhook action posts from the server. | Elementor’s HTML widget |
- **Server key for webhooks and server posts** Coming soon: A per-form key for a site builder's webhook or your own server, so a lead posted from a server is delivered instead of held in Spam. Until it ships, use the Custom HTML block.
## Who can save form markup
WordPress may strip `form`, `input` and `script` tags when someone without the right capability saves the page. If the form or the script line disappears, save it with an administrator account.
## Check it
Submit the form once from the live page and open its receipt under **Leads**. If nothing arrives, see [Troubleshooting](https://formparcel.live/docs/help/troubleshooting).
---
---
title: "Webflow"
description: "Point a Webflow form's custom action at your form address and add the bot check script to the site's custom code. Webflow then stops storing the form."
url: "https://formparcel.live/docs/guides/webflow"
---
# Webflow
Point a Webflow form's custom action at your form address and add the bot check script to the site's custom code. Webflow then stops storing the form.
Untested: Not yet tested on a live site. The steps follow how the form address works today; if one differs, [tell us](https://formparcel.live/contact).
## Set the form’s action
Select the Form Block, open its settings, and under **Send to** choose **Custom action**. Paste your form address as the action and set the method to **POST**. The address looks like `https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE`, with your own ID.
- Give every input a **Name** in its settings: the name becomes the Sheet column.
- With a custom action, Webflow no longer stores the form or emails you about it. FormParcel does that.
## Add the bot check script
In **Site settings › Custom code**, paste this into **Footer code** and publish. It adds the proof to every form that posts to FormParcel.
Example form address. Use yours from the dashboard.
```html title="Footer code"
```
> **Bot check.** A lead sent without FormParcel's bot check is saved but held in your Spam folder, not delivered. That is every post from a server, a webhook or `curl`, and every post from a page that does not load the script. The dashboard then shows **Bot check not installed** under Needs action. Fix: keep the script line on every page that has the form. [Learn more](https://formparcel.live/docs/reference/bot-check)
## Check it
Submit the form once from the published site, not the Designer preview, and open its receipt under **Leads**. If nothing arrives, see [Troubleshooting](https://formparcel.live/docs/help/troubleshooting).
---
---
title: "Framer"
description: "Framer's own form sends leads through a webhook from Framer's servers, which FormParcel holds in Spam today. Put a FormParcel form in an Embed instead."
url: "https://formparcel.live/docs/guides/framer"
---
# Framer
Framer's own form sends leads through a webhook from Framer's servers, which FormParcel holds in Spam today. Put a FormParcel form in an Embed instead.
Untested: Not yet tested on a live site. The steps follow how the form address works today; if one differs, [tell us](https://formparcel.live/contact).
## Framer forms
A Framer form with **Send To › Webhook** posts from Framer’s servers, not from the visitor’s browser. It carries no bot-check proof, so each lead is held in your Spam folder.
- **Server key for webhooks and server posts** Coming soon: A per-form key for a site builder's webhook or your own server, so a lead posted from a server is delivered instead of held in Spam. Until it ships, use an Embed.
## Add an Embed
Insert an **Embed** on the page, choose **HTML**, and paste this. The embed sends from the visitor’s browser and runs the bot check. We have not yet tried this route on a live Framer site.
Example form address. Use yours from the dashboard.
```html title="Embed (HTML)"
```
- The embed runs in its own frame, so the visitor sees FormParcel’s short confirmation inside it.
- Leave the form’s allowed origins empty: the post comes from the embed’s frame, not from your domain.
> **Bot check.** A lead sent without FormParcel's bot check is saved but held in your Spam folder, not delivered. That is every post from a server, a webhook or `curl`, and every post from a page that does not load the script. The dashboard then shows **Bot check not installed** under Needs action. Fix: keep the script line on every page that has the form. [Learn more](https://formparcel.live/docs/reference/bot-check)
## Check it
Submit the form once from the published site and open its receipt under **Leads**. If it says **Held as spam**, see [Bot check](https://formparcel.live/docs/reference/bot-check#fix-it).
---
---
title: "Wix"
description: "Put a FormParcel form in a Wix Embed HTML element. Wix Forms and Wix Automations webhooks post from Wix's servers, so FormParcel holds those leads in Spam."
url: "https://formparcel.live/docs/guides/wix"
---
# Wix
Put a FormParcel form in a Wix Embed HTML element. Wix Forms and Wix Automations webhooks post from Wix's servers, so FormParcel holds those leads in Spam.
Untested: Not yet tested on a live site. The steps follow how the form address works today; if one differs, [tell us](https://formparcel.live/contact).
## Wix Forms
A Wix form cannot post to an outside address from the browser. Sending it on with a Wix Automations webhook posts from Wix’s servers, so the lead has no bot-check proof and waits in your Spam folder.
- **Server key for webhooks and server posts** Coming soon: A per-form key for a site builder's webhook or your own server, so a lead posted from a server is delivered instead of held in Spam. Until it ships, use an Embed HTML element.
## Add an Embed HTML element
In the editor, add **Embed Code › Embed HTML**, choose **Code**, and paste this.
Example form address. Use yours from the dashboard.
```html title="Embed HTML"
```
- Wix runs the element in its own frame. The frame cannot send the whole page to a thank-you page, so the visitor sees FormParcel’s short confirmation inside it.
- Leave the form’s allowed origins empty: the post comes from Wix’s frame, not from your domain.
> **Bot check.** A lead sent without FormParcel's bot check is saved but held in your Spam folder, not delivered. That is every post from a server, a webhook or `curl`, and every post from a page that does not load the script. The dashboard then shows **Bot check not installed** under Needs action. Fix: keep the script line on every page that has the form. [Learn more](https://formparcel.live/docs/reference/bot-check)
## Check it
Submit the form once from the published site and open its receipt under **Leads**. If nothing arrives, see [Troubleshooting](https://formparcel.live/docs/help/troubleshooting).
---
---
title: "Squarespace"
description: "Paste a FormParcel form into a Squarespace Code block. Its bot check script runs only on plans that allow JavaScript in Code blocks."
url: "https://formparcel.live/docs/guides/squarespace"
---
# Squarespace
Paste a FormParcel form into a Squarespace Code block. Its bot check script runs only on plans that allow JavaScript in Code blocks.
Untested: Not yet tested on a live site. The steps follow how the form address works today; if one differs, [tell us](https://formparcel.live/contact).
## Add a Code block
Squarespace’s own Form block sends to Squarespace, so it cannot be used. Instead, edit the page, add a **Code** block, set it to **HTML**, and paste this.
Example form address. Use yours from the dashboard.
```html title="Code block"
```
- Set `_next` to a thank-you page on the same site, or delete that line.
- The form itself works on every plan. The script line needs a plan that runs JavaScript in Code blocks: Core or higher at the time of writing. Check your plan before you rely on it.
> **Bot check.** A lead sent without FormParcel's bot check is saved but held in your Spam folder, not delivered. That is every post from a server, a webhook or `curl`, and every post from a page that does not load the script. The dashboard then shows **Bot check not installed** under Needs action. Fix: keep the script line on every page that has the form. [Learn more](https://formparcel.live/docs/reference/bot-check)
## Check it
Submit the form once from the live site and open its receipt under **Leads**. If it says **Held as spam**, the script did not run: see [Bot check](https://formparcel.live/docs/reference/bot-check#fix-it).
---
---
title: "Shopify"
description: "Paste a FormParcel form into a Shopify theme's Custom Liquid section. Shopify's own contact form sends to Shopify, so it cannot post leads to FormParcel."
url: "https://formparcel.live/docs/guides/shopify"
---
# Shopify
Paste a FormParcel form into a Shopify theme's Custom Liquid section. Shopify's own contact form sends to Shopify, so it cannot post leads to FormParcel.
Untested: Not yet tested on a live site. The steps follow how the form address works today; if one differs, [tell us](https://formparcel.live/contact).
## Add a Custom Liquid section
In **Online Store › Themes › Customize**, open the page, choose **Add section › Custom Liquid**, and paste this.
Example form address. Use yours from the dashboard.
```html title="Custom Liquid"
```
- Set `_next` to a page on your store, such as `/pages/thanks` with your store’s full address in front, or delete that line.
- Not every theme has a Custom Liquid section. Without one, add the form to a page template with **Edit code**.
> **Bot check.** A lead sent without FormParcel's bot check is saved but held in your Spam folder, not delivered. That is every post from a server, a webhook or `curl`, and every post from a page that does not load the script. The dashboard then shows **Bot check not installed** under Needs action. Fix: keep the script line on every page that has the form. [Learn more](https://formparcel.live/docs/reference/bot-check)
## Check it
Submit the form once from the live store and open its receipt under **Leads**. If nothing arrives, see [Troubleshooting](https://formparcel.live/docs/help/troubleshooting).
---
---
title: "AI builders and coding agents"
description: "Paste one block of instructions into Lovable, Bolt, v0, Replit, Cursor, Claude Code or Codex, and it adds a form that posts to FormParcel from the browser."
url: "https://formparcel.live/docs/guides/ai"
---
# AI builders and coding agents
Paste one block of instructions into Lovable, Bolt, v0, Replit, Cursor, Claude Code or Codex, and it adds a form that posts to FormParcel from the browser.
Untested: Not yet tested on a live site. The steps follow how the form address works today; if one differs, [tell us](https://formparcel.live/contact).
## Copy the instructions
Copy your form address from the dashboard first. Then copy this block and replace `` with the ID at the end of your address.
```text title="Instructions for your AI"
Add a working contact form to this site using FormParcel (a form backend).
Do not build a backend, database table, API route or server function for it, and never
send the form from a server: FormParcel takes it only from the visitor's browser.
Form address (POST): https://f.formparcel.live/f/
Replace with the ID from the FormParcel dashboard, or ask me for it.
Bot-check script: https://f.formparcel.live/f/formparcel.js
Allowed origins: if the form lists them, posts from any other host get 403, so first tell me to add
your preview or staging host (Lovable, Bolt, v0, Replit) under Allowed origins on the form's Spam tab.
1. Fields: keep the page's own fields, or use name, email and message. Every input needs a
plain name attribute (it becomes the column and the email label). Up to 50 fields, names up to
64 characters, values up to 4,096 characters. No file inputs. Do not start names with "_" (reserved).
Never collect passwords, one-time codes or card numbers: they are refused.
2. Spam trap: put this inside the form, exactly as written, and keep it empty and hidden:
3. Bot check: load this once on every page that has the form. Without it, leads wait in the
owner's Spam folder and are not delivered:
4a. Plain HTML form: copy this tag exactly (its onsubmit waits for the script, which adds its proof):