title: Submission API description: Submit form data programmatically over HTTP.
Submission API
Collect submissions by POSTing JSON to the public submission endpoint. The
server is the source of truth: every value is validated against the form's own
field definitions, regardless of what the client sends.
Endpoint
POST /api/submit/{formId}
Content-Type: application/json
Request body
{
"data": {
"name": "Grace Hopper",
"email": "grace@example.com",
"message": "I'd like a demo"
}
}
data is a flat object keyed by field id (or field label for the simple
case). Only the fields you defined on the form are accepted; unknown keys are
ignored and required fields are enforced server-side.
Field validation
Each value is checked against the field's type and options:
- Required fields must be present and non-empty.
- Email fields must be a valid address.
- Select / radio fields must match a defined option.
- Numbers must parse as numbers.
Limits are enforced to prevent payload abuse: a maximum body size, a maximum number of fields, and a maximum length per field.
Responses
| Status | Meaning |
| --- | --- |
| 200 | Submission accepted. |
| 400 | Validation failed — see details. |
| 403 | Origin not authorized, or bot challenge failed. |
| 404 | Form does not exist or is not published. |
| 429 | Rate limited. |
Validation errors return a structured body:
{
"error": "Invalid request",
"details": { "email": ["Invalid email address"] }
}
Bot protection (Turnstile)
Public forms are protected by Cloudflare Turnstile. Browser submissions
include a turnstileToken; the server verifies it before processing. Server-
to-server calls using a scoped API key bypass the challenge — see
Authentication.
Idempotency
To prevent duplicate submissions on retry or double-click, send a stable
idempotencyKey. Two requests with the same (formId, idempotencyKey) insert
once; the second returns the original result.
{
"data": { "email": "grace@example.com" },
"idempotencyKey": "your-stable-request-id"
}
Example (JavaScript)
const res = await fetch(`/api/submit/${formId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
data: { name: "Grace Hopper", email: "grace@example.com" },
idempotencyKey: crypto.randomUUID(),
}),
});
if (!res.ok) {
throw new Error((await res.json()).error ?? "Submission failed");
}