Sticker captcha
A human check made of stickers.
Two of the stickers are the same. A person sees which in a moment; a bot has to tell look-alikes apart while they move. Free for your sign-ups, comments and contact forms, with keys you make below in seconds.
- 3 kinds of check
- No cookies or tracking
- Set per key
- Free
Tap the two stickers that are the same.
Verified: a person
How it works
The check the mssgs apps ask of every new account, as a widget for your own forms. Four steps, and only the third is the visitor’s.
- 1
A box on your form
“I’m a person”, in a frame from mssgs-stickers.com. Nothing else loads until it is pressed, so a page with the box costs one small script.
- 2
A set drawn for this check
Eight stickers, two of them the same. Every tile is drawn anew: turned, scaled and moved by its own amount, its caption cut off, started at its own moment. No two tiles share a file, not even the pair.
- 3
The visitor taps the pair
A wrong pick brings new stickers, an answer faster than anyone could see them is refused, and eight misses mean a wait. A person is usually through in a few seconds.
- 4
Your server asks us
The form sends a token along. Your server posts it to
siteverifywith your secret and hears whether a person passed, on which site, in which check. A token is good once, for five minutes.
Why bots have a hard time
Every kind of check stops scripts and spam tools. The harder kinds also make the pair something you have to watch.
No file matches
The pair are one sticker drawn twice, differently. Hashing the tiles, or comparing their pixels, finds nothing equal.
Nothing to read
Each sticker’s caption is cut off before it is drawn, so text recognition has no “SOUP PLEASE” to match on.
Look-alikes
In a multi-round or anti-AI check a whole set is one character: eight cream cats, eight blue penguins. The pair hides among its own kind.
The answer is in the motion
In an anti-AI check the pair moves out of step, and a copy of it that only holds a pose stands among them. A screenshot shows the wrong two as the match.
What an AI agent sees, and what you see
AI agents that fill in forms look at the page as a screenshot. These are the same nine tiles of a real anti-AI round, both times.
Not a match: one of them only sways on the spot.
These two move the same.
Three kinds of check, set per key
Pick one when you make your keys and change it whenever you like, on the settings page or with the API. The widget asks the new way from its next check on.
Simple
One round: eight different stickers, two of them the same. What the mssgs apps ask of a new account. Stops scripts and form spam; a person is through in about three seconds.
1 round · 8 stickers a set · simple
Multi-round
Two to five rounds in a row, each one a single character, so the pair hides among look-alikes. One wrong pick starts the check over. For sign-ups and anything worth farming.
2–5 rounds, 3 unless you say · 8 stickers a set · multi
Anti-AI
Look-alikes that only give themselves away in motion: the pair moves out of step, and a copy of it that only holds a pose stands among them. A screenshot, which is what an AI agent sees, points at the wrong two. For forms that AI agents fill in.
1–4 rounds, 2 unless you say · 9 stickers a set · anti_ai
Tap the two stickers that are the same.
Multi-round, as it plays
Each round is one character, never the same one twice running, and every round has to be right: one wrong pick starts the check from round one with new stickers. Guessing three rounds of eight takes about twenty-two thousand tries, and the eighth miss in a quarter of an hour makes that visitor wait.
The widget says which round it is on, so a person knows how far along they are.
Try it
The real widget, on demo keys. Pick a kind of check and find the pair.
One round: eight different stickers, two of them the same. What the mssgs apps ask of a new account. Stops scripts and form spam; a person is through in about three seconds.
Two to five rounds in a row, each one a single character, so the pair hides among look-alikes. One wrong pick starts the check over. For sign-ups and anything worth farming.
Look-alikes that only give themselves away in motion: the pair moves out of step, and a copy of it that only holds a pose stands among them. A screenshot, which is what an AI agent sees, points at the wrong two. For forms that AI agents fill in.
Add it to your site
Two parts: the widget in your form, and one call from your server when the form comes in.
1. The widget
<form action="/signup" method="post">
…your fields…
<div class="mssgs-captcha" data-sitekey="mcs_…"></div>
<button type="submit">Sign up</button>
</form>
<script src="https://mssgs-stickers.com/captcha/v1/widget.js" async defer></script>
When the visitor passes, the widget puts the token in a hidden field, mssgs-captcha-response, so it goes along with the form. The frame grows to hold the stickers and shrinks back when they are done.
2. Your server checks the token
curl https://mssgs-stickers.com/api/v1/captcha/siteverify \
-d secret=mcx_… \
-d response="$TOKEN_FROM_THE_FORM"
{
"success": true,
"challenge_ts": "2026-11-02T10:31:07Z",
"hostname": "example.com",
"mode": "multi",
"rounds": 3,
"solve_ms": 7420,
"error-codes": []
}
Refuse the form unless success is true. The answer has the shape reCAPTCHA’s and Turnstile’s have, so code that reads those reads this too. Check hostname if one key serves several sites. Send remoteip as well and the answer says whether it is the address that passed (remoteip_match). The body may be a form or JSON.
Node
const res = await fetch('https://mssgs-stickers.com/api/v1/captcha/siteverify', {
method: 'POST',
body: new URLSearchParams({
secret: process.env.MSSGS_CAPTCHA_SECRET,
response: req.body['mssgs-captcha-response'] || ''
})
});
const result = await res.json();
if (!result.success) {
return res.status(400).send('Please do the sticker check.');
}PHP
$ch = curl_init('https://mssgs-stickers.com/api/v1/captcha/siteverify');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => http_build_query([
'secret' => getenv('MSSGS_CAPTCHA_SECRET'),
'response' => $_POST['mssgs-captcha-response'] ?? '',
]),
]);
$result = json_decode(curl_exec($ch), true);
if (empty($result['success'])) {
http_response_code(400);
exit('Please do the sticker check.');
}Python
r = requests.post(
"https://mssgs-stickers.com/api/v1/captcha/siteverify",
data={"secret": os.environ["MSSGS_CAPTCHA_SECRET"],
"response": request.form.get("mssgs-captcha-response", "")},
timeout=10,
)
if not r.json().get("success"):
abort(400)Go
res, err := http.PostForm("https://mssgs-stickers.com/api/v1/captcha/siteverify", url.Values{
"secret": {os.Getenv("MSSGS_CAPTCHA_SECRET")},
"response": {r.PostFormValue("mssgs-captcha-response")},
})
if err != nil {
return err
}
defer res.Body.Close()
var result struct{ Success bool `json:"success"` }
if err := json.NewDecoder(res.Body).Decode(&result); err != nil || !result.Success {
http.Error(w, "Please do the sticker check.", http.StatusBadRequest)
return nil
}Errors siteverify can give
| error-codes | Means |
|---|---|
missing-input-secret, invalid-input-secret | No secret, or not one of ours (or its key was turned off). |
missing-input-response, invalid-input-response | No token, or not one this key’s widget handed out. |
timeout-or-duplicate | The token is older than five minutes, or was checked before. |
The widget’s options
On the div | Does |
|---|---|
data-sitekey | Your site key. The only one you need. |
data-theme | auto, light or dark, over the key’s own setting. |
data-callback | The name of a function of yours to call with the token when the visitor passes. |
data-expired-callback | Called when the token runs out (after five minutes); the box asks again. |
data-error-callback | Called with a code when the check cannot run: UNKNOWN_SITE, SITE_OFF, DOMAIN_NOT_ALLOWED. |
data-response-field-name | The hidden field’s name, if mssgs-captcha-response does not suit your form. |
For pages that build their forms in script, load widget.js?render=explicit&onload=yourFunction and call mssgsCaptcha.render(element, {sitekey: 'mcs_…', callback: fn}), which gives back an id for mssgsCaptcha.reset(id), getResponse(id) and remove(id). The widget also fires mssgs-captcha:verified and mssgs-captcha:expired events on its div.
Settings, per key
| Setting | Values |
|---|---|
mode | simple, multi or anti_ai: the three kinds of check above. |
rounds | Rounds in a row: 1 for simple, 2–5 for multi, 1–4 for anti_ai. |
tiles | Stickers in a set: 6 (3×2), 8 (4×2), 9 (3×3) or 12 (4×3). |
speed_check | Refuse answers faster than a person can give them (on unless you turn it off). |
theme | auto, light or dark. |
domains | The sites the widget may run on, each with its subdomains. Elsewhere the frame will not even load. Empty means anywhere. |
Change them on the settings page with your secret, or from a script, with the secret as the bearer:
curl https://mssgs-stickers.com/api/v1/captcha/site \
-H "Authorization: Bearer mcx_…"
curl -X PATCH https://mssgs-stickers.com/api/v1/captcha/site \
-H "Authorization: Bearer mcx_…" \
-H "Content-Type: application/json" \
-d '{"mode": "anti_ai", "rounds": 2, "domains": ["example.com"]}'
curl -X POST https://mssgs-stickers.com/api/v1/captcha/site/secret \
-H "Authorization: Bearer mcx_…"
GET answers with the key’s settings and its counts (checks begun, passed, wrong picks, tokens verified and refused). PATCH changes only what the body names. The last call makes a new secret and stops the old one at once.
Keys for your tests
| Key | Does |
|---|---|
mcs_test_pass (site key) | A real set of stickers, but any two pass, however fast. For end-to-end tests that click through. |
mcx_test_pass (secret) | siteverify always answers "success": true. |
mcx_test_fail (secret) | siteverify always answers "success": false with invalid-input-response. |
Privacy
No cookies, no tracking, no ads, nothing for anyone else. The visitor’s address is used to count tries against abuse, in memory for at most a quarter of an hour, and is sealed into the token so siteverify can tell your server whether it matches; neither you nor anyone else can read it from the token. Nothing about the visitor is stored.
Motion and access
The stickers move: that is the point of the anti-AI check, and it helps in the others. The opening shuffle stands still for visitors who ask their device for reduced motion. Every sticker is a button, reachable by keyboard, and the widget says out loud what happened after each pick. A check of pictures leaves out people who cannot see them; for forms where that matters, pair the captcha with another way in.
Get captcha keys
You get two keys:
- a site key (
mcs_…) for the widget. It is public: it sits in your page. - a secret (
mcx_…) for your server’s siteverify call. It is shown once, and we keep only a fingerprint of it.
Already have keys? Change their settings.
Free, with fair use: protect your own forms, and do not put the widget in front of anything you would not want a person to see. A key that is misused can be turned off.