HTTP ingest
One POST with a JSON object. No SDK, no dependency — you can use it from a one-line script.
Routes
| Route | What it does |
|---|---|
POST /api/v1/submit | The key goes in a header. This is the usual path. |
POST /api/v1/f/:apiKey | The key goes in the URL. Useful when you don't control the headers. |
POST /api/v1/f/:apiKey/page | The survey page's own door. The same contract; the Submission records page. See Sources. |
GET /api/v1/form | Returns 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 goes | Notes |
|---|---|
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 -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}'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 -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 answers200with 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'sTURNSTILE_SITE_KEYandTURNSTILE_SECRET_KEY. - Every refusal is a
400and 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
| Limit | Notes |
|---|---|
| 20 req/min per IP | The default. Change it with INGEST_RATE_LIMIT_IP. |
| 60 req/min per API key | The default. Change it with INGEST_RATE_LIMIT_KEY. |
| 100 KB | Maximum body size. |
| Open CORS | You 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
| Code | Meaning |
|---|---|
201 | { "ok": true, "id": "…" } |
200 | The same body: a repeated Idempotency-Key, answered with the first Submission's id. |
400 | Invalid 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. |
401 | API key missing. |
404 | Unknown key, or the Form is paused. |
413 | Body over 100 KB. |
429 | Limit exceeded. The Retry-After header carries the seconds. |
What did you think of this content?
Concepts
Noonmark receives Submissions through a POST and shows everything in the Studio. You host nothing here — the screen is yours, the URL is ours.
Sources
Every door a Submission can come through, and what each one records. Whichever door it used, a Submission is the same object: checked against the Fields, with provenance and a timeline.