Forms Tools
These tools wrap /api/v1/sites/{siteId}/forms, with /{formId}, /{formId}/actions and /{formId}/submissions beneath it,
and /api/v1/sites/{siteId}/form-capabilities.
The same API key works over REST; see
calling the same operations over REST.
See tr-forms for the full recipe.
How a form is stored
Section titled “How a form is stored”A form is a list of steps. Each step is a group of field blocks shown together, so a one-page contact form is simply a form with a single step, and a multi-step funnel is the same structure with more of them.
You rarely need to think about that. Ask for the form you want and the AI agent builds the right shape:
Create a contact form with name, email, phone (optional) and message.Recipient: hej@acme.seBuild a three-step quote request: first the property type,then square metres and timeframe, then contact details.Multi-step forms save partial answers as the visitor advances, so a drop-off after step one still tells you something.
Multi-step forms
Section titled “Multi-step forms”Each step can set its own button text with submit_label. Without one, from
render version 5 every step but the last reads
“Continue” (“Fortsätt” on Swedish sites) and the last step uses the form’s
submit_text; earlier render versions use submit_text on every step.
Visitors can go back. A “Back” (“Tillbaka”) button appears from the second step
and shows the previous step with the answers kept. Going back sends nothing;
sending an earlier step again updates the same partial submission. It is on by
default from render version 5; set the form’s allow_back to false to hide
it, or true to show it on an earlier render version.
Set show_progress to show where the visitor is: true or "text" shows
“Step 2 of 3” (“Steg 2 av 3”), "bar" a slim progress bar that screen readers
read as the same text. It counts the steps a visitor passes through, following
next. It is off by default.
Give each step either a title or a leading form/heading block, not both.
From render version 5 a step that starts with a form/heading block does not
render its title; on earlier render versions both show, and create_form and
update_form return a warning.
When a step changes, focus moves to its heading (or first label) and a polite live region announces it, with “Step X of Y” when progress is shown.
{ "allow_back": true, "show_progress": "text", "steps": [ { "id": "contact", "title": "Kontakt", "submit_label": "Nästa: företag", "blocks": [] }, { "id": "company", "title": "Företag", "blocks": [] }, { "id": "message", "title": "Meddelande", "blocks": [] } ]}In the portal, Forms → a form → Overview → Steps sets the button labels, the Back button and progress for a multi-step form.
create_form
Section titled “create_form”Creates a form. Give it fields and a recipient email, and the AI agent wraps them in a single step for you. For a funnel, describe the steps and it builds them out.
read_form
Section titled “read_form”Returns the form definition and a fresh submit_token (HMAC-signed, 24h TTL).
The AI agent fetches this when embedding the form on a page.
update_form
Section titled “update_form”Updates the definition — add or remove fields, reorder steps, change the success message or recipient.
After a submission
Section titled “After a submission”success_message is shown in place of the form when the last step is sent. It
accepts basic formatting and links (for example a booking link), and is
sanitized the same way as page text.
Set success_redirect_url to send the visitor on instead — a path on the site
(/tack/) or an https:// address such as a booking page. Other schemes are
rejected, and an empty string clears it. Visitors without JavaScript are
redirected too; a path is resolved against the page the form was on.
list_forms
Section titled “list_forms”Returns all forms defined for this site.
delete_form
Section titled “delete_form”Deletes a form. Any embed referencing it stops accepting submissions.
Submissions
Section titled “Submissions”| Tool | REST |
|---|---|
list_form_submissions |
GET /api/v1/sites/{siteId}/forms/{formId}/submissions |
read_form_submission |
GET /api/v1/sites/{siteId}/forms/{formId}/submissions/{submissionId} |
delete_form_submission |
DELETE /api/v1/sites/{siteId}/forms/{formId}/submissions/{submissionId} |
list_form_submissions reads what visitors sent, newest first, up to 200 per
page (pass next_cursor back as cursor for the next page).
read_form_submission reads one entry. For admins, both also include each
submission’s webhook delivery status (webhook_deliveries: status
(pending while a retry is scheduled, then delivered or failed), attempts,
response status, last error), as the portal shows it. delete_form_submission
removes a single entry (useful for clearing spam or a test submission) together
with its webhook delivery records. list_forms includes each form’s
submission_count. Reading submissions works with read permission; deleting
needs write permission.
Submissions contain no visitor IP address or user agent. Entries saved before
this change never return the IP address; only admins see their earlier
user_agent, shortened to 256 characters. When the site sets
form_submission_retention_days, completed submissions carry expires_at, and
an expired submission is deleted rather than listed or read. See
Forms privacy and retention.
Email notifications and webhooks
Section titled “Email notifications and webhooks”A form can email you, send the visitor a confirmation, or post selected fields
to a webhook after every submission. These are the form’s actions, the same
list the portal’s Forms editor manages. Admins read them with read_form
(webhook secrets are masked) and set them with create_form or
update_form patch={ actions: [...] }. The list replaces the current one, so
read the form first and send back every action you keep.
update_form form_id=contact patch={ actions: [ { type: "email", config: { to: "hello@example.com", subject: "New lead: {{name}}", body: "<p>{{message}}</p>", include_all: true, reply_to: "{{email}}" } }, { type: "email", config: { to: "{{email}}", subject: "Thanks, {{name}}", body: "<p>We will get back to you within a day.</p>" } }] }| Type | Config |
|---|---|
email |
to, subject, body (required; {{field}} placeholders), cc, bcc, reply_to, include_all, format (html or text) |
webhook |
url (https), fields (the field names sent), secret (signing secret; send the masked value to keep it) |
A webhook receives a signed JSON POST with only the listed fields. Up to
three webhooks of a submission get one attempt while the visitor’s request is
running, within three seconds in total; the visitor’s confirmation waits no
longer than that for webhooks and never depends on their outcome. Further webhooks, and attempts that
time out or get HTTP 408, 429 or 5xx, are retried in the background about 1, 5,
30 and 120 minutes later, five attempts in total, before the delivery is marked
failed. Other 4xx responses fail at once. Every attempt carries the same event
id (Idempotency-Key), so deduplicate on it. On a self-hosted installation the
retries run on the same scheduler as scheduled publishing, so their timing
follows its interval.
include_all appends every submitted value to the email, one row per field
in the order the fields appear in the form. Each row uses the field’s label
(the field name when it has none) and choice fields show the label of the
chosen option. Hidden fields with no value, such as utm_source when the
visitor did not arrive from a campaign, are left out. Multi-line answers keep
their line breaks.
Reading and writing actions needs admin permission, as in the portal. With
other permissions read_form returns actions: [] and a write that includes
actions is refused with 403.
Follow up on abandoned multi-step forms
Section titled “Follow up on abandoned multi-step forms”Actions run when a form is completed. An email action can instead run when a
visitor stops part-way: set trigger: "partial_abandoned" and after_hours
(1–720). It is sent once for each submission that has finished at least one
step and has not moved on for that many hours, so you hear about a lead who
gave their contact details in step 1 but never finished. A visitor who
continues resets the wait, a completed submission never sends it, and form
previews never store submissions, so they never count. The email lists the
answers given so far (include_all).
update_form form_id=lead patch={ actions: [ { type: "email", config: { to: "sales@example.com", subject: "New lead", body: "<p>Done.</p>", include_all: true } }, { type: "email", trigger: "partial_abandoned", after_hours: 2, config: { to: "sales@example.com", subject: "Unfinished lead: {{email}}", body: "<p>{{email}} stopped after the first step.</p>", include_all: true } }] }In the portal, set Send to “When a started form is abandoned” on an email
action of a multi-step form. The check runs on Typeroll’s scheduled work, the
same as scheduled publishing: Typeroll Cloud runs it for you, and a self-hosted
Core needs its scheduler (Cloud Tasks and the Cloud Scheduler job that calls
/api/internal/publish-sweep, see Self-hosting).
Without it the email goes out on the next sweep after it is due.
get_form_capabilities
Section titled “get_form_capabilities”Lists every action type the site can use, with the config fields each one
takes: the core email and webhook types and any type an installed app
provides. It also lists the prefill sources and the action triggers
(complete, and partial_abandoned for email actions with its after_hours
range). This is the same list the portal’s
Forms editor offers; read it before writing an app-provided action. Admin
permission (GET /api/v1/sites/{siteId}/form-capabilities).
Email provider
Section titled “Email provider”Emails go through the site’s email provider (Postmark, SMTP or SES), connected
under Settings → Integrations → Email & notifications or with the email tools:
get_email_settings shows whether one is connected, set_email_settings
connects it and send_test_email checks it. Without a provider, form email
actions are skipped.
Rendering
Section titled “Rendering”Forms render through the core/form block: styled inputs, client-side
validation, and the submit token wired in. You don’t hand-write form HTML.
The block’s submit_label replaces the form’s submit_text on that page
(from Core 0.2.79; earlier it was ignored).
A required radio group or checkbox group is checked by the browser too (Core 0.2.79): a radio group needs a choice, and a checkbox group needs at least one checked box, before the step can be sent. The server still validates both.
In a multi-step form the browser checks required fields and formats only for the step that is showing, so a required field in a later step never blocks an earlier one; it is checked when its step appears. Each step posts only its own fields, and the server validates them again.
Visitor messages
Section titled “Visitor messages”Everything a visitor reads that you did not write yourself follows the site’s
language (Settings → Language): Swedish on sites whose language is sv,
English otherwise. That covers button defaults, validation messages, errors
such as “Vänta en stund och försök igen.” for a step sent too quickly, the
fallback when something goes wrong and the confirmation page shown without
JavaScript. The texts are part of the published form, so a static page needs
no extra request for them. Refusals sent before the server knows the site,
such as the per-IP rate limit or an invalid token, follow the visitor’s browser
language instead. Your own texts (labels, success_message, error_messages)
are shown as written. Error codes in API responses stay the same in every
language.
Campaign parameters in hidden fields
Section titled “Campaign parameters in hidden fields”A hidden field named utm_* (utm_source, utm_medium, utm_campaign,
utm_term, utm_content or any other utm_ name) is filled from the
parameter of the same name in the address of the page the form is on. A visit
to /kontakt/?utm_source=linkedin stores utm_source: "linkedin" with the
submission and lists it in notification emails. The field’s own value is
sent when the address has no such parameter, so value: "website" on
utm_medium marks visits without a campaign. Other hidden fields always send
their own value.
Only the current page address is read, and nothing is stored in the visitor’s browser, so this needs no cookie consent. It needs JavaScript; without it the fields send their own values. A campaign is not carried from one page to the next: if visitors land on another page first, link to the form page with the parameters, or use an app that remembers the campaign (which stores it only with the visitor’s consent).
Forms in previews
Section titled “Forms in previews”Every preview renders forms in preview mode: the editor’s preview, Preview
site, preview links (get_preview_link), page and revision previews and
get_page_preview. No page or form setting is needed. A small “Preview –
nothing is sent” notice (Swedish sites: “Förhandsvisning – inget skickas”) sits
on the form, and you can click through it as a visitor would:
- each step is validated with the same rules and messages as the server, and
the next step follows the form’s step order and
nextlinks (dynamic steps are shown too); - the last step shows the success message. With
success_redirect_urlthe preview names the target (“Preview – would redirect to /tack/”) instead of leaving the preview; - nothing is sent: no submission or partial answer is stored, no email, webhook or app action runs, and nothing counts towards the submissions inbox.
Preview forms carry no submit token, and the submit endpoint refuses anything
posted from one, so even a preview opened without JavaScript cannot create a
submission. Published pages never contain the preview mode, whatever the URL.
Keep the forms runtime when you build a review copy from get_page_preview;
there is nothing to strip or stub. Test real submissions on a deployed page.
Protection
Section titled “Protection”Every submission passes three checks before it’s accepted:
- HMAC token — signed when the form is saved, so only your own forms can post
- Honeypot field — invisible to humans, filled in by naive bots
- Rate limit — per IP, to blunt floods
Token expiry
Section titled “Token expiry”The submit_token embedded in the form HTML expires after 24 hours. For forms on
long-cached static pages, the AI agent can fetch a fresh token and redeploy. On the
hosted plan, token refresh is automatic.
Validation messages and completion
Section titled “Validation messages and completion”Validation uses the site’s Settings language. Each native form field can set
required_message; API field definitions can supply error_messages keyed by
required, invalid_email, pattern, min, max or invalid_boolean.
Custom messages survive conversion to native blocks and work in preview too.
After completion, the browser focuses and scrolls to the confirmation. The
site’s sticky-header clearance still applies; --form-done-scroll-margin
(default 1rem) adds optional spacing above it.