docs

HTTP ingest

One POST with a JSON object. No SDK, no dependency — you can use it from a one-line script.

Routes

RouteWhat it does
POST /api/v1/submitThe key goes in a header. This is the usual path.
POST /api/v1/f/:apiKeyThe key goes in the URL. Useful when you don't control the headers.
POST /api/v1/f/:apiKey/pageThe survey page's own door. The same contract; the Submission records page. See Sources.
GET /api/v1/formReturns the Form definition. See noonmark.form().

Authentication

The API key identifies the Form. It is public by nature — it travels in your users' browsers — so treat it as an identifier, not a credential. Any of these works:

Where the key goesNotes
Authorization: Bearer frm_…The most common.
X-Noonmark-Key: frm_…When Authorization is already used for something else.
X-Api-Key: frm_…An alias of the previous one.
/api/v1/f/frm_…In the URL, with no header at all.
curl
curl -X POST https://noonmark.dev/api/v1/submit \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer frm_..." \
  -d '{"email":"ana@acme.com","message":"…","rating":4}'
fetch
await fetch("https://noonmark.dev/api/v1/submit", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Noonmark-Key": "frm_...",
  },
  body: JSON.stringify({ email: "ana@acme.com", rating: 4 }),
});

The body

It must be a JSON object — not an array, not a bare value. Keys beyond the Fields are kept and appear under Others in the Studio. A Form with no Fields accepts any object.

Idempotency-Key

POST /api/v1/submit and POST /api/v1/f/:apiKey take an optional Idempotency-Key header, so a client can retry after a timeout without storing the Submission twice.

curl
curl -X POST https://noonmark.dev/api/v1/submit \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer frm_..." \
  -H "Idempotency-Key: order-4817" \
  -d '{"message":"…"}'
  • The first POST with a key answers 201. Any later POST with the same key on the same Form stores nothing and answers 200 with the same body — { "ok": true, "id": "…" }, the first Submission's id — whatever the later body says.
  • A key is 1 to 200 printable ASCII characters, trimmed. A blank, longer or non-ASCII key is refused with 400, not ignored.
  • A key belongs to one Form and lasts as long as the Submission it made. Use one per thing your user did, such as an order id or a UUID made when the screen opened.
  • The survey page's door does not take one; the page has its own protection.

Bot protection

A Form can switch on Bot protection under its Integrations tab. With it off, every door behaves as described above.

  • The survey page checks two things before anything is stored. A honeypot: a field hidden from people that a script fills in. And a minimum time: the page is signed by the server when it renders, and a Submission sent sooner than the Form's minimum (3 seconds by default), or from a page more than a day old, is refused.
  • Cloudflare Turnstile, when switched on, is checked on every HTTP door: the page renders the widget, and an API client sends the token as X-Noonmark-Turnstile. Noonmark verifies it with Cloudflare before storing. The Form's own Turnstile keys are used, or the deployment's TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY.
  • Every refusal is a 400 and stores nothing. A token works once, so a retry needs a fresh one.

MCP, CSV import, Slack and email are not HTTP doors and are not affected: each has its own way of deciding who may knock.

Limits

LimitNotes
20 req/min per IPThe default. Change it with INGEST_RATE_LIMIT_IP.
60 req/min per API keyThe default. Change it with INGEST_RATE_LIMIT_KEY.
100 KBMaximum body size.
Open CORSYou can POST straight from the browser.

Reading the definition has its own counter, so querying GET /api/v1/form never spends the submit limit.

Status codes

CodeMeaning
201{ "ok": true, "id": "…" }
200The same body: a repeated Idempotency-Key, answered with the first Submission's id.
400Invalid JSON, a body that is not an object, or a Field that doesn't match — the error names the key. Also an invalid Idempotency-Key, or a failed bot check.
401API key missing.
404Unknown key, or the Form is paused.
413Body over 100 KB.
429Limit exceeded. The Retry-After header carries the seconds.

What did you think of this content?

On this page