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:
- Honeypot — an off-screen field (
_company) that real users never see; a submission that fills it is flagged. - Minimum-time guard — a submission that arrives within a few seconds of the page loading is dropped as bot-fast.
- Single-use nonce — each form load gets a fresh, server-signed token that the submission must echo, which defeats replay bots.
- Rate limit — the public submit endpoint is capped per IP.
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
- File uploads — submissions are JSON; there's no file field.
- Multi-step or conditional fields — forms are single-page, and every field always shows.
- A built-in CAPTCHA — Turnstile is the supported option (bring your own key).
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
- Zero to deployed — adds a
{% form %}to a live page. - Template language reference — the
{% form %}tag alongside the rest of the Liquid surface. - Distribution — web push and scheduled social posts, which use the same bring-your-own-webhook model.
- Email channel — compile a page into a ready-to-send email bundle.