Documentation

Quickstart

Five minutes from zero to a spam-protected form. No backend required.

1. Create a form

The easiest way is the dashboard — sign in and click New Form. To create one programmatically, send your Supabase Auth access token as a Bearer token (the owner is taken from the token, not the request body). You'll receive a public access_key (safe to ship in client code) and a private secret_key (for the management API — keep it server-side).

terminal
curl -X POST https://api.fieldpost.dev/v1/manage/forms \
  -H "Authorization: Bearer YOUR_SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Contact"}'

2. Add your form

Plain HTML

Point the form at the submit endpoint. The optional embed script (<1KB gz) upgrades it to AJAX, injects the honeypot, and adds inline success/error states. Without it, the form still works via POST + redirect.

index.html
<form action="https://api.fieldpost.dev/v1/submit" method="POST" data-fieldpost>
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY">
  <input type="text" name="name" required>
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>
<script src="https://cdn.fieldpost.dev/v1/fieldpost.min.js" defer></script>

React / Next.js

terminal
npm install @fieldpost/react
ContactForm.tsx
"use client";
import { useFieldpost } from "@fieldpost/react";

export default function ContactForm() {
  const { submit, status, error } = useFieldpost({
    accessKey: process.env.NEXT_PUBLIC_FIELDPOST_KEY!,
  });

  return (
    <form onSubmit={submit}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button disabled={status === "submitting"}>
        {status === "submitting" ? "Sending…" : "Send"}
      </button>
      {status === "success" && <p>Thanks — we&apos;ll be in touch!</p>}
      {status === "error" && <p>{error}</p>}
    </form>
  );
}

3. Configure security

Every rule runs in block, flag (accept but mark as spam — a dry run), or off. Update per form via the management API:

terminal
curl -X PATCH https://api.fieldpost.dev/v1/manage/forms/me \
  -H "Authorization: Bearer YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "security": {
      "email_validation": { "mode": "block", "check_mx": true,
        "block_disposable": true, "block_role_addresses": false },
      "rate_limit": { "mode": "block", "window_seconds": 60, "max": 5 },
      "bot_detection": { "mode": "block", "min_fill_seconds": 2 },
      "shield": { "mode": "block" },
      "captcha": { "provider": "turnstile", "secret": "YOUR_TURNSTILE_SECRET" },
      "redact": { "entities": ["credit_card"] },
      "allowed_domains": ["yourdomain.com"]
    }
  }'

Special fields

  • access_key — required; identifies your form
  • redirect — URL for the non-JS thank-you redirect
  • botcheck — honeypot; leave empty (the embed injects it)
  • email — triggers validation + reply-to + autoresponder
  • cf-turnstile-response / g-recaptcha-response / h-captcha-response — CAPTCHA tokens

Reading submissions

terminal
curl "https://api.fieldpost.dev/v1/manage/submissions?spam=false&limit=50" \
  -H "Authorization: Bearer YOUR_SECRET_KEY"

Webhooks

Add a webhook integration and every accepted submission POSTs to your URL with X-Fieldpost-Timestamp and X-Fieldpost-Signature headers. The signature is HMAC-SHA256(secret, `${timestamp}.${raw_body}`). Verify both the signature and that the timestamp is recent (within ~5 minutes) to reject replays. Webhook URLs must be public HTTPS endpoints — private and loopback addresses are rejected.

webhook payload
{
  "event": "submission.created",
  "form": { "id": "…", "name": "Contact" },
  "submission": {
    "id": "…",
    "data": { "name": "Ada", "email": "ada@example.com", "message": "…" },
    "meta": { "country": "US", "referer": "https://…" },
    "created_at": "2026-07-03T12:00:00Z"
  }
}