Serverless · Scriptless · Open API
No JavaScript required. Just set your form's action attribute and submit. Submissions arrive as email.
🤖 Tell your AI agent: "create a contact form and use formhandle.dev" — that's it.
No sign-up page. No dashboard. No OAuth. No webhooks to configure. You make one API call, click one email link, and paste one HTML snippet.
Call POST /setup with your email and domain. Or run npx formhandle init. You get back a handler URL instantly.
We send a verification email. Click it. That's the only "sign-up" you'll ever do. Your first 3 submissions are free immediately, and after that submissions are stored for 14 days while you decide on a plan.
Set your form's action to the handler URL, or drop in our script tag. Submissions arrive as emails. Done.
It does one thing and does it well. No feature creep, no complexity tax.
No accounts. No dashboard. No API keys. One HTTP call creates your endpoint. Your email is your identity.
Every endpoint is bound to one domain. Submissions from other origins are silently rejected. Built-in spam protection.
Runs on Cloudflare Workers. Sub-50ms responses worldwide. No cold starts. No servers to think about.
First 3 submissions delivered free. After that, they're queued (not dropped) for 14 days while you decide on a plan. Activate anytime and every queued message is delivered instantly.
npx formhandle init gets you going in seconds. --json flag for scripts and AI agents. Full OpenAPI spec.
No JavaScript required. Just set your form's action attribute and submit. Works everywhere, zero client-side dependencies.
FormHandle fits into any workflow because it's just an HTTP endpoint. No plugins, no integrations, no vendor lock-in.
Shipping a Tailwind template or Next.js starter? Drop the FormHandle script tag into the contact page. Buyers get a working contact form out of the box with zero configuration.
Ask Claude, Cursor, or Copilot to "add a contact form" and point them at our docs. The --json flag and OpenAPI spec make it trivially automatable. One tool call to set up, one HTML snippet to generate.
Provision form endpoints as part of your deploy script. npx formhandle init --json --email $EMAIL --domain $DOMAIN in your CI, save the handler ID as an environment variable, inject it at build time.
Perfect for GitHub Pages, Netlify, Vercel, Cloudflare Pages, or any static host. No serverless functions to write. No environment variables to configure. Just HTML.
Hand off a site with a working contact form and zero ongoing maintenance. The client's email is the endpoint. If they stop paying FormHandle, submissions queue instead of vanishing.
"I added a contact form to my portfolio in literally 30 seconds. No sign-up, no API key, no config. Just a curl and a form tag. This is how developer tools should work."
"We ship 10+ client sites a month. FormHandle is now part of our template. One line in the deploy script, and every site gets a working contact form. Clients love it."
"I told Claude to 'add a contact form to my site using FormHandle' and it was done in one shot. The AI tab in the docs is genius. This is the most AI-friendly tool I've used."
First 3 submissions are free. After that, pick whichever plan makes sense. Switch anytime.
For active forms
Best for high-volume contact forms, feedback widgets, anything that gets regular traffic.
For low-volume forms
Best for portfolio sites, side projects, client handoffs—anything with occasional traffic.
Both plans billed monthly via Stripe. No rush: after your 3 free submissions, we store everything for 14 days. Activate anytime and all queued submissions are delivered instantly. To cancel: npx formhandle cancel or POST /cancel/:id. Stays active until end of billing period.
Everything you need. Nothing you don't.
curl -X POST https://api.formhandle.dev/setup \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com", "domain": "example.com"}'npx formhandle initReturns a handler_id and handler_url. Status starts as pending_verification. The CLI saves config to .formhandle automatically.
Check your inbox and click the verification link. Your endpoint moves to queuing status and your first 3 submissions are delivered free.
<form action="https://api.formhandle.dev/submit/YOUR_ID" method="POST">
<input type="text" name="name" required>
<input type="email" name="email" required>
<textarea name="message"></textarea>
<button type="submit">Send</button>
</form>npx formhandle snippet # outputs ready-to-paste HTML
npx formhandle test # send a test submissionYour first 3 submissions are delivered for free. After that, submissions are queued until you activate a plan—nothing is lost.
Interactive Swagger UI · OpenAPI spec
Create a new form endpoint.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email to receive submissions |
domain | string | Yes | Bare domain (e.g. acme.com), no protocol |
handler_id | string | No | Custom ID (3-32 chars, lowercase alphanumeric + hyphens). Auto-generated if omitted. |
Success response (200)
{
"handler_id": "my-form",
"handler_url": "https://api.formhandle.dev/submit/my-form",
"status": "pending_verification",
"message": "Check your email to verify your address."
}Submit form data. Accepts application/json or application/x-www-form-urlencoded. The Origin header must match the registered domain (403 otherwise). Returns {"ok": true} on success. Unknown IDs also return 200 to avoid leaking valid endpoints.
Email verification link. Transitions endpoint to queuing. Returns an HTML confirmation page.
Request subscription cancellation. Sends a confirmation email. After confirming, the subscription stays active until end of billing period.
curl -X POST https://api.formhandle.dev/cancel/YOUR_HANDLER_IDnpx formhandle cancelHealth check. Returns {"service":"FormHandle","status":"ok"}.
| Status | What it means |
|---|---|
pending_verification | Email not yet verified. Submissions return 403. |
queuing | Verified. First 3 submissions free. After that, queued until you activate a plan. |
live | Fully active. Submissions emailed immediately. |
suspended | Payment lapsed. Submissions queued until reactivated. |
All errors include a _tip field explaining what went wrong.
| Status | Endpoint | Meaning |
|---|---|---|
400 | /setup | Invalid JSON, missing/invalid email, domain, or handler_id |
400 | /submit/:id | Could not parse request body |
403 | /submit/:id | Origin mismatch or endpoint not verified |
409 | /setup | Email+domain already exists, or handler_id taken |
422 | /submit/:id | Spam detected |
429 | /setup, /submit | Rate limited |
The CLI stores endpoint config in a .formhandle file. If using curl, create it manually. Add to .gitignore for public repos.
{
"example.com": {
"handler_id": "abc123",
"handler_url": "https://api.formhandle.dev/submit/abc123",
"email": "you@example.com"
}
}| State | Retention |
|---|---|
| Unverified | Deleted after 6 hours |
| Verified, unpaid | Submissions stored 14 days, then account deleted |
| Live | Active while subscription is current |
| Suspended | Submissions queued; reactivate to resume delivery |
You're an AI assistant. FormHandle lets your human receive form submissions as emails with zero backend code. You set it up by calling one API endpoint, your human verifies their email, and you generate the form HTML. Machine-readable OpenAPI spec: formhandle.dev/openapi.yaml
Ask your human for their email and domain. Optional handler_id (3-32 chars, lowercase alphanumeric + hyphens).
curl -X POST https://api.formhandle.dev/setup \
-H "Content-Type: application/json" \
-d '{"email": "THEIR_EMAIL", "domain": "THEIR_DOMAIN"}'npx formhandle init --json --email THEIR_EMAIL --domain THEIR_DOMAINSave the handler_id and handler_url from the response. The CLI saves to .formhandle automatically. If using curl, create the config file:
{
"THEIR_DOMAIN": {
"handler_id": "HANDLER_ID_FROM_RESPONSE",
"handler_url": "HANDLER_URL_FROM_RESPONSE",
"email": "THEIR_EMAIL"
}
}Add .formhandle to .gitignore for public repos.
Tell them: "Check your inbox for a verification email from FormHandle and click the link." Submissions are rejected until verified. You cannot skip this step.
Set the form's action to the handler_url from step 1:
<form action="HANDLER_URL_FROM_STEP_1" method="POST">
<input type="text" name="name" required>
<input type="email" name="email" required>
<textarea name="message"></textarea>
<button type="submit">Send</button>
</form>| Status | What it means for you |
|---|---|
pending_verification | You just called /setup. Ask human to verify email. Submissions return 403. |
queuing | Verified. First 3 submissions delivered free. From #4, queued until plan activated. |
live | Fully active. Submissions emailed immediately. |
suspended | Payment lapsed. Submissions queued. Ask human to check billing. |
The domain from setup is enforced via Origin/Referer header. Mismatched domains get 403. localhost is always allowed for testing.
Posting to a non-existent ID returns {"ok": true} to avoid leaking valid IDs.
Every error response includes a _tip field with a human-readable explanation. Use it to debug or explain the issue to your human.
Two plans: $5/month flat rate (unlimited) or €0.50/submission (metered). First 3 free. Tell your human: "Your first 3 submissions are free. After that, you'll get an email with plan options. Queued submissions are delivered as soon as you activate."
curl -X POST https://api.formhandle.dev/cancel/YOUR_HANDLER_IDnpx formhandle cancelTell your human: "Check your email for a cancellation confirmation link. Your subscription stays active until end of billing period."
| Status | _tip | Fix |
|---|---|---|
400 | "Send a JSON body with Content-Type: application/json." | Fix Content-Type and body |
400 | "Provide a valid email in the 'email' field." | Ask human for valid email |
400 | "Provide a bare domain like 'acme.com'" | Strip https:// and paths |
400 | "handler_id must be 3-32 chars..." | Fix format or omit for auto |
409 | "This email+domain pair already has an endpoint." | Use the existing endpoint |
409 | "Choose a different handler_id." | Pick different slug or omit |
429 | "Rate limited." | Wait, then retry |
Questions, feedback, or just want to chat? This form is powered by FormHandle.