Docs

The whole integration surface is one URL. This page is everything most people need; the articles go deeper.

Quick start

  1. Create an account — you'll be asked where submissions should go and which domain your site lives on.
  2. Your form gets an endpoint like normforms.com/f/x7Kp2mQ9RtLw.
  3. Point any HTML form at it:
<form action="https://normforms.com/f/YOUR_TOKEN" method="POST">
  <input type="text" name="name" placeholder="Your name">
  <input type="email" name="email" placeholder="Your email">
  <textarea name="message" placeholder="Your message"></textarea>
  <input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off">
  <button type="submit">Send</button>
</form>

That's it — submissions arrive by email. No JavaScript, no SDK, no schema. Your dashboard has this snippet pre-filled with your token, plus a "Copy AI prompt" tab if an assistant is building your site.

The endpoint

Spam protection (on by default)

Every submission passes a pipeline: honeypot, origin lock (only your registered domain may post), rate limiting, and an invisible challenge on hosted pages. Bots are never told why they failed — they get a normal success response and nothing is delivered. You don't configure any of this.

For spam that slips through, add your own blocklist entries in Settings (or click Block sender on any submission): banned phrases, email addresses, or domains. Matches go straight to the form's trash — never delivered, never counted against your quota — and the sender still sees a normal success.

Testing locally

Submit from localhost (any port) and it's automatically test mode: delivered with a [Test] subject prefix and never counted against your quota. Works with curl too:

curl -X POST https://normforms.com/f/YOUR_TOKEN \
  -H "Origin: http://localhost:3000" \
  -d "name=Norm&message=it works"

Delivery rules

Webhooks

Set a webhook URL in form settings and every delivered submission is also POSTed there as JSON (held submissions fire when released, so a webhook never leaks past your quota). Failures retry with backoff for about a day.

{
  "form": { "token": "x7Kp2mQ9RtLw", "name": "Contact" },
  "submission": {
    "id": 42,
    "received_at": "2026-07-09T21:14:03Z",
    "source": "embedded",
    "data": { "name": "Norm", "email": "[email protected]", "message": "hi" }
  }
}

Each request carries X-NormForms-Timestamp (unix seconds) and X-NormForms-Signature — a hex HMAC-SHA256 of "#{timestamp}.#{raw_body}" keyed with your form's signing secret (shown in settings once a URL is saved). Verify before trusting:

# Ruby example — any language's HMAC works the same way
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw_body}")
ok = Rack::Utils.secure_compare(expected, signature) &&
     (Time.now.to_i - timestamp.to_i).abs < 300  # reject replays

Webhook URLs must be public https:// endpoints. Answer with any 2xx status; redirects aren't followed. Retries re-POST the same submission.id with a fresh timestamp and signature, so treat submission.id as an idempotency key and ignore ids you've already processed.

Plans

Free: 50 submissions/month, unlimited forms — use it for testing and development. Pro ($7/mo): 200. Growth ($15/mo): 2,000. Business ($50/mo): 20,000. Need more? Contact us. Full details on the pricing section.

Framework guides

Building with an AI assistant? Point it at https://normforms.com/llms.txt — it has everything an agent needs to wire up a form.

Stuck? [email protected] — a human reads it.