Form submissions as email, no backend required.
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:
localhost is always allowed so you can test locally._tip guidance. Unknown endpoint IDs return { "ok": true }.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.
Check your inbox and click the verification link. Your endpoint moves to queuing status.
<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.
Create a new form endpoint.
Request body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email address to receive submissions |
domain | string | Yes | Bare domain (e.g. acme.com), no protocol or path |
handler_id | string | No | Custom 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."
}Email verification link. Clicking it verifies the email and transitions the endpoint to queuing. Returns an HTML confirmation page.
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 }.
Health check.
{
"service": "FormHandle",
"status": "ok",
"_docs": "https://fromulo.com",
"_tip": "POST /setup to create a new form endpoint."
}| Status | What it means |
|---|---|
pending_verification | Endpoint created, email not yet verified. Submissions return 403. |
queuing | Email verified. Submissions stored but not yet emailed. Awaiting payment. |
live | Fully active. Submissions emailed immediately. |
suspended | Payment lapsed. Submissions queued until reactivated. |
<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>| Status | Endpoint | Meaning |
|---|---|---|
400 | /setup | Invalid JSON, missing/invalid email, missing/invalid domain, or invalid handler_id |
400 | /submit/:id | Could not parse request body |
403 | /submit/:id | Origin doesn't match the registered domain, or endpoint not yet verified |
409 | /setup | Email+domain pair already exists, or handler_id taken |
422 | /submit/:id | Spam detected |
429 | /setup, /submit/:id | Rate limited |
All error responses include _docs (link to this page) and _tip (what went wrong and how to fix it).
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.
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.
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.
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>Endpoints go through these statuses:
| Status | What it means for you |
|---|---|
pending_verification | You just called /setup. Ask your human to check their email and click the verification link. Submissions return 403 until verified. |
queuing | Email verified. Submissions are accepted and stored, but not emailed yet. Your human will receive a payment link. Submissions are queued until then. |
live | Fully active. Submissions are emailed to your human immediately. |
suspended | Payment lapsed. Submissions are queued. Ask your human to check their billing. |
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.
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.
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.
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."
}Submit form data. Accepts JSON or URL-encoded. Always returns { "ok": true }.
Verification link (opened by the human in their browser). You don't need to call this yourself.
Health check. Use this to confirm the service is reachable.
| Status | _tip you'll see | What 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 |