Skip to content
FFormhook

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: "user@example.com",
    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>
  );
}

WordPress

Formhook is a form backend, so any WordPress form can send to it. Pick whichever matches your setup:

Native HTML form

Drop a plain form into a page or block and point its action at your endpoint. Add your site’s origin to the form’s Allowed origins so browser submissions are accepted.

<form action="https://formhook.app/f/YOUR_API_KEY" method="POST">
  <input name="email" type="email" required>
  <textarea name="message" required></textarea>
  <input type="hidden" name="_redirect" value="https://your-site.com/thanks">
  <button type="submit">Send</button>
</form>

Contact Form 7

Forward each Contact Form 7 submission from your theme’s functions.php. Because the request comes from your server (no browser Origin), authenticate it with an X-Auth-Token:

// functions.php — forward Contact Form 7 submissions to Formhook
add_action('wpcf7_before_send_mail', function ($contact_form) {
  $submission = WPCF7_Submission::get_instance();
  if (!$submission) return;
  $data = $submission->get_posted_data();

  wp_remote_post('https://formhook.app/f/YOUR_API_KEY', [
    'headers' => [
      'Content-Type' => 'application/json',
      'X-Auth-Token' => 'YOUR_AUTH_TOKEN',
    ],
    'body'    => wp_json_encode($data),
    'timeout' => 10,
  ]);
});

Gravity Forms

Use the official Webhooks add-on to POST to your endpoint, or forward from functions.php with the same server-side token:

// functions.php — forward Gravity Forms submissions to Formhook
add_action('gform_after_submission', function ($entry, $form) {
  $payload = [];
  foreach ($form['fields'] as $field) {
    $payload[$field->label] = rgar($entry, (string) $field->id);
  }

  wp_remote_post('https://formhook.app/f/YOUR_API_KEY', [
    'headers' => [
      'Content-Type' => 'application/json',
      'X-Auth-Token' => 'YOUR_AUTH_TOKEN',
    ],
    'body'    => wp_json_encode($payload),
    'timeout' => 10,
  ]);
}, 10, 2);

Generate the token under your form’s Settings → Server request authentication. See Origins & authentication for the difference between browser and server requests.

cURL (testing)

Server-to-server requests (curl, backend code) send no Origin header, so they must authenticate with an X-Auth-Token header. Generate a token under your form's Settings → Server request authentication and pass it on every request — without it, server requests are rejected.

curl -X POST https://formhook.app/f/YOUR_API_KEY \
  -H "Content-Type: application/json" \
  -H "X-Auth-Token: YOUR_AUTH_TOKEN" \
  -d '{"email":"test@example.com","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, such as curl or a backend) must send a valid X-Auth-Token header generated in the form's settings; without a token they are rejected.

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>

File uploads (Pro and Studio)

Forms can accept file attachments. Submit with enctype="multipart/form-data"; each <input type="file"> field is uploaded to object storage and linked from the submission in your dashboard. Files are downloaded via short-TTL signed URLs from the dashboard; nothing is publicly addressable.

<!-- enctype is the important bit -->
<form action="https://formhook.app/f/YOUR_API_KEY" method="POST" enctype="multipart/form-data">
  <input name="email" type="email" required>
  <textarea name="message" required></textarea>

  <!-- one or more file inputs; same field name = multiple files -->
  <input name="attachment" type="file">

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

Per-file cap: 10 MB. Per-account storage: 1 GB on Pro, 10 GB on Studio. Soft-fail responses: {ok: true, warnings: ["file_too_large"]} when a single file exceeds the per-file cap, ["storage_full"] when the account is at its allowance, ["files_not_configured"] if the operator hasn't provisioned object storage. In every case the text submission still saves.

Studio: provision and transfer client forms

Studio accounts can build a form for a client and hand it over. The studio configures and tests while owning the form; once transferred, the client owns the form and its submissions and the studio sees metadata only.

  1. On New form, fill the optional Client email field. The form is marked as a client form (sponsorship is recorded) but you still own it.
  2. Configure and test the form normally — submissions land in your dashboard while you're testing.
  3. When you're ready, open the form's Settings → Client transfer section and send the invite. The form stays live and continues to accept submissions while you wait.
  4. Your client receives an emailed link, signs up (the email is auto-verified by token possession), and clicks Claim. From that point on, submissions flow to their dashboard.

After the claim, you see the form in your Clients console with status + monthly submission counts. You no longer see submission content or attachments — that's the GDPR boundary that keeps you out of your clients' data chain. Submissions still count against your Studio cap (the billing rolls up); the client themselves can stay on Free without breaking anything.

Import submissions

Bulk-import submissions into a form from a JSON file — open the form in your dashboard, click Import, and upload a file shaped like this:

{
  "schemaVersion": 1,
  "submissions": [
    {
      "payload": { "email": "ada@example.com", "message": "Hello" },
      "createdAt": "2025-01-02T15:04:05Z"
    }
  ]
}

The top level may be an array of entries or an object with a submissions array. Each entry needs a payload object; createdAt (ISO-8601) is optional and preserved when present. Imported submissions are marked read, never trigger notifications or webhooks, and are capped at 5,000 per file (max 5 MB).

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: 250 submissions over a rolling 30-day window. Over-quota still returns 200 with warnings: ["over_quota"] in the body — submissions are never silently dropped.

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+).