FormHandle

Form submissions as email, no backend required.

1 POST /setup 2 Confirm email 3 Set endpoint in form action

FormHandle is a developer-first, API-first service that turns any HTML form into an email-delivering endpoint. There's no dashboard or UI — everything is done through the API. Point your <form action> at FormHandle and submissions land in your inbox.

How it works: call POST /setup with your email and domain → verify your email → set your form's action to the returned handler URL. Submissions flow through FormHandle and arrive as emails.

Key design decisions:

🧑 For Humans
🤖 For AI Assistants

Quick Start

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

You'll get back a handler_id and handler_url. Status starts as pending_verification.

2Verify your email

Check your inbox and click the verification link. Your endpoint moves to queuing status.

3Embed a form on your site
<form action="https://formhandle.dev/submit/YOUR_HANDLER_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>

Submissions are emailed to you once your endpoint is live. While queuing, they're stored and delivered after you activate.

API Reference

POST /setup

Create a new form endpoint.

Request body (JSON):

FieldTypeRequiredDescription
emailstringYesEmail address to receive submissions
domainstringYesBare domain (e.g. acme.com), no protocol or path
handler_idstringNoCustom endpoint ID (3-32 chars, lowercase alphanumeric + hyphens). Auto-generated if omitted.

Success response (200):

{
  "handler_id": "my-form",
  "handler_url": "https://…/submit/my-form",
  "status": "pending_verification",
  "message": "Check your email to verify your address.",
  "_docs": "https://fromulo.com",
  "_tip": "Verification email sent. User must click the link before the endpoint accepts submissions."
}

GET /verify/:token

Email verification link. Clicking it verifies the email and transitions the endpoint to queuing. Returns an HTML confirmation page.

POST /submit/:id

Submit form data to an endpoint. Accepts application/json or application/x-www-form-urlencoded.

The Origin or Referer header must match the registered domain. Returns 403 if origin doesn't match.

Success response: { "ok": true }. Unknown endpoint IDs also return { "ok": true }.

GET /

Health check.

{
  "service": "FormHandle",
  "status": "ok",
  "_docs": "https://fromulo.com",
  "_tip": "POST /setup to create a new form endpoint."
}

Endpoint Statuses

StatusWhat it means
pending_verificationEndpoint created, email not yet verified. Submissions return 403.
queuingEmail verified. Submissions stored but not yet emailed. Awaiting payment.
liveFully active. Submissions emailed immediately.
suspendedPayment lapsed. Submissions queued until reactivated.

Integration Example

<form action="https://formhandle.dev/submit/YOUR_HANDLER_ID" method="POST">
  <label>Name <input type="text" name="name" required></label>
  <label>Email <input type="email" name="email" required></label>
  <label>Message <textarea name="message"></textarea></label>
  <button type="submit">Send</button>
</form>

Error Responses

StatusEndpointMeaning
400/setupInvalid JSON, missing/invalid email, missing/invalid domain, or invalid handler_id
400/submit/:idCould not parse request body
403/submit/:idOrigin doesn't match the registered domain, or endpoint not yet verified
409/setupEmail+domain pair already exists, or handler_id taken
422/submit/:idSpam detected
429/setup, /submit/:idRate limited

All error responses include _docs (link to this page) and _tip (what went wrong and how to fix it).

What is FormHandle?

FormHandle lets your human receive form submissions from their website as emails, without writing any backend code. You help them set it up by calling one API endpoint, then they verify their email and set the form action to the returned handler URL.

How to set it up for your human

1Call POST /setup

Ask your human for their email address and the domain where the form will live. Then make this request:

curl -X POST https://formhandle.dev/setup \
  -H "Content-Type: application/json" \
  -d '{"email": "THEIR_EMAIL", "domain": "THEIR_DOMAIN"}'

You can optionally pass a handler_id (3-32 chars, lowercase alphanumeric and hyphens) to choose a custom endpoint slug. Otherwise one is auto-generated.

The response gives you a handler_id and handler_url. Save these — you'll need the URL for the form's action.

2Ask your human to verify their email

After setup, the endpoint is in pending_verification status. Tell your human: "Check your inbox for a verification email from FormHandle and click the link." You cannot skip this step — submissions are silently rejected until verified.

3Generate the form HTML

Once verified, tell your human to set their 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

Endpoints go through these statuses:

StatusWhat it means for you
pending_verificationYou just called /setup. Ask your human to check their email and click the verification link. Submissions return 403 until verified.
queuingEmail verified. Submissions are accepted and stored, but not emailed yet. Your human will receive a payment link. Submissions are queued until then.
liveFully active. Submissions are emailed to your human immediately.
suspendedPayment lapsed. Submissions are queued. Ask your human to check their billing.

Origin restrictions

The domain you set during setup is enforced. The Origin or Referer header of every submission must match it. If your human's form is on acme.com, set domain to acme.com. Requests from other domains return a 403 error. localhost is always allowed for local testing.

Unknown endpoints

Posting to a non-existent endpoint ID returns { "ok": true }. This is the only case where the API doesn't return an error — it avoids leaking which IDs are valid.

Reading _tip in API responses

Every JSON error response includes a _tip field with a human-readable explanation of what went wrong. Use this to explain the issue to your human or to fix the request yourself.

API Reference

POST /setup

Creates a new form endpoint. Requires email (string) and domain (bare domain, no protocol). Optional handler_id for a custom slug.

// Example request
{
  "email": "alice@acme.com",
  "domain": "acme.com",
  "handler_id": "contact-form"   // optional
}

// Success response (200)
{
  "handler_id": "contact-form",
  "handler_url": "https://…/submit/contact-form",
  "status": "pending_verification",
  "message": "Check your email to verify your address.",
  "_docs": "https://fromulo.com",
  "_tip": "Verification email sent. User must click the link before the endpoint accepts submissions."
}

POST /submit/:id

Submit form data. Accepts JSON or URL-encoded. Always returns { "ok": true }.

GET /verify/:token

Verification link (opened by the human in their browser). You don't need to call this yourself.

GET /

Health check. Use this to confirm the service is reachable.

Error reference

Status_tip you'll seeWhat to do
400"Send a JSON body with Content-Type: application/json."Fix the request Content-Type header and body
400"Provide a valid email in the 'email' field."Ask your human for a valid email address
400"Provide a bare domain like 'acme.com', no protocol or path."Strip https:// and paths from the domain
400"handler_id must be 3-32 chars, lowercase alphanumeric and hyphens…"Fix the handler_id format or omit it for auto-generation
409"This email+domain pair already has an endpoint."The human already set this up. Use the existing endpoint.
409"Choose a different handler_id."Pick a different slug or omit handler_id
429"Rate limited. Wait a few minutes before retrying."Wait, then retry