In production · 99.9% uptime v2.0

Stop bots.
Without your
users notice.

One checkbox. Zero API keys. Zero tracking. Your users tick and move on. Bots crash into a cryptographic wall your server validates in a single line of code.

100% self-hosted Zero cookies Zero tracking No limits
demo.html — Try it live
Tick the checkbox. Zero CPU until you do.
0
Challenges issued today
0
Verified
0
Tokens consumed
0€
Cost per use
How it works

Invisible to humans.
Impossible for bots.

Three steps. Not one more. No libraries loaded ahead of time, no third-party scripts, no keys to rotate every year.

01

User ticks a checkbox

Until that click, mCaptcha sleeps. Not a single fetch, CPU cycle or DNS lookup. Your Lighthouse score won't even notice it exists.

02

Crypto challenge in ~50 ms

The browser solves a SHA-256 Proof-of-Work. If the IP has fired >30 challenges in an hour, we switch to a slider puzzle no bot crosses without real human time.

03

Your backend gets a token

A GET to /api/captcha/verify with the token. If ok=true, it's a human and you process the form. Single-use, 2-minute lifespan. One line of code in any language.

Comparison

Same goal.
Better consequences.

Everything reCAPTCHA does for you, mCaptcha does too. What reCAPTCHA does to your users, it does not.

Google reCAPTCHA
mCaptcha Recommended
No signup, no API keys
Zero user tracking
Zero third-party cookies
Zero CPU until first interaction
100% self-hosted (no external dependency)
Compatible with existing reCAPTCHA code
Unlimited free verifications
Simple CSP configuration
Complex
Minimal
Widget size (gzipped)
~150 KB
~6 KB
Installation

Two lines. Done.

No npm install. No dashboard to sign up to. No keys to copy. Paste the snippet, deploy, close the ticket.

HTML
<form action="/contact" method="POST">
    <input name="email" required>
    <textarea name="message" required></textarea>

    <div class="mcaptcha"></div>

    <button type="submit">Send</button>
</form>

<script src="https://medel.es/captcha.js" async defer></script>
Zero impact on first paint The script loads async defer. Nothing runs until the user ticks. Your Core Web Vitals will thank you.
Light, dark or automatic Defaults to prefers-color-scheme from the browser. Force it with data-theme="dark|light" on the widget div.
Drop-in for reCAPTCHA code Accepts class="g-recaptcha" and fills g-recaptcha-response. Migrate without touching a single line of your backend.
Native mobile and touch The slider puzzle responds to fingers like a native control. No obscure bicycle selectors on 4-inch screens.
Verification

Before processing the form,
validate the token.

Your backend receives mcaptcha_token. Makes a GET, checks ok=true, moves on. Five languages, one flow, zero libraries to install.

<?php
function mcaptcha($token) {
    $url = 'https://medel.es/api/captcha/verify?token=' . urlencode($token);
    $response = json_decode(file_get_contents($url));
    return $response->ok ?? false;
}

// Usage:
$token = $_POST['mcaptcha_token'] ?? '';

if (!mcaptcha($token)) {
    http_response_code(400);
    exit('Captcha failed');
}

// ✅ Captcha verified. Process the form.
// Client-side validation — useful in SPAs before submitting to your backend.
// For real protection, ALWAYS verify on your server too.
async function mcaptcha(token) {
    const url = 'https://medel.es/api/captcha/verify?token=' + encodeURIComponent(token);
    const data = await fetch(url).then(r => r.json());
    return data.ok === true;
}

// Usage in a form:
document.querySelector('form').addEventListener('submit', async (e) => {
    e.preventDefault();
    const fd = new FormData(e.target);
    const token = fd.get('mcaptcha_token');

    if (!(await mcaptcha(token))) {
        alert('Captcha failed');
        return;
    }

    // ✅ Captcha verified. Submit to your backend.
    await fetch('/api/contact', { method: 'POST', body: fd });
});
async function mcaptcha(token) {
    const url = 'https://medel.es/api/captcha/verify?token=' + encodeURIComponent(token);
    const data = await fetch(url).then(r => r.json());
    return data.ok === true;
}

// Usage (Express):
const token = req.body.mcaptcha_token;

if (!(await mcaptcha(token))) {
    return res.status(400).send('Captcha failed');
}

// ✅ Captcha verified. Process the form.
import urllib.request, urllib.parse, json

def mcaptcha(token):
    url = 'https://medel.es/api/captcha/verify?' + urllib.parse.urlencode({'token': token})
    with urllib.request.urlopen(url, timeout=5) as r:
        return json.loads(r.read()).get('ok') is True

# Usage (Flask):
token = request.form.get('mcaptcha_token', '')

if not mcaptcha(token):
    abort(400, 'Captcha failed')

# ✅ Captcha verified. Process the form.
# You only need a GET with the token as a query param:
curl 'https://medel.es/api/captcha/verify?token=YOUR_TOKEN'

# OK:   {"ok":true,"verified_at":"2026-06-22 12:24:56"}
# FAIL: {"ok":false,"error":"already_used"}
Migrating from Google reCAPTCHA?

The widget also fills g-recaptcha-response and accepts class="g-recaptcha". If your backend already reads it, just change the verify URL:

// Before:
$url = 'https://www.google.com/recaptcha/api/siteverify?secret=' . $secret . '&response=' . $token;

// After (no secret):
$url = 'https://medel.es/api/captcha/verify?response=' . $token;
API

Endpoints

Four REST endpoints. No SDK. No auth. No versioning. Only the one you need — the widget calls the rest.

GET https://medel.es/api/captcha/verify?token=… You call this

Verifies the token. Single-use: invalidated after the first successful verify, so no one can replay-attack it.

200 OK:
{"ok": true, "verified_at": "2026-06-22 12:24:56"}
400 KO:
{"ok": false, "error": "already_used"}

Codes: missing_token · unknown_token · already_used · expired · origin_mismatch

GET https://medel.es/api/captcha/challenge Widget calls it

Issues a challenge (PoW by default, slider puzzle if the IP is suspicious). The widget calls it automatically.

POST https://medel.es/api/captcha/solve Widget calls it

The widget submits the solution (PoW nonce or slider X position) and receives the verifiable token.

GET https://medel.es/api/captcha/stats Public

Aggregated stats of the day. Zero personal data, zero IPs, zero tracking.

Compatibility

Works on 98%
with zero config.

Works automatically on

  • HTTPS sites
  • Chrome / Edge / Firefox / Safari (2017+)
  • localhost in development
  • SPA (React / Vue / Svelte)
  • Server-rendered forms

If you have strict CSP

Add the domain to 3 directives:

script-src  'self' https://medel.es;
connect-src 'self' https://medel.es;
style-src   'self' https://medel.es;
FAQ

Honest questions, honest answers

When does the slider puzzle appear?
Only if your IP has fired more than 30 verifications in the last hour. For 99% of your users, the flow ends in a single click.
Is it free forever?
Yes. No card, no plan, no upsells. The service is sustained by per-IP (100/h) and per-domain (5000/h) rate limits. Need more? Write us and we raise them.
Does it work without JavaScript?
No. mCaptcha needs JS to solve the Proof-of-Work or drag the slider. If you expect non-JS traffic, keep an HTML honeypot as a fallback.
Does it track my users?
No. Zero cookies, zero fingerprinting, zero analytics. We only store the IP for 24h for rate-limiting, then auto-delete it.
Why a slider instead of "select all traffic lights"?
Image-grid CAPTCHAs need a huge labeled dataset and ML on top. A slider is visually equivalent, requires no photo bank, and works on any device — including the oldest phone.
What if Medel Captcha goes down?
The widget shows an error and blocks submit by default. As integrator you can decide to bypass if verify times out (5 seconds is plenty). 99.9% uptime, but plan B is yours.
Is it reCAPTCHA-compatible?
Yes. The widget automatically fills g-recaptcha-response and accepts class="g-recaptcha". Migrating is changing the script src and the verify URL. Zero refactor.
How do I self-host it?
It is PHP 8.2+ with MySQL. The code will land on GitHub under MIT once I clean it up. In the meantime, write me and I will hand it over.

Paste the snippet.
Forget bots.

Integrate in 30 seconds. No card. No keys. No contracts. No fine print. When a bot tries to get in, it becomes its problem, not yours.