Forms — collect submissions from any page

StaticOwl forms follow one shape: define a form once, embed it anywhere, and watch submissions land in your inbox. A form works on your StaticOwl site and on any other site you paste it into. Submissions are stored on your site, exportable as CSV, and can fan out to whatever you already use for notifications.


Define a form

In the dashboard, go to Forms → New, add your fields, set which are required and any validation rules, and save. Or create one through the API or an AI assistant:

forms_create {
  name: "contact",
  fields: [
    { name: "email",   label: "Email",   type: "email",    required: true },
    { name: "message", label: "Message", type: "textarea", required: true },
    { name: "topic",   label: "Topic",   type: "select",   options: [
      { label: "Wholesale", value: "wholesale" },
      { label: "Support",   value: "support" }
    ] }
  ]
}

Field types: text, email, textarea, number, checkbox, and select (with options). Each field can be required and can carry validation — a max length, a pattern, a number range, or the set of choices for a select.

A form has a stable id (e.g. form:contact) that never changes, even when you edit its fields — so an embed you've already placed keeps working without a re-paste.


Embed it

There are two ways to put a form on a page, depending on where the page lives.

On a StaticOwl site — the {% form %} tag

In any template, one line renders the whole form:

{% form name:"contact" submitLabel:"Send it" successMessage:"Got it — we'll reply within a day." %}

At build time the tag emits a complete <form> — one input per field, proper <label> pairings, an off-screen anti-spam field, and a small script that posts the submission back to StaticOwl. The form's signed submit token is baked into the page, scoped to your site, so submissions can only ever land against your site.

Argument Default Effect
name: — (required) The form to render.
submitLabel: "Submit" Text on the submit button.
successMessage: "Thanks — we got it." Message shown after a successful submit.
class: — An extra CSS class on the <form> element.

On any other site — the embed widget

For a page that isn't built by StaticOwl (a hand-written page, a site on another platform, a single-page app), use the embed widget. In the dashboard, Forms → your form → Embed snippet mints a token and shows the markup:

<script src="https://app.staticowl.com/api/public/forms/widget.js"></script>
<staticowl-form token="YOUR_EMBED_TOKEN"></staticowl-form>

Drop it anywhere. The widget loads the field definitions, renders typed inputs, and posts submissions to your site. It's scoped so your host page's CSS doesn't leak in (and the widget's doesn't leak out); style it through CSS custom properties (--so-bg, --so-accent, --so-border, …). Mint the token through the API with POST /api/forms/{id}/embed-token.


Anti-spam, on by default

Every form carries baseline spam defenses with no setup:

A flagged submission still lands in your inbox (with a spam score) rather than being silently dropped — a false positive never swallows a real lead. For high-abuse sites you can add Cloudflare Turnstile: include a Turnstile widget on the page with your own site key, and your StaticOwl operator enables verification on the server.


See what came in

Submissions land in the dashboard under Forms → your form, newest first, with an unread badge. Open one to see its fields; it's marked read on open. Export gives you every submission as CSV (escaped so values with commas or quotes round-trip cleanly). Each submission also records the page it came from.

Through an assistant or the API, list submissions with forms_get_submissions (MCP) or GET /api/forms/{id}/submissions — unread by default, add ?includeRead=true for all.


Get notified on submit

StaticOwl stores submissions; it doesn't send email itself (the same reason it doesn't run SMTP for the email channel — deliverability, SPF/DKIM/DMARC, and bounce handling are better left to a provider you already trust). Instead, each submission can fire a webhook you route however you like.

In Forms → your form → Notify on submit, paste a webhook URL — a Zapier "Catch Hook", a Make or n8n webhook, a Slack or Discord channel webhook, or your own endpoint. StaticOwl POSTs a small JSON notice:

{
  "event": "form.submitted",
  "siteId": "site:your-site",
  "at": "2026-05-29T13:14:15.000Z",
  "data": { "id": "formsub:abc123", "formId": "form:contact" }
}

The notice is intentionally small and low-PII. To act on the actual field values, have your handler fetch them with GET /api/forms/{formId}/submissions. From there, a Zapier or Make workflow can email you, post to a channel, or push the lead into a CRM — and you decide what data leaves your environment.


What's not supported


API and MCP reference

Route MCP tool Purpose
GET /api/forms forms_list List the site's forms.
POST /api/forms forms_create Create a form (name, fields).
GET /api/forms/{id} forms_get Get one form.
PUT /api/forms/{id} forms_update Update a form (preserves its id).
DELETE /api/forms/{id} forms_delete Delete a form (its submissions stay queryable).
GET /api/forms/{id}/submissions[.csv] forms_get_submissions List submissions, or download them as CSV.
POST /api/forms/{id}/embed-token — Mint an embed token for the widget.
GET /api/public/forms/{token}/definition — Public field-definition fetch (used by the widget).
POST /api/public/forms/{token}/submit — Public submission endpoint.

All dashboard routes need a write role on the site; the two /api/public/forms/* routes are what the embedded form itself calls.


See also

Related docs

Resolved at build time via {% similar %} — cosine similarity over embeddings, not tag overlap. Zero arguments.

HTTP API
67% match
Integrations & webhooks — connect StaticOwl to other systems
67% match
Zero to deployed — the 5-minute quickstart
66% match