Serverless · Scriptless · Open API

The simplest form handler
on the internet.

No JavaScript required. Just set your form's action attribute and submit. Submissions arrive as email.

contact.html
<form method="POST"
action="https://formhandle.dev/submit/myco">
<input name="name" required>
<input name="email" type="email" required>
<!-- field names can be anything -->
<input name="favourite_dinosaur">
<button type="submit">Send</button>
</form>
<!-- No JavaScript. That's it. -->

🤖 Tell your AI agent: "create a contact form and use formhandle.dev" — that's it.

Three steps. That's the whole product.

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.

1

Create an endpoint

Call POST /setup with your email and domain. Or run npx formhandle init. You get back a handler URL instantly.

2

Click the email link

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.

3

Paste the form HTML

Set your form's action to the handler URL, or drop in our script tag. Submissions arrive as emails. Done.

Why developers choose FormHandle

It does one thing and does it well. No feature creep, no complexity tax.

Truly zero config

No accounts. No dashboard. No API keys. One HTTP call creates your endpoint. Your email is your identity.

Origin-locked security

Every endpoint is bound to one domain. Submissions from other origins are silently rejected. Built-in spam protection.

Edge-deployed

Runs on Cloudflare Workers. Sub-50ms responses worldwide. No cold starts. No servers to think about.

Free trial, nothing lost

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.

CLI & AI-ready

npx formhandle init gets you going in seconds. --json flag for scripts and AI agents. Full OpenAPI spec.

Scriptless by design

No JavaScript required. Just set your form's action attribute and submit. Works everywhere, zero client-side dependencies.

Built for how you actually ship

FormHandle fits into any workflow because it's just an HTTP endpoint. No plugins, no integrations, no vendor lock-in.

🚀

Quick website templates & boilerplates

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.

🤖

AI agents & vibe coding

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.

⚙️

CI/CD & deploy pipelines

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.

🌐

Static sites & JAMstack

Perfect for GitHub Pages, Netlify, Vercel, Cloudflare Pages, or any static host. No serverless functions to write. No environment variables to configure. Just HTML.

🔧

Freelancers & client sites

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.

Developers love how little there is to learn

"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."
S
Sarah K.
Frontend Developer
"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."
M
Marcus L.
Agency Lead
"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."
J
Jess T.
Indie Hacker

Simple pricing, no surprises

First 3 submissions are free. After that, pick whichever plan makes sense. Switch anytime.

Flat Rate

For active forms

$5 /month
  • Unlimited submissions
  • Real-time email delivery
  • Spam filtering
  • Cancel anytime

Best for high-volume contact forms, feedback widgets, anything that gets regular traffic.

Popular

Pay Per Use

For low-volume forms

€0.50 /submission
  • Only pay when forms are used
  • Real-time email delivery
  • Spam filtering
  • Cancel anytime

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.

Documentation

Everything you need. Nothing you don't.

Quick Start

1

Create an endpoint

curl -X POST https://api.formhandle.dev/setup \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "domain": "example.com"}'
npx formhandle init

Returns a handler_id and handler_url. Status starts as pending_verification. The CLI saves config to .formhandle automatically.

2

Verify your email

Check your inbox and click the verification link. Your endpoint moves to queuing status and your first 3 submissions are delivered free.

3

Embed a form

<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 submission

Your first 3 submissions are delivered for free. After that, submissions are queued until you activate a plan—nothing is lost.

API Reference

Interactive Swagger UI · OpenAPI spec

POST /setup

Create a new form endpoint.

FieldTypeRequiredDescription
emailstringYesEmail to receive submissions
domainstringYesBare domain (e.g. acme.com), no protocol
handler_idstringNoCustom 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."
}

POST /submit/:id

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.

GET /verify/:token

Email verification link. Transitions endpoint to queuing. Returns an HTML confirmation page.

POST /cancel/:id

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_ID
npx formhandle cancel

GET /

Health check. Returns {"service":"FormHandle","status":"ok"}.

Endpoint Statuses

StatusWhat it means
pending_verificationEmail not yet verified. Submissions return 403.
queuingVerified. First 3 submissions free. After that, queued until you activate a plan.
liveFully active. Submissions emailed immediately.
suspendedPayment lapsed. Submissions queued until reactivated.

Error Responses

All errors include a _tip field explaining what went wrong.

StatusEndpointMeaning
400/setupInvalid JSON, missing/invalid email, domain, or handler_id
400/submit/:idCould not parse request body
403/submit/:idOrigin mismatch or endpoint not verified
409/setupEmail+domain already exists, or handler_id taken
422/submit/:idSpam detected
429/setup, /submitRate limited

Config File

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

Data Retention

StateRetention
UnverifiedDeleted after 6 hours
Verified, unpaidSubmissions stored 14 days, then account deleted
LiveActive while subscription is current
SuspendedSubmissions 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

Setup Steps

1

Call POST /setup

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_DOMAIN

Save 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.

2

Ask your human to verify their email

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.

3

Generate the form HTML

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>

Things you need to know

Status lifecycle

StatusWhat it means for you
pending_verificationYou just called /setup. Ask human to verify email. Submissions return 403.
queuingVerified. First 3 submissions delivered free. From #4, queued until plan activated.
liveFully active. Submissions emailed immediately.
suspendedPayment lapsed. Submissions queued. Ask human to check billing.

Origin restrictions

The domain from setup is enforced via Origin/Referer header. Mismatched domains get 403. localhost is always allowed for testing.

Unknown endpoints

Posting to a non-existent ID returns {"ok": true} to avoid leaking valid IDs.

Reading _tip in responses

Every error response includes a _tip field with a human-readable explanation. Use it to debug or explain the issue to your human.

Pricing & retention

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."

  • Unverified accounts deleted after 6 hours
  • Unpaid accounts deleted after 14 days
  • Once paid, all queued submissions delivered immediately

Cancellation

curl -X POST https://api.formhandle.dev/cancel/YOUR_HANDLER_ID
npx formhandle cancel

Tell your human: "Check your email for a cancellation confirmation link. Your subscription stays active until end of billing period."

Error Reference

Status_tipFix
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

Say hello

Questions, feedback, or just want to chat? This form is powered by FormHandle.

This form is powered by FormHandle — no JavaScript, no backend code.