# Static Forms
> Static Forms is a secure form backend for static websites. It lets teams collect form submissions without server-side code and deliver them by email, webhooks, Google Sheets, Zapier, and dashboard storage.
Use this file to find concise, canonical documentation for integrating forms with Static Forms. The primary submission endpoint is `https://api.staticforms.dev/submit`, and forms authenticate by including an account `apiKey` field in a POST request.
Prefer the documentation links for implementation details, examples, request fields, spam protection setup, and troubleshooting. Product and optional links provide broader context when needed.
## Product
- [Home](https://www.staticforms.dev/): Product overview for the Static Forms form backend.
- [Features](https://www.staticforms.dev/features): Overview of form delivery, spam protection, integrations, and dashboard features.
- [Pricing](https://www.staticforms.dev/pricing): Plan limits, billing options, trials, and feature availability.
- [Examples](https://www.staticforms.dev/examples): Example form patterns for common static-site use cases.
- [Templates](https://www.staticforms.dev/templates): Ready-made form templates that can be adapted for different sites.
- [Contact](https://www.staticforms.dev/contact): Support and sales contact page.
## Getting Started
- [Introduction](https://www.staticforms.dev/docs): Welcome to Static Forms
- [Quick Start](https://www.staticforms.dev/docs/quick-start): Create your first form in 5 minutes
- [How It Works](https://www.staticforms.dev/docs/how-it-works): The form submission flow end-to-end
- [App Tour](https://www.staticforms.dev/docs/app-tour): Inbox, Forms, Workspace, and Account
- [Per-form vs Workspace Settings](https://www.staticforms.dev/docs/per-form-vs-workspace): Where each setting lives in v3
## Forms
- [Forms Overview](https://www.staticforms.dev/docs/forms/overview): Creating, organizing, and managing forms
- [Form Basics](https://www.staticforms.dev/docs/forms/form-basics): HTML form fundamentals and field naming
- [Create a Form](https://www.staticforms.dev/docs/forms/create-a-form): The new-form flow in the app
- [Form Editor Tour](https://www.staticforms.dev/docs/forms/editor-tour): The tabs at a glance
- [Form Builder](https://www.staticforms.dev/docs/forms/form-builder): Design a form visually and embed it with one script tag
- [General](https://www.staticforms.dev/docs/forms/general): Name, status, API key, embed snippet, and data retention
- [Delivery](https://www.staticforms.dev/docs/forms/delivery): Recipients, CC, webhooks, Sheets, Slack
- [Rules](https://www.staticforms.dev/docs/forms/rules): Conditional routing, actions, and dry-run testing (Pro/Agency)
- [Custom Domain](https://www.staticforms.dev/docs/forms/custom-domain): Send from your own domain (Pro)
- [Branding](https://www.staticforms.dev/docs/forms/branding): Remove branding, submission & reply email templates (Pro)
- [Auto-reply](https://www.staticforms.dev/docs/forms/auto-reply): Templated confirmation emails
- [AI Reply](https://www.staticforms.dev/docs/forms/ai-reply): AI-generated responses (Pro)
- [Security](https://www.staticforms.dev/docs/forms/security): Captchas, honeypot, and domain restriction
- [Honeypot Field](https://www.staticforms.dev/docs/forms/security/honeypot): Invisible bot trap
- [reCAPTCHA](https://www.staticforms.dev/docs/forms/security/recaptcha): Google reCAPTCHA v2 and v3 setup
- [Cloudflare Turnstile](https://www.staticforms.dev/docs/forms/security/turnstile): Cloudflare Turnstile setup
- [hCaptcha](https://www.staticforms.dev/docs/forms/security/hcaptcha): hCaptcha setup
- [ALTCHA](https://www.staticforms.dev/docs/forms/security/altcha): Privacy-friendly captcha
- [Domain Restriction](https://www.staticforms.dev/docs/forms/security/domain-restriction): Whitelist approved submission domains
- [Spam Filter](https://www.staticforms.dev/docs/forms/spam-filter): Content-based spam routing
- [Application Forms](https://www.staticforms.dev/docs/forms/application-forms): Job applications with file uploads
- [File Uploads](https://www.staticforms.dev/docs/forms/file-uploads): Handle file attachments
## Inbox
- [Inbox Overview](https://www.staticforms.dev/docs/inbox/overview): View, search, and manage submissions
- [Managing Submissions](https://www.staticforms.dev/docs/inbox/managing-submissions): Reply, mark, delete, and inspect delivery
- [Notifications](https://www.staticforms.dev/docs/inbox/notifications): How submission emails work
## Workspace
- [Workspace Overview](https://www.staticforms.dev/docs/workspace/overview): Team, plan, billing, credits, and domain
- [Team](https://www.staticforms.dev/docs/workspace/team): Invite collaborators with role-based access
- [Plan & Usage](https://www.staticforms.dev/docs/workspace/plan): Tier limits and usage metrics
- [Billing](https://www.staticforms.dev/docs/workspace/billing): Invoices, payment method, and overage billing
- [AI Credits](https://www.staticforms.dev/docs/workspace/credits): Top up and track AI reply credits
- [Custom Domain](https://www.staticforms.dev/docs/workspace/custom-domain): Register workspace-wide sender domains
## Account
- [Profile](https://www.staticforms.dev/docs/account/profile): Name, email, and avatar
- [Security](https://www.staticforms.dev/docs/account/security): Sign-in and connected accounts
- [Notifications](https://www.staticforms.dev/docs/account/notifications): Email preferences
- [API Key](https://www.staticforms.dev/docs/account/api-key): Where API keys live in v3
- [Delete Account](https://www.staticforms.dev/docs/account/delete): Permanently remove your account
## Integrations
- [Overview](https://www.staticforms.dev/docs/integrations): Connect Static Forms to other services
- [Slack](https://www.staticforms.dev/docs/integrations/slack): Send submissions to Slack channels
- [Discord](https://www.staticforms.dev/docs/integrations/discord): Send submissions to Discord channels
- [Telegram](https://www.staticforms.dev/docs/integrations/telegram): Post submissions to a Telegram chat
- [Make](https://www.staticforms.dev/docs/integrations/make): Visual automation platform
- [n8n](https://www.staticforms.dev/docs/integrations/n8n): Self-hosted workflow automation
- [Google Sheets](https://www.staticforms.dev/docs/integrations/google-sheets): Save submissions to spreadsheets
- [Notion](https://www.staticforms.dev/docs/integrations/notion): Save to Notion databases
- [Airtable](https://www.staticforms.dev/docs/integrations/airtable): Store in Airtable bases
- [Mailchimp](https://www.staticforms.dev/docs/integrations/mailchimp): Add subscribers to a Mailchimp audience
## Framework Guides
- [Overview](https://www.staticforms.dev/docs/framework-examples): Framework integrations
- [React](https://www.staticforms.dev/docs/react): React integration guide
- [Next.js](https://www.staticforms.dev/docs/nextjs): Next.js integration guide
- [Vue.js](https://www.staticforms.dev/docs/vue): Vue.js integration guide
- [Angular](https://www.staticforms.dev/docs/angular): Angular integration guide
- [Svelte](https://www.staticforms.dev/docs/svelte): Svelte integration guide
- [Astro](https://www.staticforms.dev/docs/astro): Astro integration guide
- [Plain HTML](https://www.staticforms.dev/docs/html): Vanilla HTML forms
## Platform Guides
- [Netlify](https://www.staticforms.dev/docs/netlify): Deploy on Netlify
- [Vercel](https://www.staticforms.dev/docs/vercel): Deploy on Vercel
- [Cloudflare Pages](https://www.staticforms.dev/docs/cloudflare-pages): Deploy on Cloudflare
- [GitHub Pages](https://www.staticforms.dev/docs/github-pages): Deploy on GitHub Pages
- [WordPress](https://www.staticforms.dev/docs/wordpress): WordPress integration
- [Webflow](https://www.staticforms.dev/docs/webflow): Webflow integration
- [Wix](https://www.staticforms.dev/docs/wix): Wix integration
## API Reference
- [Complete Reference](https://www.staticforms.dev/docs/api-reference): Endpoints, parameters, responses, and error codes
## Troubleshooting
- [Common Issues](https://www.staticforms.dev/docs/troubleshooting): Solutions to common problems
- [Debugging](https://www.staticforms.dev/docs/debugging): Debug form submissions
- [CORS Issues](https://www.staticforms.dev/docs/cors-issues): Fixing CORS errors
- [Email Delivery](https://www.staticforms.dev/docs/email-delivery): Email troubleshooting
## Help
- [FAQ](https://www.staticforms.dev/docs/faq): Frequently asked questions
- [Get Support](https://www.staticforms.dev/docs/support): Contact support
- [Changelog](https://www.staticforms.dev/docs/changelog): Recent updates
## Optional
- [Blog](https://www.staticforms.dev/blog): Tutorials, announcements, and implementation guides.
- [Alternatives](https://www.staticforms.dev/alternatives): Comparisons with other form backend and static-site form services.
- [Tools](https://www.staticforms.dev/tools): Free tools for building, checking, and debugging forms.
- [Form Checker](https://www.staticforms.dev/tools/form-checker): Tool for validating Static Forms markup and configuration.
- [Form Generator](https://www.staticforms.dev/tools/form-generator): Tool for generating starter form markup.
- [Webhook Tester](https://www.staticforms.dev/tools/webhook-tester): Tool for testing webhook payload delivery.
- [Privacy Policy](https://www.staticforms.dev/privacy-policy): Privacy and data handling policy.
- [Terms](https://www.staticforms.dev/terms): Terms of service for Static Forms.
- [Full Content](https://www.staticforms.dev/llms-full.txt): Same index plus inlined Markdown content for every published blog post.
# Blog (Full Content)
## EU Data Residency for Submissions and File Uploads
Source: https://www.staticforms.dev/blog/eu-data-residency-form-submissions-file-uploads
Date: 2026-07-26
Author: Static Forms Team
Every form backend makes a quiet decision on your behalf: where the data lands. For most of them the answer is "somewhere in the United States", and you only find out when a customer sends you a DPA questionnaire asking which country their applicants' CVs are stored in.
Static Forms now lets you make that decision yourself. Pick **European Union** when you sign up and your form data — submissions, file attachments, webhook delivery logs and AI reply logs — is stored in AWS `eu-central-1` (Frankfurt) instead of `us-east-1`. Not replicated to the US, not cached in the US, not written to a US table first and copied over. The row and the uploaded file are created in Frankfurt and read back from Frankfurt.
This post is the engineering detail behind that sentence: how a submission is routed, where an attachment physically sits, how the download link is signed, and — the part most residency announcements skip — the things that deliberately stay in the United States for every account. The exact legal scope is in our [DPA](https://www.staticforms.dev/dpa); this is the same scope, written for developers.
---
## The split: control plane vs data plane
The architecture has one rule, and everything else follows from it. **Account state is global. Form data is regional.**
| Stays global (US) | Regional (follows your pick) |
|---|---|
| Your user record, login, billing | Form submissions |
| Form/workspace configuration and API keys | File attachments (S3 objects + file records) |
| Rate-limit counters, storage quota counters | Webhook delivery logs |
| Email delivery (SES) | AI reply logs |
| Operational request logs | Submission retention and cleanup |
Why keep the control plane global? Because it is what lets there be **one product**. There is no `eu.staticforms.dev`, no second dashboard, no separate login, no parallel release train, and no second API key to manage. You log in at [staticforms.dev](https://www.staticforms.dev), and your API key resolves the same way regardless of region. Only the tables and buckets your submissions live in change.
> Your account record — owner email and anonymized consent IP — stays in the US `users` table for every account, including EU ones. We state that plainly in the DPA rather than engineering around it, because pretending otherwise would be the interesting kind of lie.
---
## What happens to a submission
There is a single ingest endpoint for everyone: `https://api.staticforms.dev/submit`. You do not swap it for a regional URL, and your HTML does not change.
```html
Contact Us
```
Behind that request:
1. **The API key is resolved in the global control plane.** One lookup tells us which account owns the form, what plan it is on, and which region it is pinned to.
2. **Validation runs before anything is stored** — spam checks, CAPTCHA, rate limits, plan limits. Nothing region-specific here.
3. **The submission row is written to the owning account's region.** For an EU account that is the Frankfurt `form_submissions` table. Not a US table with a region flag on it — a different table, in a different AWS region.
4. **Every later write follows the same route.** Delivery status, read/unread, spam and not-spam, timeline entries, webhook attempt logs, AI reply logs. They are all keyed to the region resolved in step 1, including the ones written asynchronously by queue consumers, because the region travels inside the queue payload.
The consequence worth internalizing: **an EU account's submission never exists as a row in `us-east-1`.** We verified that the hard way before shipping — after a full end-to-end pass (signup, submission, attachment, inbox, mutations, export, webhook, AI reply, deletion) the US tables held **zero** rows for that account across submissions, file uploads, webhook logs and AI reply logs.
---
## What happens to a file attachment
File uploads are the part where residency usually leaks, because a file has two lives: the bytes in object storage, and the metadata row that makes them findable. Both are regional.
Adding uploads to the form above is one attribute and one input — again, identical for both regions:
```html
```
What the platform does with `document`:
- ✅ **The bytes are uploaded to your region's private bucket.** Each region has its own S3 bucket — bucket names are globally unique, so this is a genuinely separate bucket in a separate AWS region, with Block Public Access on and `AES256` server-side encryption.
- ✅ **The file record is written to your region's `file_uploads` table**, alongside the submission it belongs to. The record — original filename, MIME type, size, object key — is data-subject content, so it lives where the submission lives.
- ✅ **Downloads are signed against the regional bucket.** Clicking a download in your [Inbox](https://www.staticforms.dev/app/inbox) mints a fresh presigned `GET` URL that expires in **5 minutes**, signed for the bucket the object is actually in. For an EU account the bytes travel from Frankfurt to your browser — there is no US proxy hop and no US copy to serve from.
- ✅ **Storage quota accounting stays global.** The counter is a number of bytes on your account record, not form data, so it lives in the control plane with the rest of your account state. The bytes it counts never leave your region.
- ✅ **Expiry and deletion happen in region.** Delete a submission and its file records and S3 objects are removed from your region's table and bucket. The retention sweep visits **every** region rather than assuming US, so an EU account's expired attachments are actually collected instead of lingering forever.
One deliberate failure mode, since it affects what you see: if persistent storage is unavailable for a file, the submission is still accepted and the file is still delivered by email, but no download button appears for it in the inbox. A storage problem never costs you a lead — and it never quietly falls back to storing the file in another region either.
File uploads are a paid-plan feature (4.5MB per file). See [file uploads in the docs](https://www.staticforms.dev/docs/forms/file-uploads) for limits and framework snippets, or the walkthrough in [Next.js contact form with document upload](/blog/nextjs-contact-form-document-upload).
---
## Teams that span regions
An agency owner pinned to the EU can invite a teammate whose own account is US-region — or who signed up before regions existed at all. Routing that teammate's reads by *their* region would query an empty Frankfurt-less table and render the workspace as if it had no data.
So routing follows **the account that owns the data**, never the person looking at it. A US-based teammate opening an EU workspace reads from Frankfurt, and when they mark a submission read, reply, or unmark spam, the write lands in Frankfurt too. Nothing about cross-region team access pulls a copy into the US.
---
## What EU residency does not move
This is the section we would rather you read twice than discover later. Selecting the EU region relocates **storage of form data**. It does not relocate everything, and we will not market it as "your data stays in the EU".
- **Email delivery is US, for every account.** Notification and auto-responder emails are sent through AWS SES in `us-east-1`, with Singapore (`ap-southeast-1`) as delivery failover. The email carries the submission, so this is a transmission out of the region — disclosed in our [DPA](https://www.staticforms.dev/dpa) §8 and in the [Transfer Impact Assessment](https://www.staticforms.dev/dpa/transfer-impact-assessment), and covered by SCCs / the EU-U.S. Data Privacy Framework. SES does not retain message content, but it does keep a per-region suppression list of recipient addresses.
- **Operational request logs are US, for every account.** Request metadata — IP, user agent, referer, delivery status, and form field **names** — is logged in the US for 365 days. **Field values are never logged**, by design and enforced in code: the submit path logs the keys of your payload, never the contents. Keeping this pipeline single-region is what keeps our abuse tooling working; an attacker should not be able to buy log immunity by picking a region at signup.
- **Account and billing data are US, for every account** (see the table above).
The supportable claim is precise: *form data stored in the EU, email delivered from the US, operational logs in the US.* That is what the DPA, the sub-processor table, Annex III and the TIA all say, and it is what the product actually does.
---
## Choosing and changing your region
- **At signup.** The region picker sits above both signup paths — email and Google — and defaults to United States. It applies to the account on its **first** write, so an OAuth signup gets the region you picked, not a US default it has to be rescued from later.
- **After signup, while you are empty.** You can change the region from your [account page](https://www.staticforms.dev/app/account) for as long as the account holds **no** stored form data. The check runs server-side across every region — submissions and file uploads, not just the pinned region — and fails closed: if the check itself errors, the change is refused. Refusing a change is recoverable; splitting an inbox across two continents is not.
- **Once your first submission arrives, the pin is final.** Not a policy choice, a physical one: the rows and objects are in that region, and there is no migration that keeps a live inbox coherent while it runs. The account page shows the pin as locked with the reason.
Pick deliberately, then. If you are collecting personal data from EU residents — job applications, patient intake, member records — the EU region is the one that removes the third-country transfer for that data at rest.
---
## Adding regions from here
The region registry is config, not code: a region is an entry with an AWS region, a label, an uploads-bucket environment variable and an email-send region. Every submission, file, log and cleanup path resolves through that registry, so adding a third region (APAC is the obvious next one) means provisioning infrastructure and adding a registry entry — not auditing hundreds of call sites again.
If you need residency in a geography we do not serve yet, [tell us which one](https://www.staticforms.dev/contact). That request is now a provisioning ticket rather than a rewrite.
---
## Get started
1. **New account?** Choose your region on the [signup page](https://www.staticforms.dev/register) — United States (default) or European Union (Frankfurt).
2. **Existing account with no submissions yet?** Set it from your [account page](https://www.staticforms.dev/app/account) before your first submission lands.
3. **Need the exact wording for a customer questionnaire?** The [DPA](https://www.staticforms.dev/dpa), the [sub-processor list](https://www.staticforms.dev/dpa/sub-processors) and the [Transfer Impact Assessment](https://www.staticforms.dev/dpa/transfer-impact-assessment) all name the regions, the sub-processors and the transfer mechanisms line by line.
4. **Just getting started with forms?** Begin with [Getting started with Static Forms](/blog/getting-started-with-static-forms), then add [privacy-first spam protection with Altcha](/blog/implementing-altcha-captcha-html-website) — no third-party tracking, which pairs well with an EU-resident inbox.
That's it. Same endpoint, same API key, same dashboard — your form data just lives where you need it to.
---
## Web3Forms vs Static Forms: Which Is Better in 2026?
Source: https://www.staticforms.dev/blog/web3forms-vs-static-forms-comparison
Date: 2026-07-05
Author: Static Forms Team
Web3Forms and Static Forms solve the same problem: they let you add a working contact form to a static site without running a backend, a database, or any server code. You drop in an HTML form, add a key, and submissions arrive in your inbox. Both are simple to set up, so the real decision comes down to price, spam protection, and which features you get.
This is a practical, side-by-side look at both. We make Static Forms, so treat this as our take, but every number below is real and we'll point out the few places Web3Forms does well.
> **Bottom line up front:** Both services are easy to set up, so simplicity isn't the deciding factor. Static Forms gives you double the free submissions (500 vs 250), half the Pro price for 2.5x the volume ($9 for 25,000 vs $18 for 10,000), and features Web3Forms doesn't offer at all, like AI auto-replies, a privacy-first CAPTCHA, and a custom sending domain. For almost any form that sees real traffic, Static Forms is the better value and the more capable tool.
## Quick Comparison Overview
| Feature | Web3Forms | Static Forms |
|---------|-----------|--------------|
| **Free Tier** | 250 submissions/month | 500 submissions/month |
| **Entry Paid Plan** | $18/mo (Pro: 10,000) | $9/mo (Pro: 25,000) |
| **Setup Difficulty** | Very easy | Very easy |
| **Free Forms** | Unlimited | Unlimited |
| **AJAX/JSON Support** | ✅ Yes | ✅ Yes |
| **Free Spam Protection** | Honeypot + hCaptcha + server-side check | Honeypot + reCAPTCHA v2 + CleanTalk filtering |
| **File Uploads** | ✅ Yes (Pro) | ✅ Yes (Pro) |
| **Webhooks** | ✅ Yes (Pro) | ✅ Yes (Pro) |
| **Integrations** | ~4 apps | 11+ apps |
| **AI-Powered Auto-Reply** | ❌ No | ✅ Yes (Pro) |
| **Privacy-First CAPTCHA (Altcha)** | ❌ No | ✅ Yes (Pro) |
| **Custom Sending Domain** | ❌ No | ✅ Yes (Pro) |
| **Submission History** | 30 days free / 1 year paid | 30 days free / up to 5 years paid |
## What Web3Forms Does Well
To be fair, Web3Forms is a capable free tool, and it's worth naming what it gets right:
- **Automatic server-side spam filtering** on the free plan with no configuration.
- **A large, active community** with polished, framework-specific examples.
- **Unlimited forms even on free**, so you can reuse one key across a whole site.
Those are real strengths. The catch is that Static Forms matches Web3Forms on simplicity and free-tier form limits, then pulls ahead on almost everything else: price, volume, spam options, integrations, and features. Here's where that plays out.
## Pricing Breakdown
### Web3Forms Pricing
**Free, $0:**
- 250 submissions per month
- Unlimited access keys (forms)
- 30-day submission history
- Honeypot spam field + server-side spam check
- hCaptcha support
**Starter, $5/month (billed yearly):**
- 5,000 submissions per month
- Aimed at students, startups, and non-profits
**Pro, $18/month:**
- 10,000 submissions per month
- 1-year submission history
- File attachments and advanced file uploader
- Webhooks, autoresponder, CC emails, domain restriction
- Advanced spam protection (reCAPTCHA v3, Cloudflare Turnstile)
**Agency, higher tier:**
- 20,000 submissions per month, team-oriented
### Static Forms Pricing
**Free, $0:**
- 500 submissions per month
- Unlimited forms
- 30-day submission history
- Honeypot + Google reCAPTCHA v2
- CleanTalk server-side spam filtering (automatic)
**Pro, $9/month ($90/year):**
- 25,000 submissions per month
- File uploads (up to 4.5MB per file)
- Webhooks and 11+ app integrations
- Auto-responder emails + AI-powered auto-reply
- reCAPTCHA v3, Cloudflare Turnstile, hCaptcha, and privacy-first Altcha
- Custom sending domain + sender display name
- Remove Static Forms branding
- Submission history up to 5 years
**Agency, $19/month ($192/year):**
- 30,000 submissions per month
- Team members and child/agency account management
- Larger file storage quota and higher rate/AI limits
### What It Costs You
Here's the practical cost at different volumes:
| Monthly Submissions | Web3Forms | Static Forms |
|---------------------|-----------|--------------|
| Up to 250 | Free | Free |
| 251 to 500 | $5/mo (Starter, yearly) | Free |
| 501 to 10,000 | $18/mo (Pro) | $9/mo (Pro) |
| 10,001 to 25,000 | Agency tier | $9/mo (Pro) |
| 25,001 to 30,000 | Agency tier | $19/mo (Agency) |
**The headline:** Static Forms gives you **twice the free submissions** (500 vs 250) and, on the entry paid plan, **2.5x the volume for half the price**, which is $9 for 25,000 vs $18 for 10,000. On cost per submission, it isn't close.
## Setup and Implementation
Both use the same "key in the HTML" model, so setup is quick and beginner-friendly either way.
### Web3Forms
You request a public **access key** by entering your email, then paste it into your form:
```html
```
The trade-off: file uploads, webhooks, and autoresponders all require the $18/mo Pro plan.
### Static Forms
You create a free account in about two minutes, then use your `apiKey` with a single submit endpoint:
```html
```
AJAX/JSON works on every plan:
```javascript
fetch('https://api.staticforms.dev/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, message, apiKey: 'YOUR_API_KEY' })
})
```
Both are about as simple as form backends get. The difference is what you unlock: with Static Forms, that quick signup gives you a full dashboard and twice the free quota from day one, plus every advanced feature waiting on a $9 plan instead of an $18 one.
## Spam Protection
Both services run automatic server-side spam checks, which is one of the best reasons to use a form backend at all. The difference is what you can configure for free.
**Web3Forms (Free):** honeypot field, a built-in server-side check, and hCaptcha. reCAPTCHA v3 and Cloudflare Turnstile are Pro-only.
**Static Forms (Free):** honeypot, Google reCAPTCHA v2, CleanTalk server-side filtering, and rate limiting. On Pro you also get reCAPTCHA v3, Turnstile, hCaptcha, and Altcha.
```html
```
Static Forms gives you more spam-fighting options on the free plan, and one feature Web3Forms can't match at any price: [Altcha CAPTCHA](/blog/implementing-altcha-captcha-html-website), a privacy-first challenge that stops bots without cookies or third-party tracking. If GDPR or user privacy matters, that's a clear win for Static Forms.
## Feature Comparison
### Email Notifications
Both send submissions to your inbox and offer autoresponders and CC emails on paid plans. Static Forms adds multiple recipients with custom reply-to, plus a **custom sending domain and sender display name** on Pro, so emails come from your own address instead of a shared one. That improves deliverability and looks more professional to the people filling out your form.
### File Uploads
Both gate uploads behind their Pro plan. Web3Forms includes an advanced file uploader; Static Forms supports files up to 4.5MB each (PDF, DOC, DOCX, images, and more):
```html
```
Since both require Pro, the deciding factor is price and volume, where Static Forms is half the cost with 2.5x the submissions.
### Webhooks & Integrations
Web3Forms covers the basics on Pro with webhooks and around four integrations (Google Sheets, Notion, Slack). Static Forms includes 11+ on Pro:
- Slack, Discord, Telegram
- Google Sheets, Notion, Airtable, Mailchimp
- Zapier, Make, n8n
- Custom webhooks
If you want submissions flowing into your existing tools without wiring everything up yourself, Static Forms covers far more ground.
### AI-Powered Auto-Reply
This is the clearest gap between the two: Static Forms has **AI Reply**, and Web3Forms has nothing like it.
AI Reply generates a contextual, personalized response to each submission using Anthropic Claude models. You upload your own knowledge base so replies reflect your product and docs, and it runs on pay-as-you-go credits on the Pro plan. It's ideal for support triage, FAQ automation, and lead nurturing, turning "we'll get back to you" into an instant, relevant answer.
Web3Forms offers standard autoresponder templates on Pro, but no AI-generated responses. Learn more about [AI Reply](/docs/ai-reply).
### Custom Redirects
Both redirect the user after submission, just with a different field name:
```html
```
### Form Building & Submission History
Both do client- and server-side validation. Static Forms adds field-level custom error messages and a **visual form builder**, and it retains submissions far longer on paid plans, up to 5 years (configurable) versus Web3Forms' 1 year. Both keep 30 days on their free tiers.
## Framework Support
Neither locks you into a framework. Both work anywhere you can submit an HTML form or make a `fetch` request. Static Forms uses the standard fetch API, so the same code works in React, Vue, Angular, Svelte, or plain JS:
```jsx
const handleSubmit = async (e) => {
e.preventDefault();
await fetch('https://api.staticforms.dev/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...formData, apiKey })
});
};
```
Detailed guides:
- [HTML form to email without a server](/blog/html-form-send-email-without-server)
- [React contact form tutorial](/blog/react-contact-form-email-tutorial)
- [Next.js integration](/blog/using-staticforms-with-nextjs)
## Privacy & GDPR
Web3Forms' CAPTCHA options all rely on third-party services (hCaptcha, reCAPTCHA, Turnstile), with no cookie-free option. Static Forms is GDPR-focused: it offers [Altcha](/blog/implementing-altcha-captcha-html-website) (no tracking, no cookies), encrypts data at rest and in transit, and lets you send from your own domain. For privacy-sensitive projects, Static Forms is the safer choice.
## Migrating From Web3Forms to Static Forms
If you're already on Web3Forms, switching takes minutes and your HTML structure stays the same:
**1. Update the form action:**
```html
```
The `honeypot` field catches the bulk of spam bots without a CAPTCHA, `replyTo="@"` makes the submitter's address your reply-to, and there's a free tier to start. You can also forward submissions to a [Google Sheet](https://www.staticforms.dev/integrations/google-sheets), [Slack](https://www.staticforms.dev/integrations/slack), or any endpoint via [webhooks](https://www.staticforms.dev/integrations/webhooks). Other hosted backends in this category include Formspree, Getform, Basin, and Web3Forms — the model is similar across them, so the differentiators are spam handling, deliverability, integrations, and pricing.
**Best for:** almost any Hugo or Jekyll site. It's the lowest-effort option that still does the job properly.
## Option 2: Forms bundled with your host
Some static hosts bundle a form-handling feature — Netlify Forms is the best-known example, and Cloudflare and others have their own takes. You add an attribute or special markup, and the host intercepts submissions during the build or at the edge.
**The upside:** it's convenient if you're already on that host and never plan to leave. **The catch:** your form is now coupled to the platform. Move your Hugo or Jekyll site to a different host and the form stops working until you rewire it. There are also usually tight limits on submissions before you hit a paid tier.
**Best for:** sites firmly committed to one host, with light form volume.
## Option 3: Your own serverless function
If you want total control, you can write a small serverless function — on Vercel, Cloudflare Workers, AWS Lambda, or similar — that receives the POST, sends an email through a provider, and screens for spam. Your Hugo or Jekyll form posts to that function's URL.
**The upside:** you own the whole pipeline and can do anything. **The cost:** you now own *everything* — email deliverability (and the SPF/DKIM headaches that come with it), spam filtering, error handling, and ongoing maintenance. For a contact form, that's a lot of surface area to babysit.
**Best for:** teams with specific custom logic and the appetite to maintain it.
## Option 4: A plain mailto: link (not really a form)
The zero-backend "solution" is a `mailto:` link or a form with `action="mailto:..."`. It opens the visitor's email client instead of submitting anywhere.
In practice it's a poor experience: it depends on the visitor having a configured desktop mail client, exposes your address to scrapers, and captures nothing structured. It's fine as a fallback link, not as your contact form.
**Best for:** a quick "email me" link — not a real contact form.
## Feature comparison at a glance
Here's how the four approaches stack up on the things that actually matter for a Hugo or Jekyll site:
| | Hosted backend | Host-bundled | Own serverless | mailto: |
|---|---|---|---|---|
| **Server code to maintain** | None | None | Yes — you own it | None |
| **Works on any host** | ✅ | ❌ tied to host | ✅ | ✅ |
| **Built-in spam filtering** | ✅ | Partial | You build it | ❌ |
| **Email delivery handled** | ✅ | ✅ | You configure SPF/DKIM | N/A |
| **Route to Sheets / Slack / webhooks** | ✅ | Limited | You build it | ❌ |
| **Captures structured data** | ✅ | ✅ | ✅ | ❌ |
| **Setup time** | Minutes | Minutes | Hours+ | Minutes |
| **Portable (just HTML)** | ✅ | ❌ | Mostly | ✅ |
The pattern is clear: a hosted backend gives you the least to maintain while still ticking every box that matters. The host-bundled option trades portability for convenience, and rolling your own trades time and maintenance for control.
## What about spam?
Spam is the number-one reason a naive contact form becomes unusable, so it deserves its own look. A static site can't run server-side filtering on its own, which leaves three layers worth understanding:
- **Honeypot fields.** A hidden input that humans never fill but bots do. It's invisible to visitors, requires no extra clicks, and catches the bulk of automated spam. Every example in our Hugo and Jekyll tutorials includes one.
- **Server-side screening.** A good hosted backend scores submissions and drops obvious junk before it ever reaches your inbox — something a `mailto:` link or a bare serverless function won't do unless you build it.
- **CAPTCHA, only when needed.** If sophisticated spam slips through, you can add a challenge like reCAPTCHA, Cloudflare Turnstile, or Altcha. The trick is to treat it as a fallback rather than a default, since every CAPTCHA adds friction for real visitors.
The right order is honeypot first, server-side screening always, and a CAPTCHA only if the first two aren't enough.
## How to choose
For the large majority of Hugo and Jekyll sites, a **hosted form backend (Option 1)** is the right answer: it matches the static-site model exactly, requires no infrastructure, and keeps your form portable across hosts. Reach for a host-bundled option only if you're locked to one platform anyway, or a custom serverless function only when you genuinely need custom logic and will maintain it.
If you want to see the hosted approach end to end, we have step-by-step tutorials for [Jekyll on GitHub Pages](https://www.staticforms.dev/blog/jekyll-contact-form-github-pages) and [Hugo](https://www.staticforms.dev/blog/hugo-contact-form-tutorial), plus a general primer on [sending form data without a server](https://www.staticforms.dev/blog/html-form-send-email-without-server).
## Switching is painless
One underrated advantage of the hosted approach: there's almost nothing to migrate. Your form is just an HTML `
```
The key fields:
- **`apiKey`** pulls your key from site config and routes the submission to your account.
- **`replyTo`** set to `@` uses the visitor's email as the reply-to address, so you reply right from your inbox.
- **`honeypot`** silently drops bot submissions — bots fill every field, humans never see this one.
## Step 3: Add a contact page
Create `content/contact.md`. Hugo doesn't render raw HTML inside Markdown by default, so the cleanest approach is to call the partial from a template. If your theme has a single-page layout you can extend, add the partial there; the most portable route is a dedicated layout. Create `layouts/page/contact.html`:
```go-html-template
{{ define "main" }}
{{ .Title }}
{{ .Content }}
{{ partial "contact-form.html" . }}
{{ end }}
```
Then set the layout in the page's front matter:
```yaml
---
title: "Contact"
layout: "contact"
---
Have a question? Drop me a line below.
```
## Step 4: Use it anywhere with a shortcode
To let yourself (or other authors) embed the form inside any Markdown post, wrap the partial in a shortcode. Create `layouts/shortcodes/contact.html`:
```go-html-template
{{ partial "contact-form.html" . }}
```
Now any content file can include the form inline:
```markdown
Want to work together?
{{* contact */>}}
```
## Step 5: Theme-specific notes
The partial works with any theme, but a couple of popular ones have a natural home for it:
- **PaperMod** — PaperMod ships with a `single.html` layout. The simplest path is the dedicated `layouts/page/contact.html` above, which overrides the theme for just that page. Don't edit files inside `themes/` directly; put your overrides in your project's root `layouts/` so theme updates don't clobber them.
- **Ananke** — Ananke renders page content through `.Content`. The dedicated layout approach works the same way. If you'd rather keep everything in Markdown, enable raw HTML by adding `[markup.goldmark.renderer]` with `unsafe = true` to your config — but the partial-and-shortcode approach keeps your content clean and avoids that flag.
## Step 6: Build, deploy, and test
Run your normal build and deploy — Hugo sites work the same on Netlify, Cloudflare Pages, GitHub Pages, or any static host:
```bash
hugo --minify
```
Open your contact page on the deployed site and send a test message. The first submission triggers a one-time verification email from Static Forms — click the link, and every submission afterward flows straight to your inbox.
By default the API returns a plain success response. To send visitors to a thank-you page instead, add a redirect field to the partial:
```go-html-template
```
The full field reference lives in the [Static Forms documentation](https://www.staticforms.dev/docs/html).
## Step 7: Style the form to match your theme
The partial inherits your theme's styles, but a contact form usually deserves a bit of intention. Add a stylesheet at `assets/css/contact-form.css` and pull it into your form layout, or drop the rules into your theme's main stylesheet:
```css
.contact-form {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 32rem;
}
.contact-form input,
.contact-form textarea {
width: 100%;
padding: 0.65rem 0.75rem;
font: inherit;
border: 1px solid #cbd5e1;
border-radius: 6px;
}
.contact-form input:focus,
.contact-form textarea:focus {
outline: 2px solid #7c3aed;
outline-offset: 1px;
border-color: transparent;
}
.contact-form button {
align-self: flex-start;
padding: 0.65rem 1.25rem;
font: inherit;
font-weight: 600;
color: #fff;
background: #7c3aed;
border: none;
border-radius: 6px;
cursor: pointer;
}
```
If you're on a recent Hugo with the asset pipeline, fingerprint and link it from your form layout:
```go-html-template
{{ with resources.Get "css/contact-form.css" | minify | fingerprint }}
{{ end }}
```
## Submit without leaving the page (AJAX)
The plain form works with zero JavaScript. If you'd rather keep visitors on the page, submit with `fetch()`. Create a small script — for example `assets/js/contact-form.js` — and include it from your form layout:
```javascript
const form = document.querySelector('.contact-form');
form?.addEventListener('submit', async (event) => {
event.preventDefault();
const data = Object.fromEntries(new FormData(form).entries());
try {
const response = await fetch('https://api.staticforms.dev/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...data, replyTo: '@' }),
});
const result = await response.json();
if (result.success) {
form.outerHTML = '
'
);
}
});
```
Because the `apiKey` is already a hidden field in the partial, `new FormData(form)` picks it up automatically — no need to repeat it in the script. The honeypot rides along in the payload too, so spam protection still applies.
## Multilingual contact forms
Hugo's first-class multilingual support is one of its biggest draws, and your contact form should follow suit. Move the user-facing strings into your i18n translation files (`i18n/en.toml`, `i18n/fr.toml`, and so on):
```toml
# i18n/en.toml
[contactName]
other = "Name"
[contactMessage]
other = "Message"
[contactSend]
other = "Send message"
```
Then reference them in the partial with the `i18n` function:
```go-html-template
```
The same single endpoint handles every language — only the labels change. You can even set a per-language email subject so you know which site version a message came from.
## Going further
- **Spreadsheets and tools.** Forward submissions to a sheet with the [Google Sheets integration](https://www.staticforms.dev/integrations/google-sheets), or send them anywhere via [webhooks](https://www.staticforms.dev/integrations/webhooks).
- **AJAX submission.** Prefer an inline success message over a page navigation? Submit with `fetch()` — the pattern is the same one we use in the [JAMstack contact form guide](https://www.staticforms.dev/blog/jamstack-contact-form).
- **Other generators.** Using more than one static site generator? We have matching tutorials for [Astro](https://www.staticforms.dev/blog/astro-contact-form-tutorial) and other frameworks.
## Troubleshooting
The most common snags on a Hugo setup:
- **`partial "contact-form.html" not found`.** The partial must live at `layouts/partials/contact-form.html` in your project root (not inside `themes/`). Hugo resolves project-level layouts first, which is also how you safely override a theme.
- **Raw HTML in Markdown is being stripped.** Hugo's Goldmark renderer drops raw HTML by default for safety. That's why this guide uses a partial and a shortcode instead of pasting `
```
That top block between the `---` lines is Jekyll front matter — it tells Jekyll to wrap the page in your `default` layout and serve it at `/contact/`. Replace `YOUR_API_KEY_HERE` with your real key.
A few fields are doing important work:
- **`apiKey`** routes the submission to your account.
- **`replyTo`** set to `@` tells Static Forms to use the submitter's own email as the reply-to address, so you can reply straight from your inbox.
- **`honeypot`** is a hidden field. Real visitors never see it, but spam bots fill in every field they find — so any submission with a value here is silently rejected.
## Step 2: Make it a reusable include
If you want the same form in your footer, your about page, and a dedicated contact page, copy-pasting the markup gets old fast. Jekyll includes solve this. Create `_includes/contact-form.html`:
```html
```
Store the key once in `_config.yml` so it isn't scattered across files:
```yaml
staticforms_key: YOUR_API_KEY_HERE
```
Now drop the form anywhere with a single Liquid tag, optionally overriding the email subject per page:
```liquid
{% include contact-form.html subject="Contact form — pricing page" %}
```
Because the API key is published in your site's HTML either way (that's true of any client-side form), treat it as a public form key rather than a secret. It only authorizes submissions to your account's endpoint.
## Step 3: Commit, push, and test
GitHub Pages rebuilds automatically on every push to your publishing branch:
```bash
git add contact.html _includes/contact-form.html _config.yml
git commit -m "Add Static Forms contact form"
git push origin main
```
Give the build a minute, then open `/contact/` on your live site and send yourself a test message. The first time you submit, Static Forms emails you a confirmation link to verify the form — click it once and every submission after that flows straight to your inbox.
## Step 4: Send visitors to a thank-you page
By default the API returns a plain success response. For a Jekyll site you almost always want to send visitors to a friendly confirmation page instead. Add a `redirectTo` field pointing at a thank-you page you build in Jekyll:
```html
```
Then create `thank-you.html` with `permalink: /thank-you/` and whatever message you like. The full list of supported fields is in the [Static Forms documentation](https://www.staticforms.dev/docs/html).
## Step 5: Style the form to match your theme
The default Jekyll theme (Minima) and most others give you sensible base styles, but a contact form usually wants a little polish. Add this to your `assets/css/style.scss` (or a `