All docs
3 min read

Validation

Hosted forms validate twice: in the browser (instant feedback) and on the server (so a hostile client can't bypass it). Both layers read from the same field schema, so what you see in the builder is exactly what we enforce.

Legacy forms — the kind where you write your own HTML — don't get schema validation. You're on the hook for that yourself; we still run honeypot, captcha, and the spam stack on every submission.

Per-field options

Every input field exposes a validation block in the inspector:

{
  "type": "text",
  "name": "username",
  "label": "Username",
  "required": true,
  "validation": {
    "min": 3,
    "max": 30,
    "pattern": "^[a-z0-9_]+$",
    "error_message": "3-30 chars, lowercase letters, digits, underscores."
  }
}

required

Field must be present and non-empty. For checkbox, at least one option ticked. For consent, the box must be checked.

min / max

  • For text-ish fields (text, textarea, email, url, tel): character count.
  • For number: numeric range.
  • For date: ISO date range (min: "2024-01-01").
  • For file: file count when multiple: true.

pattern

A JavaScript-style regex (no leading/trailing slashes). Applied to the submitted string. Use it for custom shapes:

"pattern": "^[A-Z]{2}\\d{6}$"

Bad regex doesn't crash submissions — we log it and skip the rule.

error_message

Override the default error string. Plain text, shown next to the field on failure.

File validation

file fields have their own rules on top of min/max:

  • MIME type — comes from the field's accept array. Anything not on the list is rejected before upload.
  • Per-file size — capped per plan (5 MB Free, 25 MB Pro, 100 MB Team).
  • Total upload size — same cap, summed across multiple uploads.

If a file is rejected, the submission fails — partial uploads don't land in your inbox. See Files → for the full table.

Multi-step gating

When a form has multiple steps, each Next button validates only the fields on that step. Visitors can't advance with errors pending. The final Submit validates everything — including any field they may have skipped past via browser-back.

If a field is hidden by a visibility rule, it's skipped entirely. Hidden = not validated, not submitted.

Custom error messages

Two levels:

  • Per-field — set validation.error_message on the field. Wins for that field's rules.
  • Per-form — under Settings → Errors, you can override the default messages globally (This field is required, Must be at least {min} characters, etc.). Useful for non-English forms.

Server-side enforcement

Every submission goes through SchemaValidator on the server before it's persisted. The validator:

  1. Drops keys that aren't in the schema (honeypot is the obvious one).
  2. Coerces types (string → number, ISO date → date).
  3. Applies the rules above.
  4. Returns 422 with a JSON body of { "errors": { "field_name": ["message"] } } if anything fails.

Your fetch caller can show those messages directly, or you can let the hosted page render them.

Legacy forms

If you're using legacy mode (your own HTML pointed at the endpoint), only the spam stack runs. Add required, pattern, minlength, maxlength to your <input> tags directly — that's the only validation layer. We don't validate against a schema because there is no schema.

What's next