FormRill
  1. Docs
  2. 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/submit

YOUR_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

Accepted content types
Content-TypeSent byNotes
application/x-www-form-urlencodedA plain HTML formThe default. Nothing to configure.
multipart/form-datafetch with FormData, or enctype on the formText fields only for now. A request that includes a file is refused with 415 and nothing is saved.
application/jsonfetch with JSON.stringifyThe 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 false marks 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:// or https:// URL; a relative path is ignored. Never saved.

Where the visitor lands after a normal form post, in order:

  1. The redirect field, if it is an absolute URL.
  2. The form’s Redirect after submit setting in the dashboard.
  3. 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

Response status codes
StatusMeaningWhat to do
200Stored. JSON mode, or no redirect configured.Show your success message.
303Stored, redirecting to your thank-you page.Nothing; the browser follows it.
400The JSON body is invalid or not an object.Send a JSON object, e.g. {"email":"…"}.
403The domain whitelist doesn’t include the sending site.Add the domain in the form’s settings.
404No form with this ID.Check the ID; a deleted form can’t be restored.
413Body larger than 100 KB.Shorten the fields.
415The request includes a file. File uploads aren’t supported yet; nothing was saved.Remove the file input, or ask for a link instead.
422The 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=Hello

Domain 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 Origin header, or Referer if there is no Origin.
  • A listed domain also allows its subdomains: example.com accepts www.example.com.
  • When a whitelist is set, requests with neither header are rejected with 403. That includes most curl and 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 fetch works 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

  1. The submission is saved first. Once you get 200 or 303, it is stored, whatever happens next.
  2. FormRill then sends it to every enabled automation on the form: email, Telegram, webhooks and so on.
  3. Each attempt is written to the form’s Delivery log. Any non-2xx response counts as a failure.
  4. If anything fails, you get one email listing what failed and why.
  5. 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 free

Related

Last updated