# 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 (
{/* Spam trap: keep it hidden from people. */}
``` - `_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):
For a thank-you page, add inside the form: The page must be on this site's own domain. 4b. JavaScript form (React, Next.js, Vue): on submit, prevent the default, wait for the script (it may still be loading on a slow phone), get one proof per send, then POST: 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/").catch(() => undefined); TypeScript: declare global { interface Window { FormParcel?: { proof(endpoint: string): Promise } } } POST JSON {...fields, _fp_proof} with headers Content-Type: application/json and Accept: application/json. Show success only when the reply is 200 and accepted === true. 204 means the spam trap was filled: show nothing. On 4xx, show the reply's "error" field. Never resend a lead whose reply said deliveryStatus "pending": it is stored and on its way. 5. If the site sets a Content-Security-Policy, allow https://f.formparcel.live in script-src and connect-src. Testing: never test with curl, a script or from a server: those posts have no bot check, so they go to Spam. Repeated sends from one browser within 24 hours are held in Spam too. When the form is live, tell me to press "Send a test lead" in the FormParcel dashboard, then to send the form once from the live page. A real lead held in Spam is released with Not spam. Docs: guide https://formparcel.live/docs/guides/ai.md · for agents https://formparcel.live/llms.txt ``` ## Paste them into your builder | Builder | Where | Watch for | | --- | --- | --- | | Lovable, Bolt, v0, Replit | The chat, as one message. | These builders like to add their own backend: a database table, an Edge Function or an API route. The instructions forbid it. If it adds one anyway, ask it to remove it and post from the browser. | | Cursor, Claude Code, Codex | The chat or a task, in the repository of the site. | Agents test their work with `curl`. That post has no bot-check proof and lands in Spam, so its 200 answer proves nothing. They can also read [llms.txt](https://formparcel.live/llms.txt) and the Markdown copy of any page. | ## Check what it built - The form’s `action`, or the `fetch` call, uses your form address. - The page loads the bot check script, and a `fetch` sends the proof as `_fp_proof`. - The hidden `_gotcha` field is there and stays empty. - Nothing posts the form from a server. > **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) ## Send a real lead Use **Send a test lead** in the dashboard to check delivery. Then publish the site, submit the form once from the live page, and open the lead under **Leads**. If it is in **Spam**, open it and read the reason: - **Sent without FormParcel's bot check (formparcel.js)** means the page did not send the proof. See [Bot check](https://formparcel.live/docs/reference/bot-check#fix-it). - A third send from one browser to the form in 24 hours is held as a repeat sender, even with the proof. Testing again and again from your own browser ends up there, so test with **Send a test lead** and release a real lead with **Not spam**. - A builder’s preview or staging address gets [origin not allowed](https://formparcel.live/docs/reference/responses#origin-not-allowed) when the form lists allowed origins. Add that host to the list on the form, or post from the live site. --- --- title: "POST /f/{formId}" description: "The form address takes a POST of text fields from the visitor's browser, with the bot check. It needs no API key and answers the browser preflight." url: "https://formparcel.live/docs/reference/endpoint" --- # POST /f/{formId} The form address takes a POST of text fields from the visitor's browser, with the bot check. It needs no API key and answers the browser preflight. ## The address `POST https://f.formparcel.live/f/{formId}` `{formId}` is the form’s own ID, which the dashboard shows on the form’s page as a ready-to-copy address. The address never changes, so you paste it into a client’s site once. It is the only thing that tells FormParcel which form a post is for. ## Where a post comes from Send the post from the visitor’s browser: a plain HTML form, or `fetch` on the page. The page loads FormParcel’s bot check, which adds a proof to each submission. A post from anywhere else, such as your server, a site builder’s webhook, a form plugin that forwards from its own server, or `curl`, carries no proof. It gets the same answer a lead gets, and the lead is held in your Spam folder instead of being delivered. > **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) - **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, post from the browser. ## Methods | Method | What it does | | --- | --- | | `POST` | Sends a lead. This is the only method that stores anything. | | `OPTIONS` | Answers the browser’s CORS preflight, so `fetch` from a client’s site works without a proxy. | | `GET`, `HEAD` | Answered with [405](https://formparcel.live/docs/reference/responses#method-not-allowed) and an `Allow` header that lists `POST, OPTIONS`. A browser that opens the address gets a short page that says it is a form address. | | Any other | Answered with 405 and an empty body, with no `Allow` header. | ## Request body Send the fields as one of these content types. The body is text only. - `application/x-www-form-urlencoded` - `multipart/form-data` - `application/json` - Send UTF-8. A URL-encoded form in another declared charset is decoded as that charset. A JSON or multipart body that is not UTF-8 is refused ([`UNSUPPORTED_CHARSET`](https://formparcel.live/docs/reference/responses#unsupported-charset)). - A JSON body is an object of field names and values. Its keys must be unique at the top level, and it may nest up to 4 levels in total. - A file part is refused ([`FILE_UPLOAD_NOT_SUPPORTED`](https://formparcel.live/docs/reference/responses#file-upload-not-supported)). FormParcel does not take file uploads. - Several fields with the same name, such as a group of checkboxes, are stored together as one value. - A post is at most 16 KB, with at most 50 fields, a name of at most 64 characters and a value of at most 4,096 characters. Every limit, with the code it is refused with, is on [Limits and retention](https://formparcel.live/docs/reference/limits). A few names are read by FormParcel and not stored as lead fields, such as `_next` and `_gotcha`. They are on [Special fields](https://formparcel.live/docs/reference/fields). ## No API key The address takes no API key and no sign-in. A request needs only the address and a body. FormParcel has no management API, command line or MCP server. This address is the only part of it that is documented. Its contract is also published as an [OpenAPI 3.1 file](https://formparcel.live/openapi.json). ## Calling from a browser FormParcel answers the preflight request. It allows the methods `POST, OPTIONS` and the headers `Content-Type, Accept, Authorization`, and lets a browser keep the answer for 86,400 seconds. A 503 carries the same headers, so a browser can read it. If you set allowed origins on the form, a request from any other origin is refused with 403 ([origin not allowed](https://formparcel.live/docs/reference/responses#origin-not-allowed)). How the list is matched is on [Limits and retention](https://formparcel.live/docs/reference/limits#allowed-origins). ## What it answers A browser that submits a plain form gets a page or a redirect. Everything else, whether it is fetch, XHR, curl or a server, gets JSON, for success and for refusals. The rule, the 200 body and every refusal code are on [Responses and error codes](https://formparcel.live/docs/reference/responses). ## Security of the form address The address is public by design: it ends up in the page’s HTML, and anyone who reads the page has it. Treat it as an address, not a secret. - The ID in it is random and carries at least 128 bits, so an address cannot be guessed or counted up to. A wrong ID is answered with [form not found](https://formparcel.live/docs/reference/responses#form-not-found). - What limits abuse is on the form, not in the address: the bot check, the hidden trap field, the rate limits, the allowed origins and, for a client site, the Cloudflare Turnstile check. See [Spam protection](https://formparcel.live/docs/setup/spam-protection) and [Limits and retention](https://formparcel.live/docs/reference/limits). - A post is a stranger’s text, and what it holds is written to the client’s Sheet. Text that a spreadsheet would read as a formula is neutralised before it is written. - Do not put a password, an API key or any other secret in a form page or in a hidden field. A hidden field is visible to anyone who reads the page. ## No idempotency key FormParcel has no caller-supplied idempotency key yet. A request that times out leaves you not knowing whether the lead was accepted, and sending the same lead again can create a second receipt. The advice for that case is on [the fetch guide](https://formparcel.live/docs/guides/fetch#when-you-get-no-answer). --- --- title: "Responses and error codes" description: "A 200 means the lead is stored. It does not mean it is delivered. Every answer the form address gives, and every refusal code." url: "https://formparcel.live/docs/reference/responses" --- # Responses and error codes A 200 means the lead is stored. It does not mean it is delivered. Every answer the form address gives, and every refusal code. ## What a 200 means FormParcel answers only after the lead is written to its database and has a receipt ID. Delivery to the Google Sheet runs after that, on its own, so the answer can reach you before the row reaches the Sheet. Example lead at the moment the 200 is sent Sheet Sheet: Accepted. A 200 with `deliveryStatus` "pending" leaves the lead here. "delivered" means every destination confirmed within the 2-second window. `deliveryStatus` says how far delivery got in the 2 seconds FormParcel waits before it answers: - `"delivered"`: every enabled destination confirmed in time, and no Sheet column mapping was waiting for review. - `"pending"`: the lead is stored and delivery carries on. Its receipt shows how it ends. Both are success. Show the visitor your thank-you message either way, and never send the same lead again. ## Request and response Ask for JSON with `Accept: application/json`. A script, a server or `curl` gets JSON even without it. Example form address. Use yours from the dashboard. ```bash title="request.sh" curl -X POST "https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE" \ -H "Accept: application/json" \ -d "name=Priya Sharma" \ -d "email=priya.sharma@mail.example" \ -d "message=Do you have Saturday appointments?" ``` ```json title="200 OK application/json" { "accepted": true, "deliveryStatus": "pending", "submissionId": "sub_m1x8k2q4_Zk3v..." } ``` A `curl` post like this one carries no bot-check proof, so its lead waits in your Spam folder. It shows the answer; to test delivery, use **Send a test lead** in the dashboard. See [Bot check](https://formparcel.live/docs/reference/bot-check). - **accepted boolean**: Always `true` on a 200. - **deliveryStatus"delivered" or "pending"**: How far delivery got before the answer. See [What a 200 means](https://formparcel.live/docs/reference/responses#what-a-200-means). - **submissionId string, starts with sub_**: The receipt ID. FormParcel also writes it into the `_submission_id` column (column A) of the lead’s Sheet row, so you can find the row from the receipt. ## Which answer you get A browser that submits a plain HTML form gets a page or a redirect. Everything else gets JSON, for success and for refusals alike. | Request | Status | Body | | --- | --- | --- | | fetch, XHR, curl or a server; the lead is accepted. A post without the bot check gets the same answer and waits in Spam | 200 | JSON with accepted, deliveryStatus and submissionId | | A browser form post with a trusted _next or _redirect | 303 | Empty. Location is your thank-you page. | | A browser form post with no trusted redirect | 200 | A short page headed "Your message was sent". No receipt ID is shown to the visitor. | | A script request with the trap field filled | 204 | Empty. Nothing is stored and there is no receipt. | A request counts as a browser form post only when it accepts `text/html`, its `Sec-Fetch-Mode` is missing or `navigate`, and it did not ask for JSON. ## Status and error codes A refusal stores nothing and makes no receipt. The JSON body has `code`, `error` and `"success": false`; refusals marked “No code” leave `code` out. Each code links to its entry further down this page. | Status | When it happens | Code | Retry | | --- | --- | --- | --- | | **Fix the request 400 413 415 422** | | | | | 400 | The JSON cannot be parsed | [INVALID_JSON](https://formparcel.live/docs/reference/responses#invalid-json) | No | | 400 | The body is malformed, or the JSON is not a key-value object | [INVALID_BODY](https://formparcel.live/docs/reference/responses#invalid-body) | No | | 400 | A top-level JSON key appears twice | [DUPLICATE_FIELD](https://formparcel.live/docs/reference/responses#duplicate-field) | No | | 400 | More than 50 fields | [TOO_MANY_FIELDS](https://formparcel.live/docs/reference/responses#too-many-fields) | No | | 400 | A field name is over 64 characters | [FIELD_NAME_TOO_LONG](https://formparcel.live/docs/reference/responses#field-name-too-long) | No | | 400 | A field value is over 4,096 characters | [FIELD_VALUE_TOO_LONG](https://formparcel.live/docs/reference/responses#field-value-too-long) | No | | 400 | JSON nested deeper than 4 levels | [PAYLOAD_TOO_DEEP](https://formparcel.live/docs/reference/responses#payload-too-deep) | No | | 400 | A number that is not finite, such as 1e400 | [NON_FINITE_NUMBER](https://formparcel.live/docs/reference/responses#non-finite-number) | No | | 400 | A multipart body contains a file | [FILE_UPLOAD_NOT_SUPPORTED](https://formparcel.live/docs/reference/responses#file-upload-not-supported) | No | | 400 | No field holds readable content | [EMPTY_SUBMISSION](https://formparcel.live/docs/reference/responses#empty-submission) | No | | 413 | The body is over 16 KB | [PAYLOAD_TOO_LARGE](https://formparcel.live/docs/reference/responses#payload-too-large) | No | | 415 | Not form-encoded, multipart or JSON | [UNSUPPORTED_MEDIA_TYPE](https://formparcel.live/docs/reference/responses#unsupported-media-type) | No | | 415 | A JSON or multipart body that is not UTF-8 | [UNSUPPORTED_CHARSET](https://formparcel.live/docs/reference/responses#unsupported-charset) | No | | 422 | A field named like a password, one-time code or card detail | [PROHIBITED_FIELD](https://formparcel.live/docs/reference/responses#prohibited-field) | No | | **Form settings 400 403 404** | | | | | 400 | The form has no enabled destination yet | [No code: Form not ready](https://formparcel.live/docs/reference/responses#form-not-ready) | No | | 403 | The form is paused | [No code: Form paused](https://formparcel.live/docs/reference/responses#form-paused) | No | | 403 | The page is not on the form's allowed origins | [No code: Origin not allowed](https://formparcel.live/docs/reference/responses#origin-not-allowed) | No | | 404 | No form has this ID | [No code: Form not found](https://formparcel.live/docs/reference/responses#form-not-found) | No | | **Spam check 403** | | | | | 403 | Turnstile is on and the token did not pass | [SPAM_CHECK_FAILED](https://formparcel.live/docs/reference/responses#spam-check-failed) | With a fresh token | | **Slow down 429** | | | | | 429 | Too many requests from one client, across all forms | [CLIENT_RATE_LIMITED](https://formparcel.live/docs/reference/responses#client-rate-limited) | After Retry-After | | 429 | Too many requests from one client to this form | [FORM_RATE_LIMITED](https://formparcel.live/docs/reference/responses#form-rate-limited) | After Retry-After | | 429 | One client's hourly limit in the workspace | [CLIENT_HOURLY_LIMIT](https://formparcel.live/docs/reference/responses#client-hourly-limit) | After Retry-After | | 429 | The form's hourly cap on accepted leads | [FORM_HOURLY_LIMIT](https://formparcel.live/docs/reference/responses#form-hourly-limit) | After Retry-After | | 429 | The workspace's daily cap, on a busy form | [WORKSPACE_DAILY_LIMIT](https://formparcel.live/docs/reference/responses#workspace-daily-limit) | After Retry-After | | **Plan limit 429** | | | | | 429 | The submission would be held as spam, and the workspace's Spam folder is full for the month. Nothing is stored and there is no receipt. | [SPAM_FOLDER_FULL](https://formparcel.live/docs/reference/responses#spam-folder-full) | Not this month as it is | | 429 | The workspace has used three times its monthly leads. Nothing is stored and there is no receipt. | [PLAN_LIMIT_REACHED](https://formparcel.live/docs/reference/responses#plan-limit-reached) | Not this month | | **Our side 500 503** | | | | | 500 | The lead could not be stored | [No code: Lead not saved](https://formparcel.live/docs/reference/responses#not-saved) | Once | | 503 | The form lookup failed. Sends Retry-After: 5. | [SERVICE_UNAVAILABLE](https://formparcel.live/docs/reference/responses#service-unavailable) | After 5 s | | **Wrong method 405** | | | | | 405 | A GET or HEAD request; the answer carries Allow: POST, OPTIONS. Other methods (PUT, PATCH, DELETE) get 405 with an empty body and no Allow header. | [No code: Method not allowed](https://formparcel.live/docs/reference/responses#method-not-allowed) | Use POST | > **Note** The `error` text is written for people and can change. Branch on the HTTP status first, then on `code`. ```json title="429 Too Many Requests application/json" { "success": false, "error": "Rate limit exceeded. Please try again later.", "code": "FORM_RATE_LIMITED" } ``` ## When to retry - **200**: never send the lead again, whatever `deliveryStatus` says. It is stored; a second post makes a second lead with its own receipt ID. - **429**: wait the seconds in `Retry-After`, then try once more. Not for `PLAN_LIMIT_REACHED`, which has no `Retry-After`. - **500** or **503**: try once more. A 503 asks you to wait 5 seconds. - **403** `SPAM_CHECK_FAILED`: ask the visitor to complete the spam check, then send it with the fresh token. - Any other **4xx**: do not retry. Fix the request or the form’s settings. ## Every refusal One entry for each refusal, with what the visitor sees when a browser form post is refused. The anchor of each entry is the code in lower case with dashes, or the name shown for a refusal that sends no code. ### Fix the request 400 413 415 422 #### 400 INVALID_JSON - **When**: The JSON cannot be parsed. - **Retry**: No. The same body fails the same way. - **Visitor**: A browser form post shows "Your message could not be sent". - **What to do**: Send valid JSON, or send the form as a form post. #### 400 INVALID_BODY - **When**: The body is malformed, or the JSON is not a key-value object. - **Retry**: No. The same body fails the same way. - **Visitor**: A browser form post shows "Your message could not be sent". - **What to do**: Send a form post, or a JSON object of field names and values. An array or a bare value is refused. #### 400 DUPLICATE_FIELD - **When**: A top-level JSON key appears twice. - **Retry**: No. The same body fails the same way. - **Visitor**: A browser form post shows "Your message could not be sent". - **What to do**: Send each top-level key once. #### 400 TOO_MANY_FIELDS - **When**: More than 50 fields. - **Retry**: No. The same body fails the same way. - **Visitor**: A browser form post shows "Your message could not be sent". - **What to do**: Send at most 50 fields. #### 400 FIELD_NAME_TOO_LONG - **When**: A field name is over 64 characters. - **Retry**: No. The same body fails the same way. - **Visitor**: A browser form post shows "Your message could not be sent". - **What to do**: Keep each field name to 64 characters or fewer. #### 400 FIELD_VALUE_TOO_LONG - **When**: A field value is over 4,096 characters. - **Retry**: No. The same body fails the same way. - **Visitor**: A browser form post shows "Your message could not be sent". - **What to do**: Keep each value to 4,096 characters or fewer. #### 400 PAYLOAD_TOO_DEEP - **When**: JSON nested deeper than 4 levels. - **Retry**: No. The same body fails the same way. - **Visitor**: A browser form post shows "Your message could not be sent". - **What to do**: Flatten the JSON to at most 4 levels in total. #### 400 NON_FINITE_NUMBER - **When**: A number that is not finite, such as 1e400. - **Retry**: No. The same body fails the same way. - **Visitor**: A browser form post shows "Your message could not be sent". - **What to do**: Send a finite number, or send the value as text. #### 400 FILE_UPLOAD_NOT_SUPPORTED - **When**: A multipart body contains a file. - **Retry**: No. The same body fails the same way. - **Visitor**: A browser form post shows "Your message could not be sent". - **What to do**: Take the file input out of the form. FormParcel does not take file uploads. #### 400 EMPTY_SUBMISSION - **When**: No field holds readable content. - **Retry**: No. Nothing is stored and there is no receipt. - **Visitor**: A browser form post shows "Your message was empty". - **What to do**: Make sure at least one field has a value and every field has a name attribute. A field without a name is not sent. #### 413 PAYLOAD_TOO_LARGE - **When**: The body is over 16 KB. - **Retry**: No. The same body fails the same way. - **Visitor**: A browser form post shows "Your message is too long". - **What to do**: Send 16 KB or less. #### 415 UNSUPPORTED_MEDIA_TYPE - **When**: Not form-encoded, multipart or JSON. - **Retry**: No. The same body fails the same way. - **Visitor**: A browser form post shows "Your message could not be sent". - **What to do**: Send the body as application/x-www-form-urlencoded, multipart/form-data or application/json. #### 415 UNSUPPORTED_CHARSET - **When**: A JSON or multipart body that is not UTF-8. - **Retry**: No. The same body fails the same way. - **Visitor**: A browser form post shows "Your message could not be sent". - **What to do**: Send the body as UTF-8. #### 422 PROHIBITED_FIELD - **When**: A field named like a password, one-time code or card detail. - **Retry**: No. Nothing is stored and there is no receipt. - **Visitor**: A browser form post shows "This form cannot take that information". - **What to do**: Take that field out of the form. FormParcel does not accept passwords, one-time codes or card details. ### Form settings 400 403 404 #### 400 Form not ready No code - **When**: The form has no enabled destination yet. - **Retry**: No. The owner finishes the form's setup first. - **Visitor**: A browser form post shows "This form is not ready yet". - **What to do**: The owner connects a destination to the form in the dashboard. A form accepts leads once it has somewhere to deliver them. #### 403 Form paused No code - **When**: The form is paused. - **Retry**: No. The owner resumes the form first. - **Visitor**: A browser form post shows "This form is not accepting messages right now". - **What to do**: The owner resumes the form in the dashboard. #### 403 Origin not allowed No code - **When**: The page is not on the form's allowed origins. - **Retry**: No. Post from an allowed page, or add the site to the form's allowed origins. - **Visitor**: A browser form post shows "This form cannot be used from here". - **What to do**: Add the page's site to the form's allowed origins, or post from a page that is already on the list. #### 404 Form not found No code - **When**: No form has this ID. - **Retry**: No. Check the address. - **Visitor**: A browser form post shows "We could not find this form". - **What to do**: Copy the form address again from the dashboard. ### Spam check 403 #### 403 SPAM_CHECK_FAILED - **When**: Turnstile is on and the token did not pass. - **Retry**: Yes, once the visitor passes the check, with the fresh token the widget gives. - **Visitor**: A browser form post shows "Please complete the spam check and try again". - **What to do**: Ask the visitor to complete the check, then send the post again with the new token. ### Slow down 429 #### 429 CLIENT_RATE_LIMITED - **When**: Too many requests from one client, across all forms. - **Retry**: Wait the seconds in Retry-After, then send once more. - **Visitor**: A browser form post shows "Please try again in a moment". - **What to do**: Wait the seconds in Retry-After, then send once more. #### 429 FORM_RATE_LIMITED - **When**: Too many requests from one client to this form. - **Retry**: Wait the seconds in Retry-After, then send once more. - **Visitor**: A browser form post shows "Please try again in a moment". - **What to do**: Wait the seconds in Retry-After, then send once more. #### 429 CLIENT_HOURLY_LIMIT - **When**: One client's hourly limit in the workspace. - **Retry**: Wait the seconds in Retry-After, then send once more. - **Visitor**: A browser form post shows "Please try again in a moment". - **What to do**: Wait the seconds in Retry-After, then send once more. #### 429 FORM_HOURLY_LIMIT - **When**: The form's hourly cap on accepted leads. - **Retry**: Wait the seconds in Retry-After, then send once more. - **Visitor**: A browser form post shows "Please try again in a moment". - **What to do**: Wait the seconds in Retry-After, then send once more. #### 429 WORKSPACE_DAILY_LIMIT - **When**: The workspace's daily cap, on a busy form. - **Retry**: Wait the seconds in Retry-After, then send once more. - **Visitor**: A browser form post shows "Please try again in a moment". - **What to do**: Wait the seconds in Retry-After, then send once more. ### Plan limit 429 #### 429 SPAM_FOLDER_FULL - **When**: The submission would be held as spam, and the workspace's Spam folder is full for the month. Nothing is stored and there is no receipt. - **Retry**: Not as it is. A submission that passes every check (with the bot check, from a new sender, with new content) is still accepted. - **Visitor**: A browser form post shows "This form cannot take more messages this month". - **What to do**: Add the bot check script to the form's pages. The owner reviews the Spam folder; it empties at the next UTC month. #### 429 PLAN_LIMIT_REACHED - **When**: The workspace has used three times its monthly leads. Nothing is stored and there is no receipt. - **Retry**: Not this month. There is no Retry-After header. - **Visitor**: A browser form post shows "This form cannot take more messages this month". - **What to do**: The owner upgrades the plan, or waits for the next UTC month. ### Our side 500 503 #### 500 Lead not saved No code - **When**: The lead could not be stored. - **Retry**: Yes, once. The lead was not accepted. - **Visitor**: A browser form post shows "Something went wrong on our side". - **What to do**: Send the post again once. If it keeps failing, tell us the time of the request. #### 503 SERVICE_UNAVAILABLE - **When**: The form lookup failed. Sends Retry-After: 5. - **Retry**: Yes, after 5 seconds. The lead was not accepted. - **Visitor**: A browser form post shows "Something went wrong on our side". - **What to do**: Send the post again after 5 seconds. ### Wrong method 405 #### 405 Method not allowed No code - **When**: A GET or HEAD request; the answer carries Allow: POST, OPTIONS. Other methods (PUT, PATCH, DELETE) get 405 with an empty body and no Allow header. - **Retry**: Send a POST instead. - **Visitor**: A browser form post shows "This is a form address". - **What to do**: Send a POST. OPTIONS answers the CORS preflight. --- --- title: "Special fields" description: "Field names FormParcel reads, drops or refuses: _next redirects, the _gotcha trap, the bot check proof, the Turnstile token, refused names and columns." url: "https://formparcel.live/docs/reference/fields" --- # Special fields Field names FormParcel reads, drops or refuses: _next redirects, the _gotcha trap, the bot check proof, the Turnstile token, refused names and columns. Every other field in a post is lead content: it is stored with the lead and becomes a column in the Sheet. These names are different. | Field | What FormParcel does with it | | --- | --- | | `_next`, `_redirect` | Read, not stored. Sends a browser to your thank-you page. [Redirects](https://formparcel.live/docs/reference/fields#redirects) | | `_gotcha` | The spam trap. Filled in, the post is dropped. [The spam trap](https://formparcel.live/docs/reference/fields#trap-field) | | `_fp_proof` | The bot check’s proof. Checked, never stored. [The bot check proof](https://formparcel.live/docs/reference/fields#bot-check-field) | | `cf-turnstile-response` | The Turnstile token. Checked, never stored. [The spam check token](https://formparcel.live/docs/reference/fields#turnstile-field) | | Other control fields | Dropped in any letter case. [Dropped fields](https://formparcel.live/docs/reference/fields#dropped-fields) | | Passwords, card details | The post is refused. [Refused names](https://formparcel.live/docs/reference/fields#refused-names) | | `_submission_id` and three more | System columns of the Sheet. [System columns](https://formparcel.live/docs/reference/fields#system-columns) | ## Redirects with _next Put a hidden field named `_next` in a plain HTML form. Its value is the page the visitor should land on. The older name `_redirect` is also accepted, and `_next` wins when both are present. ```html title="contact.html" ``` When the address is trusted, FormParcel answers a browser form post with a `303` redirect to it, after the lead has been saved. FormParcel reads the field and does not store it or write it to the Sheet. A browser that trips the spam trap, for example through autofill, is sent to the same address, so a person never lands on a dead page. ### The trust rule FormParcel will not redirect to an arbitrary address, because that would let anyone use your form to send visitors to another site. A redirect is followed only when the address is on the client’s own site: - It is on the **site’s domain**, the domain you entered for the client site in FormParcel. The scheme, host and port must match. - Or its host is one of the form’s **allowed origins**. A wildcard such as `*.client.example` matches the domain and its subdomains, but not an address with a port, and a wildcard over a shared host such as `*.github.io` or `*.netlify.app` is never trusted. - Or it is a **path** that starts with `/`, such as `/thanks`. It is resolved against the site’s domain, or against the first plain address in the allowed origins, and must stay there. These are never followed: - An address with a username or password in it. - An address that begins with `//`, or that contains a backslash or a control character. - Anything that is not `http` or `https`, such as `javascript:` or `data:`. - A `_next` that is repeated. It must appear once. ### When the address is not trusted The lead is still saved. The visitor sees FormParcel’s short confirmation page instead of your thank-you page. If that happens on your own site, check that the client site’s domain is set in FormParcel and matches the address in `_next`, including `https` and any `www`. | The _next value | Followed? | Why | | --- | --- | --- | | `https://client.example/thanks` | Yes | Same origin as the site’s domain. | | `/thanks` | Yes | A path, resolved against the site’s domain. | | `https://www.client.example/thanks` | Only if allowed | A different host. For redirects, www is not the same as the bare domain: add it to the form’s allowed origins. | | `https://other.example/thanks` | No | Not the site’s domain or an allowed origin. | | `//other.example/thanks` | No | Starts with two slashes. | | `https://client.example@other.example/` | No | Contains credentials. | ### fetch never redirects Anything that is not a browser navigation gets JSON back and no redirect, whatever it sends in `_next` or `_redirect`: fetch, XHR, curl, a server calling FormParcel, a JSON body, or `Accept: application/json`. A script that sends no `Accept` header gets JSON too. With fetch, you decide what happens next in your own code. See [the fetch guide](https://formparcel.live/docs/guides/fetch) and [Responses and error codes](https://formparcel.live/docs/reference/responses). ## 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. - A script gets an empty `204`. Show no success message. - A browser that trips it, for example through autofill, is shown the same page or sent to the same `_next` address as a real lead, so a person is not left on a page that does nothing. You can change the trap’s name in the form’s settings. A name that matches a common real field, such as `name`, `email`, `phone` or `message`, is refused, because it would drop real leads. When the trap has a different name, a field called `_gotcha` is treated like any other dropped field. How to hide it is on [the plain HTML guide](https://formparcel.live/docs/guides/html#the-trap-field). ## The bot check proof FormParcel’s bot check script adds a hidden `_fp_proof` field to the form. A page that sends with fetch gets the value from `window.FormParcel.proof()` and sends it as a field of the JSON body. FormParcel checks it and never saves it with the lead or writes it to the Sheet. Without a valid proof, or with one sent a second time, the submission waits in the Spam folder. See [Spam protection](https://formparcel.live/docs/setup/spam-protection#the-bot-check). ## The spam check token When a client site has the Cloudflare Turnstile check on, the widget adds a `cf-turnstile-response` field to the form. FormParcel checks it with Cloudflare and never saves it with the lead. A script sends it as a field of the JSON body. See [Spam protection](https://formparcel.live/docs/setup/spam-protection). ## Dropped fields Other form services add control and captcha fields to a post. They are instructions or opaque tokens, not lead content, so FormParcel drops them in any letter case: they are not stored, not in the Sheet and not in email. `_gotcha`, `_captcha`, `_template`, `g-recaptcha-response`, `h-captcha-response`, `cf-turnstile-response`, `_fp_proof` ## Refused names A post is refused with status 422 ([`PROHIBITED_FIELD`](https://formparcel.live/docs/reference/responses#prohibited-field)) when a field is named like a password, a one-time code or a card detail. A post with no readable value in any field is refused too ([`EMPTY_SUBMISSION`](https://formparcel.live/docs/reference/responses#empty-submission)). Neither creates a receipt. The names, and what you must not collect, are on [Limits and retention](https://formparcel.live/docs/reference/limits#prohibited). ## Fields the lead email reads The owner email uses two of your fields. - **Reply-To.** 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 address, so replying goes to the visitor. - **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. ## System columns FormParcel writes four columns of its own into every Sheet it creates: `_submission_id`, `_received_at`, `_source_site` and `_source_form`. A form field with one of those names is written to a column called `custom_` followed by the name, so it never overwrites a system column. What each one holds is on [Google Sheets](https://formparcel.live/docs/setup/google-sheets#what-is-in-the-sheet). --- --- title: "Bot check" description: "The bot check is one script on the page with the form. A post without its proof, such as one from a server, a webhook or curl, is held in your Spam folder." url: "https://formparcel.live/docs/reference/bot-check" --- # Bot check The bot check is one script on the page with the form. A post without its proof, such as one from a server, a webhook or curl, is held in your Spam folder. ## The script Load it once on every page that has the form. The dashboard’s code and every example in these docs already include it. Example form address. Use yours from the dashboard. ```html title="On the page" ``` It is served from the form endpoint host at `/f/formparcel.js`. It sets no cookies and shows nothing. How it works, step by step, is on [Spam protection](https://formparcel.live/docs/setup/spam-protection#the-bot-check). | Part | What it is | | --- | --- | | `GET /f/formparcel.js` | The script. It finds each form whose `action` is a form address on the same host and adds the proof when the form is sent. | | `FormParcel.proof(address)` | For a page that sends with `fetch`. Resolves to one proof for one submission. Send it as the `_fp_proof` field. | | `_fp_proof` | The hidden field that carries the proof. FormParcel checks it and never stores it with the lead. | | `GET /f/{formId}/challenge` | The challenge the script asks for. You never call it yourself. | ## Without the proof A post without a valid proof is not refused. It is saved, held in your Spam folder, and not delivered or counted. It gets the same answer a lead gets, so a bot learns nothing. Your dashboard shows **Bot check not installed** for the form under Needs action. Release a real lead with **Not spam**: it is then delivered and counted. ## Who cannot send the proof Only a visitor’s browser can make the proof. These posts never carry one, so their leads wait in Spam: - Your own server, a serverless function or an API route that forwards the form. - A site builder’s webhook: Framer’s webhook, Wix Automations, Elementor Pro’s Webhook action, the WPForms Webhooks addon, or a plugin that sends Contact Form 7 to a webhook. - A test with `curl`, Postman or a script. Use **Send a test lead** in the dashboard instead: it skips the check. - A page that does not load the script, or whose Content-Security-Policy blocks it. - **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, post from the visitor’s browser. ## Fix it - **Plain form:** keep the script line on every page with the form. See [the HTML guide](https://formparcel.live/docs/guides/html#the-bot-check). - **fetch:** load the script, then send `_fp_proof` from `FormParcel.proof()` with each lead. See [the fetch guide](https://formparcel.live/docs/guides/fetch). - **Content-Security-Policy:** allow `https://f.formparcel.live` in `script-src` and `connect-src`. - **A builder that only posts from its server:** put the form in an HTML embed on the page instead. Each builder’s guide says how: [Choose your platform](https://formparcel.live/docs/setup). --- --- title: "Limits and retention" description: "What a form accepts, how fast it can be called, which pages may post to it, what each plan allows, what you must not collect and how long lead content is kept." url: "https://formparcel.live/docs/reference/limits" --- # Limits and retention What a form accepts, how fast it can be called, which pages may post to it, what each plan allows, what you must not collect and how long lead content is kept. ## What a request can contain | Limit | Value | If it is exceeded | | --- | --- | --- | | Request body | 16 KB | HTTP 413, [`PAYLOAD_TOO_LARGE`](https://formparcel.live/docs/reference/responses#payload-too-large) | | Fields in one post | 50 | HTTP 400, [`TOO_MANY_FIELDS`](https://formparcel.live/docs/reference/responses#too-many-fields) | | Length of a field name | 64 characters | HTTP 400, [`FIELD_NAME_TOO_LONG`](https://formparcel.live/docs/reference/responses#field-name-too-long) | | Length of a field value | 4,096 characters. For a list or an object, the length of its JSON. | HTTP 400, [`FIELD_VALUE_TOO_LONG`](https://formparcel.live/docs/reference/responses#field-value-too-long) | | JSON nesting | 4 levels in total | HTTP 400, [`PAYLOAD_TOO_DEEP`](https://formparcel.live/docs/reference/responses#payload-too-deep) | | Content types | `application/x-www-form-urlencoded`, `multipart/form-data`, `application/json` | HTTP 415, [`UNSUPPORTED_MEDIA_TYPE`](https://formparcel.live/docs/reference/responses#unsupported-media-type) | | Charset | UTF-8. A URL-encoded form in another declared charset is decoded as that charset. | HTTP 415, [`UNSUPPORTED_CHARSET`](https://formparcel.live/docs/reference/responses#unsupported-charset) (JSON and multipart) | | Files | Not accepted | HTTP 400, [`FILE_UPLOAD_NOT_SUPPORTED`](https://formparcel.live/docs/reference/responses#file-upload-not-supported) | | Content | At least one field with a value a person could read | HTTP 400, [`EMPTY_SUBMISSION`](https://formparcel.live/docs/reference/responses#empty-submission) | Numbers must be finite ([`NON_FINITE_NUMBER`](https://formparcel.live/docs/reference/responses#non-finite-number)), and a JSON object may not repeat a top-level key ([`DUPLICATE_FIELD`](https://formparcel.live/docs/reference/responses#duplicate-field)). A refused request never creates a receipt. Every refusal is on [Responses and error codes](https://formparcel.live/docs/reference/responses). ## How fast a form can be called These are the current defaults. They exist to stop abuse, and a normal contact form never gets near them. A refused request is 429 with a `Retry-After` header. The form and workspace caps count accepted leads only, so a malformed or spam request never spends them. | Limit | Default | Code | | --- | --- | --- | | One sender, one form | 10 requests a minute | [`FORM_RATE_LIMITED`](https://formparcel.live/docs/reference/responses#form-rate-limited) | | One sender, all forms | 120 requests a minute | [`CLIENT_RATE_LIMITED`](https://formparcel.live/docs/reference/responses#client-rate-limited) | | One sender, one workspace | 30 requests an hour | [`CLIENT_HOURLY_LIMIT`](https://formparcel.live/docs/reference/responses#client-hourly-limit) | | One sender, one workspace | 10 requests a day | Further submissions are held in the Spam folder, not refused | | One form, all senders | 1,000 accepted leads an hour | [`FORM_HOURLY_LIMIT`](https://formparcel.live/docs/reference/responses#form-hourly-limit) | | One workspace, all forms | 10,000 accepted leads a day | [`WORKSPACE_DAILY_LIMIT`](https://formparcel.live/docs/reference/responses#workspace-daily-limit) | | One sender, bot check challenges | 30 requests a minute | [`CLIENT_RATE_LIMITED`](https://formparcel.live/docs/reference/responses#client-rate-limited) | The per-sender hourly limit across a workspace is lower than both caps, so one sender can never use them up for everyone else. Once a workspace has used its daily cap, only a form that has already accepted 100 or more leads in the current hour is refused. The workspace’s quiet forms keep accepting. A request whose sender address cannot be read shares one bucket, with 10 times the per-sender limits. ## Allowed origins A form can list the sites allowed to post to it. With no list, any page can. With a list, FormParcel checks the request’s `Origin` header, or its `Referer` when there is no usable Origin, against it, and refuses a mismatch with 403 ([origin not allowed](https://formparcel.live/docs/reference/responses#origin-not-allowed)). - A host such as `client.example` matches any scheme and port. `www.client.example` and `client.example` count as one site, whichever you list. - An entry with a scheme, such as `https://client.example:8443`, must match scheme, host and port. - A wildcard such as `*.client.example` matches the domain and all its subdomains. A wildcard over a shared host or a public suffix, such as `*.github.io`, `*.netlify.app` or `*.co.uk`, is refused when you save it and ignored when matching, because it would admit every site on that host. - `Origin: null` comes from sandboxed frames, local files and sites that hide their referrer. With no list it is accepted. With a list, the Referer decides, and a request with neither is refused. FormParcel counts the posts it refuses for their origin, and the leads it accepts from a page with a hidden origin, and shows both on the form’s settings, so you can tell when a real visitor was turned away. The same list decides which redirect addresses are trusted: [Special fields](https://formparcel.live/docs/reference/fields#redirects). ## What is stored about the page A receipt keeps the request’s `Origin` (up to 255 characters) and the `Referer` as origin and path only (up to 512 characters), without its query string or fragment, which can carry personal data. Both are cleared together with the lead content at the retention purge. ## What you must not collect Do not use FormParcel to collect passwords, payment card numbers or highly sensitive regulated information. Forms accept text only. As a safeguard, a post is refused with HTTP 422, [`PROHIBITED_FIELD`](https://formparcel.live/docs/reference/responses#prohibited-field) when a field is named like one of these, whatever its letter case or punctuation, at the top level or as a nested JSON key: `password`, `passwd`, `pwd`, `passcode`, `otp`, `cvv`, `cvc`, `card number`, `cc number`, `credit card`, `debit card`, `card cvv`, `ssn` The match is on the whole name, so ordinary fields such as `pincode` and `passport` are fine. This check stops honest mistakes and automated misuse. It does not promise that a form carries nothing sensitive, and you stay responsible for what your forms collect. See the [terms](https://formparcel.live/terms) and the [security page](https://formparcel.live/security). The other fields FormParcel drops or refuses are on [Special fields](https://formparcel.live/docs/reference/fields). ## How long lead content is kept - Lead content that was delivered is deleted 7 days after the lead was accepted. - Lead content that has not been delivered is kept for up to 30 days, so you can fix the problem and retry. After that its content is deleted and the lead can no longer be sent. - The receipt stays. It holds status, times and attempts, and no lead content. How long the dashboard lists it is the plan’s receipt history, in [plan limits](https://formparcel.live/docs/reference/limits#plan-limits) below. - Your Google Sheet keeps its own copy of every delivered lead for as long as you keep it. You can also delete one lead’s content yourself from its receipt. The receipt and the evidence of every delivery that already happened stay. More on [receipts](https://formparcel.live/docs/reference/receipts#retention) and on the [security page](https://formparcel.live/security). ## Sending email Email recipients are limited per form by plan. A form can have 1 notification recipient on Free, 3 on Starter and 10 on Agency. An address that bounced takes no place. 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. The auto-reply to visitors has its own daily limits, on [Auto-reply](https://formparcel.live/docs/setup/autoresponder). ## Bot check and spam check Every form runs FormParcel’s bot check. A proof is accepted for 30 minutes after its challenge was issued, no sooner than 2 seconds after it, and once. A submission without a valid proof is not refused: it waits in the Spam folder, is not delivered and is not counted. The challenge endpoint answers 30 requests a minute per sender, with the same 429 and `Retry-After` as the other limits. See [the bot check](https://formparcel.live/docs/setup/spam-protection#the-bot-check). When a client site has the Turnstile check on, a visitor who does not pass it is refused with status 403 and the code [`SPAM_CHECK_FAILED`](https://formparcel.live/docs/reference/responses#spam-check-failed). If Cloudflare cannot answer, the lead is still accepted. See [Spam protection](https://formparcel.live/docs/setup/spam-protection). ## Plan limits On top of the limits above, each plan has limits on volume and on how many client sites it covers. They are enforced. Receipts, automatic retries and repair are the same on every plan. | Limit | Free | Starter | Agency | | --- | --- | --- | --- | | Leads per month | 50 | 1,000 | 10,000 | | Client sites | 1 site, 2 forms | 3 sites | 25 sites | | Google Sheets delivery | 1 Sheet | Included | Included | | Receipt history | 30 days | 1 year | 1 year | | Notification recipients per form | 1 | 3 | 10 | - Each plan has a monthly limit on leads: 50 on Free, 1,000 on Starter and 10,000 on Agency, where the volume is shared across all client sites. A month is a UTC calendar month. Spam and dashboard test leads are not counted. - A month's lead limit is the highest your plan allowed at any point that month: an upgrade lifts it at once, and a downgrade applies from the next month. - Free includes one site with two forms, Starter 3 sites and Agency 25 sites. Starter and Agency do not cap the number of forms. Archived sites and forms do not count. - The one site on the Free plan is the one created for you when you sign up, so an agency that adds its clients' sites needs Starter or Agency. - Receipt history is 30 days on the Free plan and 1 year on Starter and Agency. Older delivered receipts are hidden, not deleted, and a receipt that needs action is always shown. - Site, form, Sheet and recipient limits follow your current plan at once, and a lower plan deletes nothing: you keep what you have and cannot add more than the plan allows. ### Over the monthly limit - Going over your monthly limit does not stop delivery. Until a workspace has used three times its limit, every lead is still stored and delivered to your Sheet and by email. - A lead counted past the monthly limit gets no auto-reply. The lead and the owner's emails carry on. - At three times the limit, the form refuses new leads for the rest of the month. The visitor is told plainly that their message was not accepted. A refused lead is not saved and has no receipt. A script that posts to a form in that state gets HTTP 429 with the code PLAN_LIMIT_REACHED and no Retry-After header, because waiting a few seconds changes nothing. See [`PLAN_LIMIT_REACHED`](https://formparcel.live/docs/reference/responses#plan-limit-reached). - Spam is not counted toward your limit. FormParcel emails the workspace owner when usage reaches 80% of the monthly limit, again when the limit is used up, and when a form starts refusing leads. Each email goes out once a month for the limit in force. ```json title="Refusal at three times the limit" { "success": false, "error": "This form cannot take more messages this month. Please contact the business another way.", "code": "PLAN_LIMIT_REACHED" } ``` The plans and prices are on the [pricing page](https://formparcel.live/pricing). --- --- title: "Receipts and delivery states" description: "Every accepted lead gets a receipt. What each state means, how retries and repair work, and how long a receipt and its lead are kept. Accepted is not delivered." url: "https://formparcel.live/docs/reference/receipts" --- # Receipts and delivery states Every accepted lead gets a receipt. What each state means, how retries and repair work, and how long a receipt and its lead are kept. Accepted is not delivered. ## Accepted is not delivered A lead is **accepted** when FormParcel has stored it, and that is when it gets its receipt. Delivery to a Sheet is a separate step that can succeed, wait or fail, and the receipt shows both. The counts always add up: **Accepted = Delivered + In flight + Needs action.** - **In flight**: the lead is stored and delivery is under way or scheduled. No action is needed yet. - **Needs action**: something needs a person. The lead is kept, and the receipt names the fix. - **Delivered**: every destination the lead goes to has confirmed it. Example lead Receipt sub_…Zk9QxT Sheet Sheet: Delivered. FormParcel writes the receipt ID into the `_submission_id` column of the Sheet it creates, column A. That is how you find a lead's row from its receipt. ## States Each row has its own anchor, so a link such as `#reconnect-google` lands on it. The wording is the dashboard’s. The dashboard also shows a time on Retrying and Confirming with Google when the next attempt is due. | State | What it means | What to do | | --- | --- | --- | | **In flight** | | | | Accepted | FormParcel saved the lead and is about to deliver it. | Nothing. | | Delivering | The lead is saved and is being written to the Sheet now. | Nothing. | | Retrying | Google did not accept the last attempt. The lead is saved and is tried again automatically, with longer waits between attempts. The receipt shows when the next attempt is due. | Nothing yet. | | Confirming with Google | Google did not say whether the row was written. Before it writes again, FormParcel checks the Sheet for the lead's receipt ID, so no duplicate row is written. | Nothing. | | **Needs action** | | | | New fields to review | The lead has fields the Sheet has no columns for. It is saved, and nothing was written to the Sheet. | Review the fields: add a column for each new field, or ignore the ones you do not want in the Sheet. | | Sheet columns changed | The Sheet's header row no longer matches the form. The lead is saved, and nothing was written, so it cannot land in the wrong columns. | Fix the header row and re-check the Sheet, or use a new Sheet. | | Waiting to resend | The destination is reachable again. The lead is saved and waiting to be sent. | Nothing: it is sent automatically. Or retry all waiting leads now. | | Sheet needs repair | The Sheet is in Trash, missing, or no longer editable. The lead is saved, and nothing was written. | Replace the Sheet, or restore it and re-check it. Waiting leads are then sent again automatically. | | Reconnect Google | Google access was revoked or expired. The lead is saved, and nothing was written to the Sheet. | Reconnect Google. Waiting leads are then sent again automatically. | | Connect Google | The form's Sheet has no Google connection. The lead is saved, and nothing was written. | Connect Google. Waiting leads are then sent automatically. | | Delivery paused | Delivery is paused by a platform configuration problem, not by Google. The lead is saved. | Nothing you can fix. The lead is sent once the problem is fixed. | | Delivery failed | The lead was saved but could not be delivered. If it was not delivered before its retention deadline, its content was deleted and it can no longer be sent; the receipt remains. | Retry delivery while the lead content is still kept. | | Email needs a recipient | No one can receive the lead's email yet: no recipient has confirmed, or every address bounced. The lead is saved. | Add or confirm a recipient. Waiting leads are then emailed automatically. | | Email paused | FormParcel could not send email, for a reason on its side. The lead is saved. | Nothing you can fix. The lead is emailed once sending is restored. | | Email failed | The email provider reported that the email did not reach its recipient, and it is not retried on its own. The lead is saved. | Check the recipient's address, then retry delivery. | | Email outcome unknown | The email provider never confirmed the email, and it was too long ago to send it again safely. | Check with the recipient. Retry delivery if it did not arrive. | | **Delivered** | | | | Delivered | Google Sheets confirmed the lead was written to the Sheet. | Open it in the Sheet. | | **Not counted** | | | | Held as spam | The submission came without FormParcel's bot check, failed it, or looked automated or repeated (a third submission from one sender in a day, the same message twice, many links or link markup), so it is in the Spam folder. It was not delivered and does not count toward your plan. Held submissions are deleted after 30 days. | If it is a real lead, choose Not spam. It is then counted and delivered. | | Stopped: marked as spam | You marked the lead as spam before it reached this destination, so it was not sent there. What was already delivered elsewhere stays. A lead that was already delivered somewhere still counts toward your plan; one that was not does not. | If it is a real lead, choose Not spam. It is then sent here. | | Email turned off | Email was turned off for this form, so no email was sent for this lead. Its other destinations are not affected. | Nothing. | A post that was refused at intake, such as one blocked as spam, is not an accepted lead. It has no receipt and is never counted in the totals above. The reasons are on [Responses and error codes](https://formparcel.live/docs/reference/responses). ## Test leads A lead sent with **Send a test lead** is a real receipt, marked as a test, and the name in its Sheet row starts with `[TEST]`. It shows in the receipt counts, so they match the rows in your Sheet, but it never counts as your first real lead, and it does not count toward your plan’s monthly limit. ## The receipt ID in the Sheet The receipt ID starts with `sub_`. FormParcel writes it into the `_submission_id` column, column A, of every row it adds. Searching that column for the ID finds the lead’s row, and a row’s ID finds its receipt. If a row looks duplicated, search column A for the ID: a retry that finds the ID already there writes nothing more. When Google reports the row, the receipt also names it, next to the ID in column A. For a delivered lead the receipt shows how long it took from Accepted to Delivered. The figure is measured from that lead’s own times. It is not a typical speed and not a promise. ## Retries and repair When Google does not accept an attempt, the lead stays stored and FormParcel tries again on its own, with longer waits between attempts. The wait before each attempt is: | Attempt | Wait before it | | --- | --- | | 1 | 1 minute | | 2 | 5 minutes | | 3 | 15 minutes | | 4 | 1 hour | | 5 onward | 6 hours | Each wait varies by up to 20% either way, so many retries do not arrive at Google together. FormParcel makes at most 10 attempts for each delivery. When Google says how long to wait, with a `Retry-After` header, FormParcel waits that long instead, and that wait does not use up an attempt. - **Confirming with Google.** When Google does not say whether a row was written, FormParcel looks for the lead’s receipt ID in the Sheet before it writes again. If the ID is there, nothing more is written, so a retry never makes an unexplained duplicate row. - **Needs action.** A problem that waiting cannot fix, such as Reconnect Google, Sheet needs repair or Sheet columns changed, pauses delivery. The lead is kept. When you fix it, the waiting leads are sent again automatically. The fixes are on [Troubleshooting](https://formparcel.live/docs/help/troubleshooting). - **Retry delivery.** After the last attempt, the receipt says Delivery failed. Use **Retry delivery** on it while the lead’s content is still kept. - **What stops a retry.** The attempt limit, and the retention deadline below. Once a lead’s content is deleted it can no longer be sent. The receipt stays. ## Email Each email destination adds a row to the receipt. A lead that goes to a Sheet and to email shows one state for each, and is called delivered only when both are. For email, Delivered means the email provider accepted the message for the confirmed recipients. It does not mean the message reached an inbox. The email states are Email needs a recipient, Email paused, Email failed and Email outcome unknown. In each case the lead is saved. A failed or soft-bounced email is retried automatically. The provider recognises a repeat of the same send for 24 hours, and automatic email retries stop at 23 hours, inside that window. A second copy is therefore rare: it takes a lost response, a day of failures and then a resend. Sheet rows are protected separately and are never duplicated by a retry. See [Email notifications](https://formparcel.live/docs/setup/email). ## Auto-reply When a form has the auto-reply on, the receipt also has an auto-reply line: **Sent**, **Skipped** with the reason, **Failed** or **Queued**. The auto-reply is not a delivery destination. It never changes the state of the receipt, and a skipped or failed reply never affects the lead. See [Auto-reply](https://formparcel.live/docs/setup/autoresponder). ## How long a receipt and its lead are kept Lead content is deleted 7 days after a lead is accepted, or after up to 30 days if it was never delivered. The receipt stays: its status, times and attempts hold no lead content. Receipt history is 30 days on the Free plan and 1 year on Starter and Agency. Older delivered receipts are hidden, not deleted, and a receipt that needs action is always shown. You can also delete one lead’s content yourself from its receipt. The receipt and the evidence of every delivery that already happened stay. The other limits are on [Limits and retention](https://formparcel.live/docs/reference/limits#retention), and the security side is on the [security page](https://formparcel.live/security). --- --- title: "Nothing arrived" description: "Start from the status code you got or the state on the receipt, then find the cause and the fix: refusals, Sheet problems, missing receipts and duplicate rows." url: "https://formparcel.live/docs/help/troubleshooting" --- # Nothing arrived Start from the status code you got or the state on the receipt, then find the cause and the fix: refusals, Sheet problems, missing receipts and duplicate rows. Every accepted lead has a receipt, so the first question is whether this lead has one. Open **Leads** in the dashboard and search for the lead. ## Start from what you see | What you see | Go to | | --- | --- | | The post got an error answer, or a browser showed a page that says it was not sent | [You got an error](https://formparcel.live/docs/help/troubleshooting#an-error-answer) | | The post was answered with success, but there is no receipt | [There is no receipt](https://formparcel.live/docs/help/troubleshooting#no-receipt) | | A receipt says Reconnect Google, Connect Google, Sheet needs repair or Sheet columns changed | [A receipt needs action](https://formparcel.live/docs/help/troubleshooting#a-receipt-needs-action) | | Leads are in the Spam folder, and receipts say Held as spam | [My leads are all in the Spam folder](https://formparcel.live/docs/help/troubleshooting#leads-in-the-spam-folder) | | A receipt says New fields to review | [New fields to review](https://formparcel.live/docs/help/troubleshooting#new-fields-to-review) | | A receipt says Delivery paused or Delivery failed | [Delivery paused](https://formparcel.live/docs/help/troubleshooting#delivery-paused) and [Delivery failed](https://formparcel.live/docs/help/troubleshooting#delivery-failed) | | A receipt says Delivered, but you cannot find the row | [Delivered, but no row](https://formparcel.live/docs/help/troubleshooting#delivered-but-no-row) | | A row looks duplicated | [A row looks duplicated](https://formparcel.live/docs/help/troubleshooting#a-row-looks-duplicated) | ## You got an error A refused post stores nothing and has no receipt, so nothing was lost on FormParcel’s side: the lead was never accepted. Branch on the HTTP status, then on `code`. Every refusal has a row, with what to do about it, on [Responses and error codes](https://formparcel.live/docs/reference/responses). The usual ones: - **403, origin not allowed.** The form has allowed origins and the page the form is on is not one of them, or the request carries neither an Origin nor a Referer header. Add the page’s site to the form’s [allowed origins](https://formparcel.live/docs/reference/limits#allowed-origins), and `localhost` while you test locally. [The refusal](https://formparcel.live/docs/reference/responses#origin-not-allowed). - **403, [`SPAM_CHECK_FAILED`](https://formparcel.live/docs/reference/responses#spam-check-failed).** The site has the spam check on and the visitor did not pass it. The visitor completes the check and sends again with the fresh token. See [Spam protection](https://formparcel.live/docs/setup/spam-protection). - **400, [form not ready](https://formparcel.live/docs/reference/responses#form-not-ready).** The form has no destination yet. Connect a Google Sheet to it. - **HTTP 400, [`EMPTY_SUBMISSION`](https://formparcel.live/docs/reference/responses#empty-submission).** No field holds a readable value. Check that every field has a `name` and the form uses `method="POST"`. - **HTTP 422, [`PROHIBITED_FIELD`](https://formparcel.live/docs/reference/responses#prohibited-field).** A field is named like a password or a card detail. Take it out of the form. - **429.** Sending too fast: wait 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, and the owner upgrades the plan or waits for the next UTC month. - **503.** The lead was not accepted. Send it again after the `Retry-After` wait. ## There is no receipt If the answer was a success and you still find no receipt, check these in order: - **The spam trap was filled.** A script that fills `_gotcha` gets an empty `204`, and a browser that trips it is shown the normal thank-you page. Nothing is saved and no receipt is created. A real visitor never fills that field in. See [Special fields](https://formparcel.live/docs/reference/fields#trap-field). - **The post went to another form.** Compare the address in the page with the address on the form’s page in the dashboard. - **A field has no `name`.** A field without a name is not sent. If none of them has one, the post is empty and was refused. - **The post was refused.** See [You got an error](https://formparcel.live/docs/help/troubleshooting#an-error-answer). ## My leads are all in the Spam folder The leads are stored, so nothing is lost. Open one and read its reason. - **Sent without FormParcel's bot check (formparcel.js).** The page with the form is not running the bot check. Add the script line after the form, on every page that has it, with your own endpoint host. The form’s **Spam & security** page shows the exact line. In this example it is ``. - **Still missing after you added it?** Open the page, then the browser’s developer tools. If the console says a Content-Security-Policy blocked `/f/formparcel.js` or the challenge request, add the form endpoint host to the site’s `script-src` and `connect-src`. A page builder or plugin that strips scripts, or a script blocker in the visitor’s browser, has the same effect. - **Sent from a server, a webhook or a form plugin.** A post from your server, a site builder’s webhook (Framer, Wix Automations, Elementor Pro, WPForms, a Contact Form 7 webhook plugin) or a `curl` test never carries the proof, so every such lead waits here. Put the form on the page instead: [Choose your platform](https://formparcel.live/docs/setup) has the way for each builder, and [Bot check](https://formparcel.live/docs/reference/bot-check#who-cannot-send-the-proof) lists what cannot send it. - **A page that sends with fetch.** Loading the script is not enough there. Ask `window.FormParcel.proof()` for a proof before each send and send it as `_fp_proof`. See [the fetch guide](https://formparcel.live/docs/guides/fetch). - **Sent again and again from one browser.** A third submission from one sender to a form in 24 hours is held, even with a valid proof, so testing from your own browser ends up here. Test with **Send a test lead** in the dashboard, and release a real lead with **Not spam**. - **Failed FormParcel's bot check.** The proof was there but did not pass, for example a page left open while the computer slept, so its challenge expired, a proof made for another form, or a proof sent a second time (a script must ask `window.FormParcel.proof()` for a new one before every send). One or two now and then is normal. If every lead has it, check that the form address in the page is the one in the dashboard. The **Spam & security** page says **Bot check working** once new submissions come with a valid proof. Then release the real leads already held with **Not spam**: they are counted and delivered. See [Spam protection](https://formparcel.live/docs/setup/spam-protection#the-bot-check). ## A receipt needs action The lead is stored. Nothing is lost, and nothing is written to the Sheet until the problem is fixed. After the fix, the waiting leads are sent again automatically. ### Reconnect Google Google access was revoked or expired. The lead is saved, and nothing was written to the Sheet. Google access ends when it is revoked in the Google account, or when it expires. Reconnect Google in the dashboard and the waiting leads are sent. [Connecting Google](https://formparcel.live/docs/setup/google-sheets#connect-google) explains the permission. ### Connect Google The form's Sheet has no Google connection. The lead is saved, and nothing was written. Open the form’s Sheet step, choose **Connect Google**, and the waiting leads are sent. ### Sheet needs repair The Sheet is in Trash, missing, or no longer editable. The lead is saved, and nothing was written. Restore the Sheet from Trash in Google Drive and choose **Re-check Sheet**, or choose **Replace Sheet** to make a new one. Waiting leads are then sent. ### Sheet columns changed The Sheet's header row no longer matches the form. The lead is saved, and nothing was written, so it cannot land in the wrong columns. Put the header row back as FormParcel wrote it, and choose **Re-check Sheet**. You can add your own columns to the right of FormParcel’s, but do not rename, move or delete a header cell. Or choose **Replace Sheet** to start a new one. See [Editing the Sheet](https://formparcel.live/docs/setup/google-sheets#editing-the-sheet). ### New fields to review The lead has fields the Sheet has no columns for. It is saved, and nothing was written to the Sheet. Nothing is dropped. Open the receipt, then add a column for each new field or ignore the ones you do not want in the Sheet, and the lead is delivered. A field a spam bot made up is a good one to ignore. ### Delivery paused Delivery is paused by a platform configuration problem, not by Google. The lead is saved. Nothing you can fix. The lead is sent once the problem is fixed. If it lasts, [contact us](https://formparcel.live/contact) with the receipt ID. ### Delivery failed The lead was saved but could not be delivered. If it was not delivered before its retention deadline, its content was deleted and it can no longer be sent; the receipt remains. Choose **Retry delivery** on the receipt while the lead’s content is kept. How long that is, is on [Limits and retention](https://formparcel.live/docs/reference/limits#retention). ## Retrying and Confirming with Google Retrying, Confirming with Google and Waiting to resend need nothing from you. The lead is stored and FormParcel keeps trying. How retries work is on [Receipts and delivery states](https://formparcel.live/docs/reference/receipts#retries). ## Delivered, but no row Delivered means Google confirmed the row was written. Check the Sheet the form is connected to, and not another copy of it, and clear any filter or sort in the Sheet. Then search column A, `_submission_id`, for the receipt ID. If the ID is there, the row is there. ## A row looks duplicated Search column A for the receipt ID of the lead. Every row FormParcel writes carries its receipt ID, and a retry that finds the ID already in the Sheet writes nothing more. Two rows with different IDs are two leads: the visitor, or a script, posted twice, and each post made its own receipt. FormParcel has no caller-supplied idempotency key yet, so a script that resends a lead after a timeout can make a second one. See [the fetch guide](https://formparcel.live/docs/guides/fetch#when-you-get-no-answer). ## A lead email did not arrive Check these in the order that usually finds it. - **Has the recipient confirmed?** 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. The confirmation email’s subject does not repeat the form or site name, so ask the recipient to look for a message from FormParcel’s notify address. - **Is it in the spam folder?** Lead emails come from FormParcel’s own notify address, not from your client’s domain, so a mail system can file the first one as spam. Marking it as not spam, or adding the sender to contacts, usually fixes that. FormParcel cannot tell you where a mail system put a message. - **What does the receipt say?** Delivered for email means the email provider accepted the message. It does not mean it reached an inbox. Email needs a recipient means no recipient has confirmed yet, or every address bounced. Email paused means FormParcel could not send for a reason on its side. Email failed and Email outcome unknown are on [Receipts and delivery states](https://formparcel.live/docs/reference/receipts#states). - **Was the address paused?** After a permanent bounce or a spam report, FormParcel stops emailing that address and tells the form’s owner. Check the address for a typo, or add a different one. See [Email notifications](https://formparcel.live/docs/setup/email#paused-addresses). - **Is replying to the visitor not working?** The reply address comes from the first field called `email, email_address, your_email, mail, contact_email, _replyto, replyto, reply_to, reply-to` that holds exactly one plain address. Without one, no reply address is set. - **Is it waiting for a sending limit?** 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. - **Two emails for one lead?** A lead email is sent once. The provider recognises a repeat of the same send for 24 hours, and automatic retries stop at 23 hours, inside that window. A second copy is rare. ## Still stuck [Contact us](https://formparcel.live/contact) and quote the receipt ID, which starts with `sub_`, or the time of the request if it was refused. Do not send the lead’s content.