Manuals / Forms gateway

Which version is for me? Two ways to read this guide. Step by step is for anyone — even if you've never opened a terminal; it walks you through it and includes a builder that writes the command for you. Quick reference (this page) is for developers who just want the parameters and the HTTP API.

Deploy a form

Put a working contact form on any site in one command. The gateway handles spam protection, storage and email notification — you just declare the form. New to terminals? Try the step-by-step walkthrough with a form builder that writes the command for you.

How it works

One gateway serves every site. A form is a definition stored in a database — not code — so adding or changing one is a data write that takes effect immediately, with no redeploy.

A page posts its fields to https://forms.flowsmith.online/f/<site>/<form>. The gateway validates, stores the submission, and emails a notification to the form's recipient. You define the form once; the markup to paste into the page is generated for you.

Authentication

Managing forms is done with a service token sent as a header:

Authorization: Bearer <FORMS_ADMIN_TOKEN>
The token is a secret. It is issued to you privately by Flowsmith and lets the holder create and change forms. Keep it out of source control, client-side code and screenshots. It only grants form management — it cannot read submitted data (that stays behind the password-protected admin). If a token leaks, tell us and we'll rotate it.

CLI — onboard-form.mjs

The script wraps the API so you never touch raw HTTP. It needs Node 18+ and the repo it ships in. Don't have Node yet? See Set up your computer. (No tooling at all? The step-by-step guide does the same job with a single curl line — nothing to install.)

It reads two environment variables:

VariableRequiredMeaning
FORMS_ADMIN_TOKENrequiredYour service token.
FORMS_BASEoptionalGateway base URL. Default https://forms.flowsmith.online.

Commands

CommandWhat it does
listList all form definitions.
get <site> <form>Show one definition.
put <site> <form> [flags]Create or update a form (idempotent).
snippet <site> <form>Print the ready-to-paste HTML for the page.
delete <site> <form> [--hard]Deactivate a form (--hard removes it entirely).

Flags for put

FlagRequiredMeaning
--recipient <email>requiredWhere notifications go. Must be a verified destination in our email routing.
--origins <a,b>optionalComma-separated list of sites allowed to post (apex + www). CORS allowlist.
--require <a,b,c>optionalComma-separated required field names (e.g. name,email,message).
--subject "…"optionalSubject line of the notification email.
--label "…"optionalHuman-readable name shown in the admin.
--sender "Name <from@…>"optionalFrom override. Defaults to the gateway sender.
--no-turnstileoptionalDisable the Turnstile check (integration testing only).

Example — onboard a whole site in one line

FORMS_ADMIN_TOKEN=… node scripts/onboard-form.mjs put madhouse contact \
  --recipient [email protected] \
  --origins https://madhouse.vip,https://www.madhouse.vip \
  --require name,email,message

Then get the markup to paste into the page:

FORMS_ADMIN_TOKEN=… node scripts/onboard-form.mjs snippet madhouse contact

HTTP API

If you'd rather call it directly (or from an AI agent), the endpoints under /admin/api/forms mirror the CLI. All require the Authorization: Bearer header.

MethodPathPurpose
GET/admin/api/formsList all definitions
GET/admin/api/forms/{site}/{form}One definition
PUT/admin/api/forms/{site}/{form}Create or update (idempotent)
DELETE/admin/api/forms/{site}/{form}Deactivate (?hard=1 to remove)
GET/admin/api/forms/{site}/{form}/snippetReady-to-paste HTML

site and form are slugs: lowercase letters, digits and hyphens.

PUT body

{
  "recipient": "[email protected]",
  "origins": ["https://madhouse.vip", "https://www.madhouse.vip"],
  "subject": "New MADHOUSE enquiry",
  "label": "MADHOUSE — Contact",
  "require_turnstile": true,
  "active": true,
  "schema": { "fields": [
    { "name": "name",    "required": true },
    { "name": "email",   "required": true },
    { "name": "message", "required": true }
  ] }
}

Response: { "ok": true, "created": true|false, "site": "…", "form": "…" }

Form fields

Any field your form posts is stored and shown in the admin. A few names are special:

FieldMeaning
emailValidated as an email; used as the Reply-To so you can reply straight from your inbox.
messageRendered as a textarea in the generated snippet.
company_websiteThe honeypot — keep it in the markup, hidden, always empty. Bots fill it; real users don't.
_redirectOptional hidden field; for no-JS posts the gateway redirects here on success.

Mark fields required either with --require (CLI) or the schema body. Required fields are enforced both in the browser and on the server.

Onboarding recipe

  1. Define the formput <site> <form> --recipient … --origins …. It's live immediately.
  2. Get the markupsnippet <site> <form>. Paste the two script tags once per page and the form where it should appear.
  3. Allowed origins — the --origins you set are the CORS allowlist; include apex and www.
  4. Turnstile hostname — add the site's hostname to the kh-forms Turnstile widget (currently a one-off manual step in Cloudflare).

Two common patterns

One form, many pages

Define a form once and paste the same snippet on as many pages as you like — home, about, footer. They all post to the same /f/<site>/<form>, land in one inbox and group together in the admin. Just list every hostname those pages live on in origins (apex + www, and any subdomain).

A different form for one section

When a section needs its own structure — say an academy with different fields or a different recipient — define a second form under the same site with its own slug:

node scripts/onboard-form.mjs put photorobot academy \
  --recipient [email protected] \
  --origins https://photorobot.com,https://academy.photorobot.com \
  --require name,email,course,message

A site can have any number of forms; each has its own fields, recipient and snippet. If the section lives on a different subdomain, add that subdomain to origins.

What you get for free

Every form on the gateway is protected the same way, without you configuring anything:

Submitted data is private. It's visible only in the password-protected admin (sign-in by one-time code to an authorised email) — the management token used for this guide cannot read it.