Formhook docs

Paste your API key into a form action, get submissions in your dashboard, get a system push notification for each new entry.

Quickstart

1. Sign up and verify your email.
2. Create a form in your dashboard. Copy the API key (looks like fh_…).
3. Paste it as the action of your HTML form. That's it.

HTML form (no JavaScript)

The simplest integration. Native form submission, optional redirect on success.

<form action="https://formhook.app/f/YOUR_API_KEY" method="POST">
  <input name="email" type="email" required>
  <textarea name="message" required></textarea>

  <!-- honeypot: bots fill this, humans don't see it -->
  <input type="text" name="_gotcha" tabindex="-1" autocomplete="off"
         style="position:absolute;left:-9999px">

  <!-- where to send the user after success -->
  <input type="hidden" name="_redirect" value="https://example.com/thanks">

  <button type="submit">Send</button>
</form>

JavaScript fetch

await fetch("https://formhook.app/f/YOUR_API_KEY", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "[email protected]",
    message: "Hello",
  }),
});

React component

"use client";
import { useState } from "react";

export function ContactForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "ok" | "error">("idle");

  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus("sending");
    const data = Object.fromEntries(new FormData(e.currentTarget));
    const res = await fetch("https://formhook.app/f/YOUR_API_KEY", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    });
    setStatus(res.ok ? "ok" : "error");
  }

  if (status === "ok") return <p>Thanks — we'll be in touch.</p>;

  return (
    <form onSubmit={onSubmit}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button disabled={status === "sending"}>Send</button>
      {status === "error" && <p>Something went wrong. Try again.</p>}
    </form>
  );
}

cURL (testing)

curl -X POST https://formhook.app/f/YOUR_API_KEY \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","message":"hi"}'

Reserved fields

These three field names are stripped from the stored payload:

  • _gotcha — honeypot. Anything non-empty here returns 200 success but silently drops the submission and doesn't consume your quota.
  • _redirect — URL to redirect to on success (form-encoded posts only). Must be on an origin in your form's allowlist; otherwise ignored.
  • cf-turnstile-response — Turnstile token, validated server-side.

CORS

For browser submissions, add the origin (scheme + host, no path) to your form's Allowed origins in settings. An empty list rejects browser-origin requests entirely. Server-to-server requests (no Origin header) always go through, subject to rate limits.

Cloudflare Turnstile (optional)

Enable Turnstile in your form's settings, then embed the widget in your HTML:

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js"
        async defer></script>

<form action="https://formhook.app/f/YOUR_API_KEY" method="POST">
  <input name="email" type="email" required>
  <div class="cf-turnstile" data-sitekey="YOUR_TURNSTILE_SITEKEY"></div>
  <button type="submit">Send</button>
</form>

Rate limits and quotas

  • Per-IP across all forms: 10 requests/minute → 429 with Retry-After.
  • Per-form: 60 requests/minute → 429.
  • Body size cap: 64 KB → 413.
  • Free tier monthly quota: 200 submissions over a rolling 30-day window. Over-quota still returns 200 in MVP with warnings: ["over_quota"] in the body.

Error responses

HTTPcodemeaning
200oksuccess
302redirect to _redirect URL
400invalid_bodymalformed JSON / unsupported content-type
403origin_not_allowedCORS allowlist mismatch
403turnstile_failedTurnstile token missing/invalid
403account_suspendedform owner is suspended
404form_not_foundunknown api_key
413body_too_largebody > 64 KB
429rate_limitedincludes Retry-After header
500internal_errortry again, or report

Push notifications (for you, the form owner)

Open your dashboard and click Enable notifications — your browser will ask for permission, then every new submission triggers a system notification, even when the tab is closed. On iOS, install Formhook to your home screen first (Safari 16.4+).