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.
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.
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.
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.
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.
Same goal.
Better consequences.
Everything reCAPTCHA does for you, mCaptcha does too. What reCAPTCHA does to your users, it does not.
Two lines. Done.
No npm install. No dashboard to sign up to. No keys to copy. Paste the snippet, deploy, close the ticket.
<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>
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;
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.
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;
Honest questions, honest answers
When does the slider puzzle appear?
Is it free forever?
Does it work without JavaScript?
Does it track my users?
Why a slider instead of "select all traffic lights"?
What if Medel Captcha goes down?
Is it reCAPTCHA-compatible?
How do I self-host it?
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.