Skip to content
BetaDocs

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.

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.se
Build 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.

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.

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.

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.

Updates the definition — add or remove fields, reorder steps, change the success message or recipient.

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.

Returns all forms defined for this site.

Deletes a form. Any embed referencing it stops accepting 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.

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.

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.

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).

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.

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.

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.

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).

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 next links (dynamic steps are shown too);
  • the last step shows the success message. With success_redirect_url the 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.

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

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 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.