Forms

Forms collect submissions from your public website — contact forms, applications, quote requests — with validation on the server and an automation hook on every submission.

Defining a form

Create one under Forms with a name and fields. Each field has a name, a label, a type, and optionally required and (for choice types) options:

TypeBehaviour on submit
text (default)Stored as text
emailMust be a valid address; lowercased
numberMust be numeric
dateMust parse as a date
select, radioMust be one of the declared options
checkboxBooleans — or, with options, a list of allowed values

A form can be switched inactive at any time; an inactive form refuses submissions without being deleted.

The public submission endpoint

This is the one deliberately unauthenticated write in a project — it's what your site posts to:

POST /api/projects/{project}/forms/{form}/submissions
Content-Type: application/json

{ "email": "visitor@example.com", "message": "Hi there" }

What protects it:

  • Only declared fields are stored — arbitrary keys are dropped, nested objects rejected, so nothing unexpected enters your database.
  • Every value is validated against its declared type, with a 422 and per-field messages on failure.
  • Rate-limited per IP per form.
  • Still workspace-scoped: the form must belong to a project of the workspace the request's domain resolves to.

Each stored submission records the data plus the submitter's IP and user-agent, and increments the form's submission counter.

await fetch(`${API}/forms/${FORM_ID}/submissions`, {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify(Object.fromEntries(new FormData(formEl))),
});

Reading submissions

The Submissions view (and GET on the same route, authenticated with a session or an API key holding forms.read) lists submissions newest-first with pagination.

Doing something with them

The point of a form is rarely storage — attach an event with the form submission trigger. The submitted fields become the flow's trigger payload, so {{email}} and {{message}} are available to templates: auto-reply to the visitor, notify your team, file a record, call a webhook, or all four. A failing automation never rejects the visitor's submission — it lands in the event's execution log instead.