Documentation

Add Spamo in about two minutes.

Spamo blocks automated form spam with no CAPTCHA. There are two ways to use it. In hosted mode you add one script tag and one attribute to your form, and Spamo stores the lead, emails you and optionally calls your webhook — no backend needed. In API mode your own backend POSTs to /api/v1/verify and gets back a plain {allowed, reason} decision. Both run the identical five checks: honeypot, timing, signature, expiry and dotted Gmail.

How do I add Spamo to a plain HTML form?

Create an account, create a site, and copy your site key from the dashboard. Then paste this. You do not need a backend, a build step or a package manager.

Hosted mode — copy and paste

<!-- 1. Load the widget once, anywhere on the page -->
<script src="https://spamo.net/assets/js/spamo.js" defer></script>

<!-- 2. Add your site key to the form. That is the whole install. -->
<form data-spamo-site="spamo_pk_YOUR_KEY_HERE"
      data-spamo-success="Thanks — we will be in touch.">
  <input type="text"  name="name"    placeholder="Your name"  required>
  <input type="email" name="email"   placeholder="Your email" required>
  <textarea name="message" placeholder="How can we help?"></textarea>
  <button type="submit">Send</button>
</form>

That is the whole install. The script adds the hidden trap field and the signed token by itself. Do not add the trap field to your HTML by hand — its name is random per site and changes nothing if you guess it wrong, but a hard-coded one is a name a bot can learn.

What does the visitor see?

Nothing. No puzzle, no checkbox, no delay. On success the form resets and shows your data-spamo-success message, or redirects to the success URL you set on the site. A blocked bot sees the exact same thing, which is the point.

How do I use Spamo from my own backend?

Use API mode. Your page still loads the widget so the token and trap field exist, but the form posts to your server. Your server then asks Spamo for a verdict using your private key, and decides what to do with the answer. Spamo never sees your lead data in this mode.

curl

# 1. Your page fetches a challenge (this happens in the browser)
curl "https://spamo.net/api/v1/challenge/spamo_pk_YOUR_KEY_HERE"
# -> {"ok":true,"token":"...","issued_at":1788159902,
#     "expires_at":1788203102,"honeypot":"yitxh","min_seconds":2}

# 2. Your BACKEND asks Spamo for a verdict, using your private key
curl -X POST "https://spamo.net/api/v1/verify" \
  -H "Authorization: Bearer spamo_sk_YOUR_PRIVATE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "site_key": "spamo_pk_YOUR_KEY_HERE",
        "spamo_token": "THE_TOKEN_FROM_THE_FORM",
        "fields": { "name": "Ada", "email": "ada@example.com", "yitxh": "" }
      }'
# -> {"allowed":true,"reason":null}
# -> {"allowed":false,"reason":"honeypot"}

PHP

<?php
// Server-side verification from your own backend.
$payload = [
    'site_key'    => 'spamo_pk_YOUR_KEY_HERE',
    'spamo_token' => $_POST['spamo_token'] ?? '',
    'fields'      => $_POST,
];

$ch = curl_init('https://spamo.net/api/v1/verify');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode($payload),
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'Authorization: Bearer ' . getenv('SPAMO_SECRET'),
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 5,
]);
$result = json_decode(curl_exec($ch), true);

if (($result['allowed'] ?? false) === true) {
    // Real person. Save the lead, send your email, redirect.
} else {
    // Blocked. It is up to you what to do — most people show the same
    // success message a real person sees, so the bot learns nothing.
}

JavaScript (server side only)

// Node / any JS backend. NEVER run this in the browser — the private key
// would be visible to anyone who opens dev tools.
const res = await fetch('https://spamo.net/api/v1/verify', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${process.env.SPAMO_SECRET}`,
  },
  body: JSON.stringify({
    site_key: 'spamo_pk_YOUR_KEY_HERE',
    spamo_token: body.spamo_token,
    fields: body,
  }),
});

const { allowed, reason } = await res.json();
if (allowed) {
  // real submission
} else {
  console.log('blocked because:', reason);
}

What is the difference between the widget and the API?

The widget lies to bots on purpose: it returns success no matter what, so a caught bot never learns it was caught. The API tells you the truth — {"allowed": false, "reason": "honeypot"} — and leaves the lying to you. If you use the API, showing a bot the same success message a human sees is the recommended behaviour.

How does Spamo actually decide?

Every submission runs these checks in this order, on our server:

1
HoneypotA hidden field with a random nonsense name, hidden with clip-path rather than display:none — bots skip fields hidden the obvious ways. Anything typed in it is a bot.
2
TimingAt least 2 seconds must pass between the token being issued and the form arriving. Nobody reads and fills a form faster than that.
3
SignatureHMAC-SHA256 over site_key|issued_at|nonce with your site's private key, compared in constant time. Edit the timestamp and the signature stops matching.
4
ExpiryTokens last 12 hours. This is the only check that returns a visible failure, so a real person who left a tab open is told to try again instead of being silently dropped.
5
Dotted GmailGmail ignores dots in the local part, so one mailbox becomes endless "unique" addresses. Three or more dots is treated as spam. A +tag suffix is normal and is ignored.
6
Rate limitA global per-site, per-IP ceiling in a rolling 10-minute window — 20 on Free, higher on paid plans. IP addresses are hashed before storage, never kept raw.

What does Spamo NOT do?

This section is not fine print. Knowing where the tool stops is how you decide whether it fits.

  • Does not stop a patient bot that loads the real page, waits a few seconds, and uses a normal-looking email. This blocks cheap, lazy spam — it is not a wall.
  • A challenge token can be reused by the same bot many times within its 12-hour window; only the site's own rate limit slows that down.
  • No CAPTCHA, no puzzle, no IP blocking — Spamo ignores IP reputation entirely by design.
  • Does not verify email deliverability or existence, does not block disposable email providers such as Mailinator, and does not block plus-alias tricks.
  • Does not stop a human manually filling junk data.
  • Does not touch login forms, password resets, admin pages, or your own APIs — Spamo is scoped to public lead, contact and signup forms only.

Does Spamo store my form data?

In hosted mode, submissions that pass are stored so you can read them in your dashboard, and are emailed to your notification address. Blocked attempts keep metadata only — a hashed IP, the user agent, the reason and a timestamp — so a spam flood cannot fill your account with junk. Raw IP addresses are never written to disk. In API mode nothing but the verdict metadata is stored, because the payload stays on your server.

Endpoint reference

MethodPathAuthReturns
GET/api/v1/challenge/{site_key}None (public key)token, issued_at, expires_at, honeypot
POST/api/v1/submit/{site_key}None (public key){ok:true} always, except {ok:false,"reason":"expired"}
POST/api/v1/verifyBearer private key{allowed, reason}

Block reasons: honeypot, timing, signature, expired, gmail_dots, rate_limit, inactive.

Need somewhere to put your site?

Spamo protects the form, not the hosting. If you still need a place to put the site itself, this is what we use.

Ready? Grab your site key.

Free plan, 500 submissions a month, no card. The snippet is waiting in your dashboard.

Create a free account