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");
}