- Docs
- Reference
Submission API
One public endpoint receives every submission. It works from a plain HTML form with no JavaScript, and from fetch when you want JSON back.
Endpoint
POST https://api.formrill.com/YOUR_FORM_ID/submitYOUR_FORM_ID is the unguessable ID from your dashboard (it starts with f_). The URL is safe to publish: it can only create submissions, never read them. Reading needs your dashboard login.
The smallest working form:
<form method="POST"
action="https://api.formrill.com/YOUR_FORM_ID/submit">
<input type="checkbox" name="botcheck" hidden />
<input type="text" name="name" required />
<input type="email" name="email" required />
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>Content types
| Content-Type | Sent by | Notes |
|---|---|---|
application/x-www-form-urlencoded | A plain HTML form | The default. Nothing to configure. |
multipart/form-data | fetch with FormData, or enctype on the form | Text fields only for now. A request that includes a file is refused with 415 and nothing is saved. |
application/json | fetch with JSON.stringify | The body must be a JSON object. Strings are stored as-is; numbers, booleans, arrays and objects are stored as their JSON text. |
Every other field name is stored exactly as sent and shows up in your email, dashboard and automations. Name your email field email and the notification’s Reply button goes straight to the visitor.
Reserved fields
botcheck- Honeypot. Hide it from people; bots tend to fill it. Any non-empty value other than
falsemarks the submission as spam: it goes to your Spam folder and is not emailed or delivered. The field itself is never saved. redirect- Where to send the visitor after a normal (non-JSON) submit. Must be an absolute
http://orhttps://URL; a relative path is ignored. Never saved.
Where the visitor lands after a normal form post, in order:
- The
redirectfield, if it is an absolute URL. - The form’s Redirect after submit setting in the dashboard.
- A plain “Thanks!” page served by FormRill.
Redirects use 303 See Other, so a page refresh never resubmits.
Responses
Send the header Accept: application/json to get JSON instead of a redirect or HTML page. Redirects are skipped in JSON mode.
{ "ok": true, "id": "s_4fJ2…" }{ "ok": false, "error": "This form does not accept submissions from this domain" }Without that header, errors come back as a plain-text message with the same status code.
Status codes
| Status | Meaning | What to do |
|---|---|---|
200 | Stored. JSON mode, or no redirect configured. | Show your success message. |
303 | Stored, redirecting to your thank-you page. | Nothing; the browser follows it. |
400 | The JSON body is invalid or not an object. | Send a JSON object, e.g. {"email":"…"}. |
403 | The domain whitelist doesn’t include the sending site. | Add the domain in the form’s settings. |
404 | No form with this ID. | Check the ID; a deleted form can’t be restored. |
413 | Body larger than 100 KB. | Shorten the fields. |
415 | The request includes a file. File uploads aren’t supported yet; nothing was saved. | Remove the file input, or ask for a link instead. |
422 | The honeypot was filled. | Make sure botcheck is hidden and left empty (unticked). |
Submitting with fetch
Keep a normal <form action method="POST"> so it still works if JavaScript fails, then take over the submit:
const form = document.querySelector('#contact')
const status = document.querySelector('#status')
form.addEventListener('submit', async (event) => {
event.preventDefault()
status.textContent = 'Sending…'
try {
const res = await fetch(form.action, {
method: 'POST',
body: new FormData(form),
headers: { Accept: 'application/json' },
})
const result = await res.json()
if (!result.ok) throw new Error(result.error)
form.reset()
status.textContent = 'Thanks! We got your message.'
} catch (err) {
status.textContent = 'Could not send: ' + err.message
}
})Or post JSON from anywhere that can make an HTTP request (this example uses top-level await, so run it as an ES module):
const res = await fetch('https://api.formrill.com/YOUR_FORM_ID/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
name: 'Ada Lovelace',
email: 'ada@example.com',
message: 'Hello!',
interests: ['pricing', 'demo'], // stored as the string '["pricing","demo"]'
}),
})
// res.status === 200 → { "ok": true, "id": "s_…" }From a terminal:
curl -X POST https://api.formrill.com/YOUR_FORM_ID/submit \
-H "Accept: application/json" \
-d name=Ada -d email=ada@example.com -d message=HelloDomain whitelist Pro, free in beta
In the form’s settings, Allowed domains takes a comma-separated list such as example.com, shop.example.com. Empty means any site may submit.
- FormRill checks the request’s
Originheader, orRefererif there is noOrigin. - A listed domain also allows its subdomains:
example.comacceptswww.example.com. - When a whitelist is set, requests with neither header are rejected with
403. That includes mostcurland server-side calls, so leave the list empty if you submit from a server.
Limits and CORS
- Size: 100 KB per submission.
- CORS: the submit endpoint answers any origin, so
fetchworks from any site, localhost included. Use the domain whitelist to restrict who can post. - Monthly volume: Free 500, Pro 10,000 submissions a month. Going over never drops a submission; see pricing.
Webhook payload Pro, free in beta
The Webhook, n8n, CRM and Google Sheets automations all send the same JSON with a POST and Content-Type: application/json:
{
"form_id": "f_…",
"form_name": "Contact form",
"submission_id": "s_…",
"created_at": 1793750400000,
"data": {
"name": "Ada Lovelace",
"email": "ada@example.com",
"message": "Hello!"
}
}created_at is Unix time in milliseconds. data holds the submitted fields as strings, without botcheck and redirect. Requests are not signed yet, so use a long, secret URL for your endpoint and treat it like a password. Destination URLs must use https://.
Delivery and retry
- The submission is saved first. Once you get
200or303, it is stored, whatever happens next. - FormRill then sends it to every enabled automation on the form: email, Telegram, webhooks and so on.
- Each attempt is written to the form’s Delivery log. Any non-2xx response counts as a failure.
- If anything fails, you get one email listing what failed and why.
- Failures are retried automatically about 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours after it fails: six automatic attempts over about a day. If all of them fail you get one “gave up” email. Press Retry now in the delivery log to send the stored submission again at any time. Spam is never retried.
Delivery log, failure alerts and retry are free on every plan.
Not supported yet
- File uploads Coming soon
- Signed webhook requests Coming soon
- A read API for your own code, and the MCP server Coming soon
- AI spam scoring; today the honeypot and domain whitelist are the spam controls Coming soon
Get a form URL to test the API with
Free up to 500 submissions a month and unlimited forms. No server, no credit card.
Start freeRelated
- formrill.js reference
Funnel analytics and validation for your form.
- Next.js quick start
A client component with loading and error states.
- Integrations
Email, Telegram, Sheets, webhooks and more.