send.webforms.toRead the docs →

Documentation

Spam protection

Layered, scored, and silent to whatever triggers it. Every submission runs through the same pipeline before it reaches storage or a destination.

How scoring works

Every layer below produces a partial score between 0 and 1; they add up. Once the total passes your form's threshold (default 0.8, configurable per form), the submission is quarantined - stored and visible in your inbox, but never delivered to a destination. A few signals (honeypot filled, disposable email when that option is set to hard-block, a blocked domain match) reject outright at score 1.0 regardless of threshold.

Either way, the sender always sees a normal success response. Returning an error to a rejected submission is how a bot learns what got it caught - silence is the point.

Signals

  • Honeypot fieldA field name (_gotcha by default, or your own choice) that a real visitor never sees or fills, hidden with CSS. A bot filling every field trips it instantly. Rejects outright.
  • Time trapSet _start_time to a timestamp when the form renders. A submission completed in under ~2 seconds (configurable) is treated as scripted.
  • Disposable emailChecks the submitted email's domain against a list of throwaway-email providers. Can hard-block or just add to the score.
  • Blocked domains / keywordsPer-form lists you maintain - block specific sending domains outright, or flag content containing chosen keywords.
  • Link floodMany URLs packed into one field is a strong spam signal on its own.
  • Origin mismatchIf you've set an allowed-origins list for the form, a submission whose Origin header doesn't match adds to the score rather than being silently accepted.
  • Empty or tiny contentA submission with almost nothing in it (common with scripted probing) scores slightly higher.
  • Sender reputationSubmissions are tracked by sender identity (email/domain, never IP) across your workspace. A sender previously confirmed as spam by the AI classifier scores worse next time, for free, without another model call.
  • AI classifierA model call as a second opinion, only when the deterministic score is ambiguous enough that it could change the outcome - not run on every submission. Available on plans with AI spam detection.
  • CaptchaTurnstile, hCaptcha or reCAPTCHA, verified server-side against the token in _captcha (or the provider's own field name).

Setting up a honeypot

Add a field named _gotcha (or your form's configured honeypot field name) and hide it with CSS - not type="hidden", which some bots skip.

index.html
<input type="text" name="_gotcha" style="position:absolute;left:-9999px" tabindex="-1" autocomplete="off" />

Setting up the time trap

Stamp a hidden field with the current time when the form renders, so the server can measure how long it took to submit:

index.html
<input type="hidden" name="_start_time" id="start-time" />
<script>
  document.getElementById("start-time").value = Date.now();
</script>

Configuring per form

Every signal is a toggle or a threshold in Form → Settings → Spam, and the same shape is writable from the write API:

Terminal
curl -X PATCH https://api.webforms.to/v1/forms/frm_xxx \
  -H "Authorization: Bearer wf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "spamConfig": {
      "honeypot": true,
      "timeTrapMs": 2000,
      "captcha": "turnstile",
      "blockDisposableEmail": true,
      "blockedKeywords": ["viagra", "crypto airdrop"],
      "allowedOrigins": ["example.com"]
    }
  }'

Reviewing quarantine

Quarantined submissions live alongside genuine ones in the dashboard, filterable by status, so nothing caught by a false-positive is actually lost - you can review and release it. The Submissions API excludes them by default; pass status=quarantined to include them.

Get your endpoint

Free for 300 submissions a month. No card required.

Start free