Překlad připravujeme. Tato stránka je zatím v angličtině — překlad doplňujeme. · Translation in progress. This page is still in English.

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.

Editing a definition doesn't rewrite a page. The definition (recipient, required fields, origins) controls the gateway and the generated snippet — it takes effect instantly. But a form already published on a page is plain HTML; changing the definition won't add or remove fields there. To change what a page shows, copy the updated snippet and paste it back onto that page.

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": "…" }

Put it on a page — two ways

Dynamic embed (auto-updating)

Paste a placeholder and one script. The fields are pulled live from Flowsmith and rendered using your page's own styling (we output plain inputs). Edit the fields in the admin (or via the API) and every embedding page updates itself — nothing to re-paste.

<div data-kh-embed="madhouse/contact"></div>
<script src="https://forms.flowsmith.online/assets/js/kh-forms-embed.js" defer></script>

Needs JavaScript. Under the hood it reads a public, non-sensitive definition at GET /f/<site>/<form>/def (fields, Turnstile sitekey, endpoint — never the recipient).

Multilingual, automatically. The embed renders in the page's language — it reads the page's <html lang> (override with data-kh-lang="de", fallback the visitor's browser, then English). Labels, the button, status messages and locale-true example placeholders (a native-looking name, phone, company) all come back translated, and the form flips to right-to-left for languages like Arabic and Hebrew. Around 100 ISO language codes are recognised; any not yet translated render correctly in English until their words are filled in. On a multi-language site the form simply switches with the page.

Static snippet (fixed)

The generated HTML (from the CLI snippet command or the admin's Snippet button). Works without JavaScript; the fields are fixed at paste time — to change them, paste the new snippet.

Which to use? Dynamic embed for central control and auto-updates across many pages; static snippet when you need a no-JS fallback or full control of the markup.

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.