Building your form
BootForm forms are schemaless: whatever name attributes you put on your inputs become the submission's fields, with no schema to declare up front. This page covers everything you can do with those fields, and the platform behavior around them.
<form action="https://f.bootform.com/{form_id}" method="POST">
<!-- Required: BootForm's spam trap. Keep this hidden field and leave it empty. -->
<input type="text" name="honeypot" style="position:absolute;left:-9999px" tabindex="-1" autocomplete="off" />
<input name="email" type="email" required />
<textarea name="message"></textarea>
<button type="submit">Send</button>
</form>That's the whole minimum - see Getting started if you haven't posted your first submission yet. Everything below is what you can add on top of it.
The hidden spam-trap field is called honeypot on new forms. If you renamed it, use the name in your form's snippet in the dashboard (or from bootform_get_snippet over MCP). See Spam protection.
How field names are shown
Where a person reads a submission (the dashboard, notification emails, Discord, Slack and Telegram), BootForm turns each field name into a readable label: first_name and firstName show as "First name", check-in as "Check in", interests[] as "Interests". A name that already contains a space, like name="Your Name", is shown exactly as written. The dashboard shows the original name when you hover over a label.
Everything a program reads keeps the original name: the API, MCP tools, webhooks, Zapier, Make, CSV exports, and the placeholders in your email templates.
Special fields
A handful of field names are reserved - BootForm strips them before storing the submission and uses them to control behavior instead:
| Field | Effect |
|---|---|
_redirect (or _next) | Redirect the browser here after a successful submission |
_replyto | Sets the notification email's Reply-To header |
_subject | Overrides the notification email's subject line |
_format=json | Forces a JSON response even for a classic (non-AJAX) browser POST |
_idempotency_key | Makes a retry safe: a repeat with the same key is not stored twice. See Retrying safely |
_language | Language for BootForm-generated content (confirmation pages, etc.) |
Your form's honeypot field (honeypot unless you renamed it) | Spam trap: required while the form's honeypot is on, and must stay empty. You can change the name under Settings → Spam protection → Honeypot; the form's snippet always shows the current one. Don't give a real field the same name. See Spam protection |
A few more are dropped and never stored:
- A named submit button, such as
<button name="submit">or<input type="submit" name="Submit">. The browser sends it as a field holding nothing or just the button's label, so BootForm ignores it. Your submit button doesn't need anameat all. g-recaptcha-responseandh-captcha-response, the tokens reCAPTCHA and hCaptcha add to a form. BootForm doesn't check them (its CAPTCHA support is Cloudflare Turnstile), so they are discarded rather than shown as a field.
Field values
BootForm doesn't control what value a field submits - the browser serializes your HTML before BootForm ever sees it, so standard HTML form semantics apply:
<select>submits the chosen<option>'svalueattribute. If an<option>has novalueattribute, its own text content is submitted instead (the HTML spec's fallback) - so<option value="us">United States</option>submitsus, while a bare<option>United States</option>submitsUnited Statesitself.- Radio buttons (
<input type="radio">) submit whichever option'svalueattribute is checked. Unchecked radio buttons/checkboxes aren't submitted at all - the field is absent from the submission entirely, not present with an empty value. - Checkboxes with the same
name(e.g. a "pick all that apply" group) submit one value per checked box, all sharing that name - the browser sends them as repeated key=value pairs, and BootForm joins every value for a repeated field name into one comma-separated string (tags=red&tags=bluebecomes the fieldtagswith valuered,blue). BootForm forms have no array field type - this comma-joining is the only way a single field ends up holding more than one value.
File uploads
Send multipart/form-data and any file input's contents are stored as an attachment, downloadable later from the dashboard (fresh, short-lived links - see Managing submissions). File uploads need the Starter plan or higher (Free returns 403 PLAN_LIMIT). Per file: 10 MB on Starter, 25 MB on Pro, 50 MB on Business. Each account also has a total for all its stored files: 1 GB on Starter, 10 GB on Pro, 50 GB on Business. Any number of files per submission. Each must fit the plan's per-file size (422 FILE_TOO_LARGE), and the whole request must stay under 100 MB (413 REQUEST_TOO_LARGE). Files are stored exactly as uploaded, metadata included: a photo straight from a phone usually carries the GPS location it was taken at. If that shouldn't be stored or shown, strip it in the browser before sending (the feed docs have a short snippet).
When attachment storage is full
If a submission's files would not fit in what is left of your total, the submission is still saved, without its files. Its text is never affected. The submission says how many files were not stored, you get one email at 80% and one when a submission first loses its files, and the dashboard shows a banner while storage is full. The Attachment storage meter on the Billing page shows how much is used.
To make room, delete old submissions (their files go with them), upgrade, or turn on Automatically delete the oldest attachments when storage is full on your Account page. With that on, the files of your oldest submissions are deleted to make room for new ones; those submissions keep their text and say their files were removed. On a team account only an Admin can change this setting.
Each file is kept with the name of the input it came from (resume below). On Pro and above, add that name to a moderated content feed's public fields to show approved uploads, such as photos, on your own site.
A plain HTML form handles this with no JavaScript at all - just add enctype="multipart/form-data" and a file input:
<form action="https://f.bootform.com/{form_id}" method="POST" enctype="multipart/form-data">
<!-- Required: BootForm's spam trap. Keep this hidden field and leave it empty. -->
<input type="text" name="honeypot" style="position:absolute;left:-9999px" tabindex="-1" autocomplete="off" />
<input name="email" type="email" required />
<input name="resume" type="file" required />
<button type="submit">Send</button>
</form>Doing it via fetch instead - useful once you also want the JSON response below - reads that same form's fields straight out of FormData, so the HTML is identical; only the submit handling changes:
<form id="upload-form" action="https://f.bootform.com/{form_id}" method="POST" enctype="multipart/form-data">
<!-- Required: BootForm's spam trap. Keep this hidden field and leave it empty. -->
<input type="text" name="honeypot" style="position:absolute;left:-9999px" tabindex="-1" autocomplete="off" />
<input name="email" type="email" required />
<input name="resume" type="file" required />
<button type="submit">Send</button>
</form>const form = document.getElementById('upload-form')
form.addEventListener('submit', async (event) => {
event.preventDefault()
const response = await fetch(form.action, {
method: 'POST',
headers: { Accept: 'application/json' },
body: new FormData(form), // picks up the file input's contents automatically
})
const result = await response.json()
})See this same pattern in React, Vue, Angular, or Vanilla JS.
One gotcha: a file upload posted to a form with no owning account yet (i.e. you haven't claimed it) is rejected outright (422 FORM_NOT_CLAIMED)
- there's no plan/quota to validate a file against for a form nobody owns yet. Plain scalar-field submissions to an unclaimed form are unaffected; only file uploads need the form claimed first.
Spam protection
Honeypot, Cloudflare Turnstile CAPTCHA, an IP blocklist, and country restriction - all configured per form from the dashboard's Settings tab, and all covered in depth, with click-by-click Turnstile setup and exact response codes, on their own page: Spam protection.
"Powered by BootForm" credit
The snippets BootForm suggests (the dashboard's Example form, the API's html_snippet and the MCP bootform_get_snippet tool) end with a small credit under the form: "Powered by" and the BootForm wordmark, linking to bootform.com. It's one line of HTML with the wordmark drawn as an inline SVG in your page's own text colour, so it fits light and dark designs alike and needs no image to load.
It's optional and nothing depends on it. Delete it, restyle it, or ask an agent for a snippet without it (include_powered_by: false). If you keep it, thank you: it's how other developers find us.
<p style="font-size:12px"><a href="https://bootform.com/?ref=powered-by" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:6px;color:inherit;text-decoration:none">Powered by<svg viewBox="0 0 210 64" width="79" height="24" role="img" aria-label="BootForm" style="display:inline-block;font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,sans-serif"><defs><filter id="bootform-glow" x="-60%" y="-60%" width="220%" height="220%"><feDropShadow dx="0" dy="0" stdDeviation="1.6" flood-color="#00FFA3" flood-opacity="0.85"/></filter></defs><text x="9" y="45" font-size="36" font-weight="800" letter-spacing="-0.5" fill="currentColor">boot</text><text x="87" y="45" font-size="36" font-weight="400" letter-spacing="-0.5" fill="currentColor" opacity="0.6">form</text><text x="160" y="45" font-size="36" font-weight="800" fill="#00FFA3" filter="url(#bootform-glow)">_</text></svg></a></p>Validation rules
Beyond spam filtering, you can require specific fields to be present and well-formed - configured per form from the dashboard's Settings tab. Each rule targets one field and can require it to be present, match a type (email, url, tel, number, or datetime), fall within a min/max (numbers and dates) or min/max length (text), match an accepted MIME type list (file fields), or
- for
emailfields specifically - require a work email address rather than a free consumer provider.
A submission that fails validation is rejected with 422 and a structured errors array (one entry per failed field), or, for a classic non-AJAX browser POST, a redirect back to the form's page with ?bootform_error=...&bootform_field=... appended instead - there's no script running on that request to read a JSON body.
Responses and redirects
By default, a successful submission redirects the browser back to the page it came from. Two ways to change that:
- Set
_redirect(or_next) to send the browser somewhere specific after success. - Send
Accept: application/json(or include_format=json) to get a200 {"ok":true}JSON response instead of a redirect - the right choice for an AJAX/fetch()submission that updates the page in place rather than navigating away.
const form = document.querySelector('form')
form.addEventListener('submit', async (event) => {
event.preventDefault()
const response = await fetch(form.action, {
method: 'POST',
headers: { Accept: 'application/json' },
body: new FormData(form),
})
const result = await response.json()
if (result.ok) {
form.reset()
// show your own success message here - the page never navigated away
} else {
// result.error / result.code (and result.errors for per-field validation failures)
console.error(result.error)
}
})If the form hasn't been claimed yet, a JSON response includes "status": "pending_claim" and a claim_url you can surface to whoever's testing the integration.
Retrying safely
An app or script that retries a submission (an offline queue, a request that timed out, an app closed before it saw the answer) can send an idempotency key: any id you generate once per submission, such as a UUID. The first submission with that key is processed normally. A repeat with the same key on the same form within 7 days gets the same success answer, with "duplicate": true in JSON, and nothing else happens: no second submission, no second notification, nothing counted against your quota.
await fetch('https://f.bootform.com/YOUR_FORM_ID', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Idempotency-Key': reportId, // generated once, reused on every retry
},
body: JSON.stringify({ message, email }),
})Send it as the Idempotency-Key header, or as a hidden _idempotency_key field from a plain HTML form. If both are sent, the header wins. A key is 1 to 128 letters, digits, ., _, : or -; anything else is rejected with 422 INVALID_IDEMPOTENCY_KEY. If a submission is rejected (a validation error, a failed CAPTCHA, the monthly quota), the key is not used up, so retrying it later works as a fresh submission.
Autoresponder emails
On Pro plan and higher, BootForm can send a confirmation email back to whoever submitted the form. You configure it on the dashboard's Workflow tab with a subject and a Markdown body, both supporting {field_name} interpolation from the submission's own fields. This needs the submission to carry an email address (an email field, or _replyto) to have anywhere to send to; a submission with neither simply doesn't trigger a response, since BootForm forms have no fixed schema to check against ahead of time.
If you leave them empty, the subject is "Thanks for your message" and the body is "Thanks for reaching out. We'll get back to you as soon as we can."
The body supports Markdown formatting (bold, links, lists, quotes), but not raw HTML: any HTML tags in it are shown as text. Submitted values are inserted as plain text, so a submitter can't add links or formatting to the email by typing Markdown or HTML into your form. Readers whose mail app shows only plain text get the same message without the Markdown symbols.
If the submitter replies to the confirmation, the reply goes to your account email. If you turn on a copy to yourself, replying to that copy reaches the submitter, and the copy says which address the confirmation was sent to.