Form ID

Spam protection

Honeypot, signed timestamps, spam scoring, a human check, allowed domains and rate limits.

Every form gets layered protection with no setup: a honeypot, a signed render timestamp, spam scoring and rate limits. On top of that you can add a human check and restrict the form to your own domains.

LayerOn by defaultOutcome
Rate limitsYes, Standard preset429 rate_limited
Allowed domainsNoSpam, or 403 origin_not_allowed
Human checkNo403 challenge_required or a check page
HoneypotYes+5 to the score
Spam scoringYesSpam at 5 or more

How spam is handled

A post that is judged as spam is stored with the status spam and still gets a normal success response, so bots learn nothing. Spam sends no webhooks and no emails, doesn't count toward your monthly usage, and is deleted after 30 days. Review it in the dashboard or with the REST API; marking a submission as not spam sends its webhooks then.

Honeypot

Add a hidden input that people never fill in. The default name is _gotcha; you can change it per form under Honeypot field in the form's settings. Hiding techniques are on HTML forms.

Honeypot
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />

Spam scoring

Each post gets a score from these signals. At 5 or more it is stored as spam.

SignalReason in the dashboardScore
The honeypot is filled inhoneypot+5
Sent less than 2 seconds after the form renderedtimestamp_fast+3
The signed render timestamp is forged or older than 24 hourstimestamp_invalid+3
A link in a name or subject fieldurl_in_name+3
The same fields were sent to this form in the last 24 hoursduplicate+3
Three or more links in totalmany_urls+2
A known spam phrase (22 built in)keyword+2
An email address on a disposable email domaindisposable_email+2
  • The render timestamp only exists when the form is rendered by the SDK or the shadcn/ui component, which send a signed _ikiform_ts. Plain HTML posts without it are not penalised.
  • A name field is any field whose name contains name, firstname, lastname, fullname, surname or subject, but not words like company, website or domain.
  • Two reasons force spam regardless of score: origin_mismatch from allowed domains and captcha_unavailable when Turnstile can't be reached.

Human check

Each form has a Spam check setting with three options. It is off by default, so existing fetch and curl integrations keep working when you create a form.

OptionWhat it does
OffNo challenge. Honeypot, timestamp and scoring still run.
Ikiform (recommended)Cloudflare Turnstile run by Ikiform on ikiform.com. No Cloudflare account or keys needed.
Your own Turnstile keysYour Turnstile widget and secret key. The token's hostname is checked against your allowed domains.

How the Ikiform check runs

How a form gets a human check token, by how it submits

HTML form

  1. The browser posts the form
  2. No token yet: ikiform.com shows a short check page
  3. Turnstile runs, the page posts the same fields again with a token
  4. 303 to your redirect or the thank-you page

JavaScript

  1. challenge(formId) mounts a hidden ikiform.com frame
  2. Turnstile runs, and only shows itself if it needs a click
  3. submit(formId, data, { turnstileToken })
  4. 201 with the submission id

Server with a secret key

  1. POST with Authorization: Bearer ik_sk_...
  2. The Ikiform check is skipped
  3. 201 with the submission id
form.js
import { challenge, submit } from "@ikiform/sdk";

const form = document.querySelector("form");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const turnstileToken = await challenge("frm_xxxxxxxxxxxxxxxx");
  const result = await submit("frm_xxxxxxxxxxxxxxxx", new FormData(form), { turnstileToken });
  if (result.ok) form.replaceWith("Thanks, we got it.");
});

Tokens are single use and live 300 seconds, so get one per submit. A JSON post without a token gets 403 challenge_required with a challenge_url; a token that fails verification gets 403 captcha_failed.

403 Forbidden
{
  "ok": false,
  "error": {
    "code": "challenge_required",
    "message": "Complete the human check, then send the form again.",
    "challenge_url": "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx/challenge"
  },
  "request_id": "req_01k7v2c9t3e8m5q4x6z0a1b2c3"
}
If Turnstile itself can't be reached, the post is accepted and stored as spam with the reason captcha_unavailable, so you can restore it rather than lose it.

Your own Turnstile keys

Paste your site key and secret key into the form's settings. The secret is encrypted at rest. Render the widget in your form so it adds cf-turnstile-response to the post, or pass the token to submit as turnstileToken. Posts without a valid token get 403 captcha_failed, including posts with a secret API key.

Allowed domains

List the domains your form lives on under Allowed domains. A post must then come from one of them or a subdomain, judged by the Origin header, or the Referer when there is no origin. Leave the list empty to accept posts from any site.

Allowed domainsPost fromMatches
example.comhttps://example.comYes
example.comhttps://www.example.comYes, subdomain
example.comhttp://localhost:3000No, add localhost while developing
example.comhttps://example.coNo
app.example.comhttps://example.comNo, a parent domain doesn't match

When a post comes from another site decides what happens to a mismatch: Mark as spam (the default) or Reject with 403 origin_not_allowed. Posts with a secret API key skip this check. Allowed domains also decide which _redirect targets are followed and where the Ikiform check frame may be embedded.

Rate limits

Each form has a Rate limit preset, counted exactly per visitor IP and form. Standard allows 5 posts a minute, Relaxed 20 a minute and Strict 2 every 10 minutes. The full rules are on Rate limits.