Skip to content

Forms

Forms are defined in the admin and submitted from your website. Creating or renaming a form needs the Admin role; managing its fields and triaging submissions needs Editor.

text, textarea, email, url, number, select, checkbox, date and file. Fields can be required or optional. Validation runs on the server against the form’s own field definitions, and text input is sanitized before it is stored. email fields are trimmed and stored in lowercase.

POST /api/v1/public/forms/{slug}/submissions is rate limited (5 requests per 60 seconds per IP). It needs no session unless the form requires a website account.

  • A form without a file field takes a JSON body.
  • A form with a file field must be submitted as multipart/form-data.

From Astro, cms.forms.submit({ formSlug, data }) does both: pass a plain object for JSON, or a FormData for files (@kenresoft-cms/astro 0.8.0 and later; older versions only send JSON).

A validation failure returns 400 with issues (path and message per field). See Astro API client.

Forms > (form) > Edit > Refuse duplicates on takes field names, comma separated, for example app, email on a waitlist. A submission with the same values for all of those fields as an earlier one is refused with 409. Values are compared trimmed and in lowercase, and only real submissions count: test submissions are ignored.

The check is a unique index in the database, so two submissions arriving at the same moment can’t both get through. Leave the setting empty to allow duplicates, which is how every form behaves until you set it. Existing submissions are never changed or checked.

Tick Require a Turnstile human check to make the public submit route verify a Cloudflare Turnstile token before it reads the body or stores anything. Your site sends the token in the x-turnstile-token header (turnstileToken in cms.forms.submit).

It only applies when the deployment has TURNSTILE_SECRET_KEY set (see environment variables). With the secret unset the tick does nothing, so turning it on never locks out a deployment without Turnstile. Without a token the API answers 400 TURNSTILE_REQUIRED, with a bad or reused token 403 TURNSTILE_FAILED, and 503 TURNSTILE_UNAVAILABLE if Cloudflare can’t be reached (it fails closed). These responses have the shape { "code": "…", "error": "…" }. A token works once, so reset the widget after a failed attempt.

A file field accepts PDF, DOCX and PNG/JPEG/GIF/WebP, up to 10 MB each. The type is decided by the file’s content, never its name or the browser’s declared type, so a crafted request can’t get around your site’s accept attribute. Legacy .doc, macro-enabled Word files renamed to .docx, empty files and anything unrecognized are refused with a 400.

To narrow a field further, tick Accepted files when you add or edit it: PDF, Word (.docx) and/or Images. Through the API this is the field’s config, for example { "accept": ["pdf", "docx"] }. With nothing ticked, the field accepts any of the supported types.

Files are stored as private Media in R2. They never appear in the Media Library grid and are downloaded only through authenticated routes: staff through the admin, and the owning website account through its own account routes.

A submission is stored all-or-nothing. Its files are saved first, and if any file, the submission itself or its attachment links can’t be stored, everything already saved is removed and the API answers 500. A successful response always means every submitted file exists.

The Submissions views list submissions per form or across all forms, with status new, read or archived, an attachment indicator, the stage if the form has stages, and a detail panel. A form’s page can also create test submissions, which are flagged as tests.

Export CSV on a form’s Submissions page downloads its real submissions, oldest first, as a spreadsheet file: the time, the status and one column per field. Test submissions are left out. Cells that begin with =, +, - or @ are prefixed with an apostrophe so a spreadsheet shows them as text instead of running them. It exports what the admin has loaded, and only signed-in staff who can see the form’s submissions can use it. File fields appear as their JSON details, not the file itself.

A form can require a website account: Forms > (form) > Edit > Require a website account. Then:

  • only a signed-in account with a verified email can submit it, from an origin in CORS_ORIGINS. Without a session the API answers 401, and without a trusted Origin it answers 403
  • the submission belongs to that account. The owner always comes from the session, never from the request body
  • the account can follow its own submissions from your site through the account routes: its list, each submission’s stage and history, the conversation, and its files. Another account’s submission, or a file that isn’t part of the submission, is always a 404

Website accounts have role none. Being able to use the account routes never gives access to the admin API.

Account page URL (same dialog) is where a submission can be viewed on your site, with {id} standing in for the submission id, for example https://example.com/requests/{id}/. When it’s set, stage-change and reply emails link there. Leave it empty if your site has no such page.

The submission’s detail panel shows the linked account. If that account is later deleted, the submission, its files and its conversation stay: the panel shows the account as No longer available, replies go to the email address in the submission’s own data, and nobody can follow it on the site any more.

Existing anonymous forms keep working exactly as before.

A form can be about one published entry, such as a job application for a vacancy. In the form’s Edit dialog set About an entry of this content type to a content type slug, then submit with the entry’s slug in the URL:

POST /api/v1/public/forms/job-application/submissions?context=marketing-specialist

The server looks the entry up itself. A missing or unpublished entry returns 404, and a request without context returns 400. The reference and a title snapshot are stored with the submission and cannot be changed by the visitor, so a context value in the form body is ignored. Staff see the entry in an About card, the owning account gets it as context from its submissions endpoints, and notification emails name it. The stage-update email template can use {{context.title}}.

To stop accepting submissions when the entry is closed, add rules as field = value lines, for example status = Open and applicationEnabled = true, and optionally a closing-date field. A closed entry returns 409. A date field with only a day stays open for that whole day (UTC).

A form can list its own ordered stages (Progress stages in the Edit dialog, one per line, up to 12), for example Submitted, In Progress, Completed. New submissions start in the first stage. Staff change the stage from the submission’s detail panel, and every change is recorded in a history the owning account can also see.

A stage is separate from the inbox status (new, read, archived), which is for staff only and never shown to the account. When a form has an Account page URL, a stage change emails the owning account through the Request update (website account) email template, unless you untick the email option for that change.

  • Notifications: a form can list notification email addresses. Each submission then emails them, and so does each message the owning account sends from your site. This is opt-in per form and needs email configured; there is no deployment-wide default recipient.
  • Replies: from a submission’s detail panel you can reply to the sender with a rich-text message and attachments. The reply is sent through your configured email provider, Reply-To defaults to your configured Email sender address, or the replying staff member’s own address if none is set, and the reply is stored and shown as a thread on the submission. Uploaded attachments are kept as private Media, so they stay downloadable from the thread after the email has gone. Reply is disabled, with an explanation, if no email provider is configured.
  • Account-linked submissions: a reply goes to the account’s email address, also appears in the account’s view of the submission, and links to the Account page URL when one is set. The account can answer from your site with a message and up to 5 files, which appear in the same thread marked as sent from the site.
  • Reply in your own mail app: a per-person Preferred mail client setting on your Profile (Gmail, Outlook, Yahoo, Zoho, or the default mailto: handler) controls what the “Reply in email app” action opens.

Sending from the CMS is limited to 10 emails per 60 seconds per staff user. See Rate limiting.