# StaticForms > Form backend for static sites. Point an HTML form at a URL, we validate the origin, email you the submission, and log everything. ## What it does StaticForms gives any static site a form endpoint without any JavaScript or server setup. You register once, connect your SMTP2GO API key, create a form in the dashboard, and get a unique POST endpoint (`/f/{8-char-id}`). When a form is submitted: 1. The origin header is validated against the form's allowed domain 2. The submission is stored in a database 3. A notification email is sent from your own SMTP2GO account to your chosen address 4. The visitor is redirected to your thank-you URL or the default page ## Integration (HTML) ```html
``` No JavaScript required. Works with any static site generator (Hugo, Jekyll, Astro, plain HTML, etc.). ## API **Submit a form** ``` POST https://static-forms.com/f/{formId} Content-Type: application/x-www-form-urlencoded | multipart/form-data | application/json ``` - Include any form fields in the body - Add a hidden `_gotcha` field to enable honeypot spam protection - On success: HTTP 303 redirect to redirect_url or /f/thank-you - On error: JSON `{ "error": "..." }` with appropriate status code **Responses** - `303` — Submission accepted, redirecting - `403` — Origin not allowed - `404` — Form not found - `415` — Unsupported content type - `503` — Email provider not configured ## Auth & Dashboard - Sign up at https://static-forms.com/sign-up (via Clerk) - Dashboard at https://static-forms.com/dashboard - Manage SMTP2GO/Resend accounts in Settings - Create and manage forms in the Forms section ## Provisioning API (for agents / bulk onboarding) If you're an AI agent helping someone onboard many static sites at once, use this API instead of the dashboard UI. It requires an API key, created once by a human in Settings → API (a `sf_live_...` token, shown only at creation — store it securely). **Prerequisite:** you must have a connected email provider account (SMTP2GO or Resend) with at least one verified sender domain (or verified single-sender email). Connect the account and verify the sender domain there first if it isn't yet. The API auto-detects which connected account owns the **sender** domain; you never pass an account ID. Matching checks a local cache first and falls back to a live provider lookup on a miss, so a domain you just verified directly in SMTP2GO/Resend (without clicking "Refresh domains" in Settings) is still picked up immediately. `domain`, `from_email`, and `notification_email` are independent: - `domain` — the website origin the HTML form will be posted from (CORS / Origin check). It does **not** need to be a verified sender domain. - `from_email` — the From address on notification emails. Its domain **must** be verified in a connected provider account (e.g. `forms@a.com` when `a.com` is verified). Optional; see create below. - `notification_email` — who gets notified. Any valid email; it does not need to be on a verified domain (e.g. `info@c.com` is fine). - `reply_to_submitter` — when true (the default), Reply-To is the submitter's address if the posted form includes an email field. So a form can run on `c.com`, send as `forms@a.com`, notify `info@c.com`, and set Reply-To to the visitor — as long as `a.com` is a verified sender. **Create a form** ``` POST https://static-forms.com/api/v1/forms Authorization: Bearer sf_live_... Content-Type: application/json { "domain": "c.com", "from_email": "forms@a.com", "notification_email": "info@c.com", "redirect_url": "https://c.com/thanks", // optional "reply_to_submitter": true // optional, default true } ``` If `from_email` is omitted, the API looks up `domain` as a verified sender instead (legacy behaviour). From then becomes `notification_email` when that address is already on the verified domain, or `forms@{verified-domain}` if not — never an unverified From. Prefer passing `from_email` whenever the form origin and the sending domain differ. Response `201`: ```json { "id": "aB3dEf9k", "endpoint": "https://static-forms.com/f/aB3dEf9k", "domain": "https://c.com", "notification_email": "info@c.com", "from_email": "forms@a.com", "redirect_url": "https://c.com/thanks", "provider": "smtp2go", "reply_to_submitter": true } ``` Errors: `400` bad input, `401` missing/invalid API key, `402` plan form-limit reached, `422` no verified sender matching `from_email`'s domain (or `domain` when `from_email` is omitted), `429` rate limit exceeded (see **Rate limits** below). **Read a form's settings** ``` GET https://static-forms.com/api/v1/forms/{id} Authorization: Bearer sf_live_... ``` Returns JSON including `domain`, `notification_email`, `from_email`, `redirect_url`, `allow_links`, `confirmation_enabled`, `confirmation_from_email`, `confirmation_message`, and `reply_to_submitter`. **Update a form's settings** ``` PATCH https://static-forms.com/api/v1/forms/{id} Authorization: Bearer sf_live_... Content-Type: application/json { "from_email": "forms@a.com", "notification_email": "info@c.com", "redirect_url": "https://c.com/thank-you", "allow_links": false, "confirmation_enabled": true, "confirmation_from_email": "no-reply@a.com", "confirmation_message": "Thanks, we'll be in touch!", "reply_to_submitter": true } ``` All fields optional — only send the ones you want to change. Returns the full updated settings object, same shape as GET. Changing `from_email` re-checks that its domain is a verified sender (422 if not). `reply_to_submitter` (boolean, default `true`): when true, notification emails set Reply-To to the visitor if the submitted form includes an email field (`email`, `Email`, `EMAIL_ADDRESS`, or `_replyto`). The message is still sent from the form's From address (your verified sending domain), so the site owner can reply directly to the submitter. Set `false` to omit Reply-To. GET and the create `201` response include the current value. **Bulk onboarding pattern:** loop the create call once per site (there is no batch endpoint). Pass `from_email` whenever the site origin is not itself a verified sender. Each call is independent, so failures for one site (e.g. unverified sender) don't affect the others — keep going and report which domains succeeded. **Rate limits:** each API key is limited to 60 requests per minute. If you're scripting bulk onboarding (e.g. migrating 40+ sites in one run), do not fire all requests at once — space calls out to roughly 1 per second (or batch in groups of ~10-20 with a short pause between groups) to stay well under the limit and avoid overloading the request in a tight loop. If you get a `429`, the response includes a `Retry-After: 60` header — wait that many seconds before retrying, and slow down subsequent requests. This limit exists to protect the service for everyone; it is not negotiable per-key, but 60/min is enough for onboarding hundreds of sites in a few minutes if paced correctly. ## Infrastructure - Runs on Cloudflare Workers (global edge) - Storage: Cloudflare D1 (SQLite) - Email delivery: SMTP2GO (bring your own API key) - API keys encrypted at rest with AES-256-GCM