Skip to main content
Image
SOC 2 Type 2 attested · Data Privacy Framework certified

Stop bots without compromising trust

MTCaptcha pairs an adaptive, invisible verification engine with SOC 2 Type 2 attestation, Data Privacy Framework certification, and WCAG 2.1 AAA accessibility: real compliance for regulated industries, built into a widget your users never notice.

SOC 2 Type 2 · GDPR & DPF certified · WCAG 2.1 AAA · Proven in China

Live widget, not a screenshot

See it on your site before you write a line of code

Every control changes the real MTCaptcha widget and the code that ships it. Start from our defaults, then make it yours.

Where it sits

Palette

Capabilities

Modern MiniCompact, one line
InvisibleNo challenge for humans
AccessibilityWCAG 2.1 AAA, audio

Language

Explore 20 brand templates Banking, health, gaming, luxury and more
app.yoursite.com/login

Welcome back

Sign in to continue

', '', ].join('\n'); } function paintCode(mount, src) { var n = src.split('\n').length, nums = ''; for (var i = 1; i <= n; i++) nums += i + '\n'; mount.innerHTML = '' + '
' + highlight(src) + '
'; } /* ---------- render ---------- */ var patched = false; function patchRemove() { if (patched || !window.mtcaptcha || !window.mtcaptcha.remove) return; var orig = window.mtcaptcha.remove.bind(window.mtcaptcha); window.mtcaptcha.remove = function (id) { try { return orig(id); } catch (e) { return undefined; } }; patched = true; } setInterval(patchRemove, 200); var timer = null; function rerender() { clearTimeout(timer); timer = setTimeout(function () { patchRemove(); /* By id, never by the bare 'mtcaptcha'. The vendor resolves a missing or unknown id to `domId || "class:mtcaptcha"`, which on a page carrying a second widget - the contact form's, on every Use Cases page - is the wrong one as often as the right one: this would tear down and rebuild the contact captcha, discarding a token a visitor had already solved. */ if (window.mtcaptcha && window.mtcaptcha.remove) window.mtcaptcha.remove('mtcaptcha-demo'); CFG.renderQueue.push('mtcaptcha-demo'); fitWidget(); }, 280); } /* The widget's own stylesheet floors it at 285px wide and clips the rest, so on a phone narrower than that it is scaled down instead of losing its right edge. Everywhere there is room this is a no-op: the factor stays at 1. The slot's height is set from the scaled height because the widget is transformed, and a transform does not affect layout: without this the card would keep reserving the unscaled height and leave a gap above Sign in. */ var WIDGET_MIN_W = 285; function fitWidget() { var slot = document.querySelector('.mtc-slot'); if (!slot) return; var holder = slot.querySelector('.mtcaptcha'); var avail = slot.clientWidth; if (!holder || !avail) return; if (avail >= WIDGET_MIN_W) { slot.classList.remove('is-scaled'); slot.style.removeProperty('--mtc-fit'); slot.style.height = ''; return; } /* Width and transform go on first, so offsetHeight is the height the widget actually takes at 285px rather than at its squeezed width. */ slot.classList.add('is-scaled'); var f = avail / WIDGET_MIN_W; slot.style.setProperty('--mtc-fit', String(f)); var h = holder.offsetHeight; slot.style.height = h ? Math.ceil(h * f) + 'px' : ''; } window.addEventListener('resize', fitWidget); /* Watch the card, not the slot: fitWidget sets the slot's height, so observing the slot would feed its own writes back in. Only a real change in available width does anything. */ if (window.ResizeObserver) { var cardEl = document.querySelector('.mtc-card'); var lastW = 0; if (cardEl) { new ResizeObserver(function () { var w = cardEl.clientWidth; if (w === lastW) return; lastW = w; fitWidget(); }).observe(cardEl); } } /* The console's controls are bordered with --line, an alpha of the ink rather than a tinted hex, which is what makes them read as subtle at any background. The mock's fields use the same two values so its borders match the language select exactly, and stay subtle under every palette instead of picking up that palette's tint. */ function subtleLine(th) { return th.dark ? 'rgba(255, 255, 255, 0.09)' : 'rgba(4, 10, 26, 0.09)'; } /* What a visitor types is the console's --ink, not the palette's. Every palette carries its own ink, tuned to its own hues - Sandstone's is a warm #2f2617, Ocean's a blue-black - and switching palette moved the field text off the colour the language select uses. Read the token rather than repeating its value, so it cannot drift from global.css. */ function siteInk() { var v = getComputedStyle(document.documentElement).getPropertyValue('--ink').trim(); return v || (siteIsDark() ? '#f4f6ff' : '#040a1a'); } /* Alpha rather than a second hex per palette, so a lightened colour cannot drift away from the ink it was lightened from. */ function fade(hex, a) { var c = toRgb(hex); return 'rgba(' + c[0] + ', ' + c[1] + ', ' + c[2] + ', ' + a + ')'; } function subtleLineStrong(th) { return th.dark ? 'rgba(255, 255, 255, 0.16)' : 'rgba(4, 10, 26, 0.14)'; } /* sRGB relative luminance and contrast ratio, WCAG 2.x. */ function toRgb(hex) { var h = hex.replace('#', ''); if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2]; var n = parseInt(h, 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; } function lum(c) { var a = c.map(function (v) { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }); return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2]; } function contrast(a, b) { var l1 = lum(a), l2 = lum(b); return l1 < l2 ? (l2 + 0.05) / (l1 + 0.05) : (l1 + 0.05) / (l2 + 0.05); } function mix(fg, bg, a) { return [0, 1, 2].map(function (i) { return Math.round(fg[i] * a + bg[i] * (1 - a)); }); } /* The lightest `hex` can be drawn on `bg` while still clearing `target`. Contrast falls monotonically as the colour is mixed toward the background, so the smallest passing alpha is a plain bisection. A palette with no headroom left is returned untouched rather than darkened: this exists to lighten, and quietly strengthening a palette's own colour would be a different change than the one asked for. */ function lightestAA(hex, bgHex, target) { var fg = toRgb(hex), bg = toRgb(bgHex); if (contrast(fg, bg) < target) return hex; var lo = 0, hi = 1; for (var i = 0; i < 14; i++) { var mid = (lo + hi) / 2; if (contrast(mix(fg, bg, mid), bg) >= target) hi = mid; else lo = mid; } var c = mix(fg, bg, hi); return 'rgb(' + c[0] + ', ' + c[1] + ', ' + c[2] + ')'; } function styleFor(th) { var out = { cardColor: th.card, cardBorder: '1px solid ' + th.line, cardBorderRadius: '12px', inputBackgroundColor: th.inputBg, inputBorderColor: { byDefault: subtleLine(th), hover: subtleLineStrong(th), active: th.accent }, inputTextColor: siteInk(), /* The prompt is lighter than the text a visitor types, and lighter than the widget's own default, which is close enough to the typed colour to read as a pre-filled value. Ink at 38% over the field background lands well clear of both while still tracking the palette. */ placeHolderColor: fade(siteInk(), 0.38), /* "Privacy & Terms" and "Powered by MTCaptcha" are the widget's outer captions, and this is the only key that reaches them, so they lighten without touching the fields, the prompt or the card. Lightened to the limit rather than by a fixed amount. A single alpha cannot work here: the palettes' muted greys already sit between 4.7:1 and 8.1:1 on their own cards, so the same step that visibly lightens MTCaptcha's #4a5060 pushes Ocean's #5a7690 straight through the floor. Each palette is given the lightest caption it can afford instead. The floor is 3.6:1, which is BELOW normal-text AA, and that is a decision rather than an oversight - do not "fix" it back to 4.5 without asking. Both strings are small text: 12.32px for "Powered by MTCaptcha" and 10.72px for the legal block, well under the 18.66px that would let bold count as large, so AA would ask 4.5:1 of them. It was set there first and the captions were judged still too heavy. The two share one key, so the legal block - a pair of real links - could not be held at AA while only the caption lightened. 3.6 rather than lower because that is the AA floor for large text and the minimum WCAG asks of any UI component boundary, so the captions stay above the point where they stop reading as text at all. */ msgTextColor: lightestAA(th.muted, th.card, 3.6), buttonIconColor: a11y ? th.accent : th.muted, inputTextFont: FONT_STACK, msgTextFont: FONT_STACK, }; /* Outlined is the default here: every palette already gets a 1px card border from its own line colour. Only the gradient treatment adds anything on top. */ if (th.gradient) { out.cardBGGradient = 'linear-gradient(160deg, ' + th.card + ' 0%, ' + th.accentSoft + ' 100%)'; } return out; } function siteIsDark() { return document.documentElement.classList.contains('dark'); } /* Name and treatment belong to the brand; colours come from the mode. */ function resolve(entry) { return Object.assign({ name: entry.name, gradient: entry.gradient }, siteIsDark() ? entry.dark : entry.light); } function apply() { var th = resolve(THEMES[themeIx]), c = CTX[ctx]; /* Scoped to the demo container, never to . */ root.style.setProperty('--demo-page', th.page); root.style.setProperty('--demo-card', th.card); root.style.setProperty('--demo-line', th.line); root.style.setProperty('--demo-ink', th.ink); root.style.setProperty('--demo-field-ink', siteInk()); root.style.setProperty('--demo-muted', th.muted); root.style.setProperty('--demo-accent', th.accent); root.style.setProperty('--demo-on-accent', th.onAccent); root.style.setProperty('--demo-input-bg', th.inputBg); root.style.setProperty('--demo-input-line', subtleLine(th)); root.style.setProperty('--demo-shadow', th.dark ? '0 14px 40px rgba(0,0,0,0.5)' : '0 12px 34px rgba(6,10,24,0.08)'); $('mtc-addr').textContent = c.url; $('mtc-c-title').textContent = c.title; $('mtc-c-sub').textContent = c.sub; $('mtc-c-go').textContent = c.go; $('mtc-c-fields').innerHTML = c.fields.map(function (f, i) { return '' + ''; }).join(''); /* Invisible has nothing to render, which is the point. An empty gap reads as a broken widget, so it says so instead. */ $('mtc-inv-note').innerHTML = invisible ? '
' + S.invisibleNoteTitle + ' ' + S.invisibleNoteBody + '
' : ''; $('mtc-a11y-note').innerHTML = a11y && !invisible ? '

' + S.a11yNote + '

' : ''; var cfg = { sitekey: '', lang: wlang }; /* No autoFadeOuterText on the dark palettes. It fades the widget's outer text out entirely, so "Privacy & Terms" and "Powered by MTCaptcha" went invisible against a dark card. msgTextColor already carries the dark palette's muted tone, which is the same token the form's own labels use, so the captions stay legible and match the rest of the card. */ if (th.dark) { cfg.theme = 'blackmoon'; } if (mini) cfg.widgetSize = 'mini'; if (invisible) cfg.lowFrictionInvisible = 'force-invisible'; cfg.customFonts = { fonts: [{ family: 'Poppins', url: FONT_URL, format: 'woff2' }], defaultFontFamily: FONT_STACK }; cfg.customStyle = styleFor(th); codeText = snippet(cfg); paintCode($('mtc-snippet'), codeText); CFG.lang = wlang; CFG.theme = th.dark ? 'blackmoon' : null; CFG.autoFadeOuterText = null; CFG.widgetSize = mini ? 'mini' : null; CFG.customFonts = cfg.customFonts; CFG.customStyle = cfg.customStyle; /* Deleted rather than nulled: the vendor reads the key's presence. */ if (TESTKEY) CFG.enableTestMode = TESTKEY; else delete CFG.enableTestMode; if (invisible) { CFG.testmode_lowFrictionInvisible = 'force-invisible'; CFG.lowFrictionInvisible = null; CFG.loadAnimation = 'false'; } else { CFG.testmode_lowFrictionInvisible = 'force-visible'; CFG.lowFrictionInvisible = 'force-visible'; CFG.loadAnimation = 'true'; } $('mtc-live').textContent = th.name + ', ' + ctx; rerender(); } /* ---------- controls ---------- */ var sw = $('mtc-swatches'); function chooseTheme(i) { themeIx = i; [].forEach.call(sw.children, function (x, j) { x.setAttribute('aria-checked', String(j === i)); x.tabIndex = j === i ? 0 : -1; }); apply(); } THEMES.forEach(function (th, i) { var b = document.createElement('button'); b.className = 'mtc-sw'; b.type = 'button'; b.setAttribute('role', 'radio'); b.setAttribute('aria-label', th.name); b.setAttribute('aria-checked', i === 0 ? 'true' : 'false'); b.tabIndex = i === 0 ? 0 : -1; var chip = resolve(th); b.style.background = 'linear-gradient(135deg,' + chip.page + ' 0 50%,' + chip.accent + ' 50% 100%)'; b.onclick = function () { chooseTheme(i); }; b.onkeydown = function (e) { var d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0; if (!d) return; e.preventDefault(); var n = (i + d + THEMES.length) % THEMES.length; chooseTheme(n); sw.children[n].focus(); }; sw.appendChild(b); }); var ctxBtns = [].slice.call(document.querySelectorAll('#mtc-ctxs [data-ctx]')); function pickCtx(i) { ctx = ctxBtns[i].getAttribute('data-ctx'); ctxBtns.forEach(function (x, j) { x.setAttribute('aria-checked', String(j === i)); x.tabIndex = j === i ? 0 : -1; }); apply(); } ctxBtns.forEach(function (b, i) { b.tabIndex = i === 0 ? 0 : -1; b.onclick = function () { pickCtx(i); }; b.onkeydown = function (e) { var d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0; if (!d) return; e.preventDefault(); var n = (i + d + ctxBtns.length) % ctxBtns.length; pickCtx(n); ctxBtns[n].focus(); }; }); function toggle(id, set) { var el = $(id); el.onclick = function () { var on = el.getAttribute('aria-checked') !== 'true'; el.setAttribute('aria-checked', String(on)); set(on); apply(); }; } toggle('mtc-t-mini', function (v) { mini = v; }); toggle('mtc-t-inv', function (v) { invisible = v; }); toggle('mtc-t-a11y', function (v) { a11y = v; }); Select($('mtc-lang-mount'), LANGS.map(function (l) { return { value: l[0], label: l[1] }; }), initialLang, 'mtc-g-lang', function (v) { wlang = v; apply(); }); var tabP = $('mtc-tab-preview'), tabC = $('mtc-tab-code'); function selectTab(which) { var p = which === 'preview'; tabP.setAttribute('aria-selected', String(p)); tabC.setAttribute('aria-selected', String(!p)); tabP.tabIndex = p ? 0 : -1; tabC.tabIndex = p ? -1 : 0; $('mtc-pane-preview').hidden = !p; $('mtc-pane-code').hidden = p; $('mtc-copy').hidden = p; $('mtc-addr').hidden = !p; } tabP.onclick = function () { selectTab('preview'); }; tabC.onclick = function () { selectTab('code'); }; [tabP, tabC].forEach(function (b) { b.onkeydown = function (e) { if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return; e.preventDefault(); var next = b === tabP ? tabC : tabP; selectTab(next === tabP ? 'preview' : 'code'); next.focus(); }; }); var copyBtn = $('mtc-copy'); copyBtn.onclick = function () { navigator.clipboard.writeText(codeText); copyBtn.textContent = copyBtn.getAttribute('data-copied'); setTimeout(function () { copyBtn.textContent = copyBtn.getAttribute('data-copy'); }, 1200); }; /* The site's theme toggle flips a class on , so an observer is the only signal available; there is no event to listen for. Guarded on tracksSite so an explicit palette choice survives a theme change. */ new MutationObserver(function () { [].forEach.call(sw.children, function (x, j) { var chip = resolve(THEMES[j]); x.style.background = 'linear-gradient(135deg,' + chip.page + ' 0 50%,' + chip.accent + ' 50% 100%)'; x.setAttribute('aria-label', THEMES[j].name); }); apply(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); selectTab('preview'); setTimeout(apply, 500); })(); })();

Proven at scale

2B+
verifications per month
99.999%
uptime SLA
190+
countries served
<30ms
added latency

Why MTCaptcha

Everything you need to stop bots

Enterprise-grade protection that respects privacy, accessibility, and your users.

SOC 2 Type 2 attested
WCAG 2.1 AAA AAA accessibility
GDPR EU data protection
DPF EU, UK & Swiss
CCPA CA privacy

Adaptive invisible NoCAPTCHA

Frictionless verification for real humans, robust against bots. Most visitors pass without lifting a finger.

99.5% human first-passInvisibleNo tracking

Adaptive risk engine

Continuously reads the threat environment and adjusts difficulty. Easy on humans, hard on bots.

Built for the enterprise

Multi-user console, automated regression tests, server-side token decryption, and Threat SPECT.

Enterprise dashboards

Multiple user accounts with rich analytics and metrics to monitor traffic and risk.

GDPR-compliant captcha

Privacy-first protection against spam and abuse, without invasive tracking.

24/7 global availability

Decentralized architecture and worldwide data centers keep you protected, even under DDoS.

Accessibility & WCAG/EAA

WCAG 2.1 AAA, EAA, Section 508 and EN 301 549 compliant. Colorblind-safe and screen-reader friendly.

Customizable themes

Eleven preset themes plus fully customizable CSS to match your brand exactly.

SOC 2 Type 2 attested

Independently audited proof of our security controls, ready for enterprise procurement.

How we compare

MTCaptcha vs reCAPTCHA vs hCaptcha vs Turnstile vs Friendly Captcha

Checked directly against every vendor's own site, not recalled from memory. Where a claim isn't published, it says so.

Capability MTCaptcha reCAPTCHAhCaptchaTurnstileFriendly Captcha
Free tier Yes, up to 10K/mo Yes, up to 10K/mo Yes (Basic) Yes, unlimited (Standard) Yes, 1K/mo (non-commercial)
Paid plan starts at $29/mo ~$1 per 1K after free tier $99/mo (100K included) No paid tier for most use €9/mo
Detection method Adaptive invisible + Proof of Work Invisible, risk score (v3/Enterprise) Checkbox + puzzle; passive mode on higher tiers Invisible, managed challenge Invisible, Proof of Work
SOC 2 Type 2 Attested Google Cloud SOC 2 only Not published Not published ISO 27001 in progress, not SOC 2
Data Privacy Framework Certified Not published Not published Not published Not published
WCAG conformance 2.1 AAA Not published 2.2 AA Not published 2.2 AA
Works in China Yes, verified Inconsistent "Works in every country" (self-reported) Not published Not published

Checked directly against each vendor's own site, 11 Aug 2026. "Not published" means no public claim was found either way, not that the vendor lacks it.

Trust & compliance

Security your auditors will love

Independent, third-party assurance, not marketing claims. Deploy with the attestations and data-handling guarantees enterprise procurement actually requires.

Features

How it works

Protected in three steps

01

Embed the widget

Add one script tag and a widget element to any form.

02

Verify server-side

Check the returned token from your backend using your private key.

03

Block the bots

Automated and abusive traffic is stopped before it ever reaches you.

Built for developers

Integrate in minutes, not sprints

Clean APIs, predictable tokens, and documentation that gets out of your way. Ship privacy-first protection without rebuilding your forms.

Read the developer guide
login.html
<html>
  <head>
    <!-- Configuration to construct the captcha widget. Sitekey is a Mandatory Parameter-->
    <script>
      var mtcaptchaConfig = {
        "sitekey": "<YOUR SITE KEY>"
       };
      (function(){var mt_service = document.createElement('script');mt_service.async = true;mt_service.src = 'https://service.mtcaptcha.com/mtcv1/client/mtcaptcha.min.js';(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(mt_service);
      var mt_service2 = document.createElement('script');mt_service2.async = true;mt_service2.src = 'https://service2.mtcaptcha.com/mtcv1/client/mtcaptcha2.min.js';(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(mt_service2);}) ();
    </script>
  </head>
  <body>
    <!-- MTCaptcha library by default looks for a DIV of class='mtcaptcha' to place the captcha widget -->
    <div class="mtcaptcha"></div>
  </body>
</html>

Trusted by enterprises

Granicus
ABB
ORES – Opérateur des Réseaux Gaz et Électricité
Philips
American Physical Society
enmacc
MAIF
axi
EPAM
Vodafone
DP World
FLEETCOR
Shipco Transport
3Shape
Colliers
Munich RE
Mercateo
Graco
Woozworld
Leica Microsystems

Loved by customers

Trusted by teams who care about UX and privacy

Reviews from G2

Works in China, GDPR compliant, a great support team, and an informative dashboard to monitor everything. Multiple language support in the captcha too.
V Verified User Financial Services
MTCaptcha is a great captcha alternative that adheres to current privacy laws, with low-friction invisible verification. It is priced well, and their team was incredibly responsive with high-expertise assistance.
B Bob H. Verified customer
MTCaptcha is privacy compliant in the EU. Implementing it on any site was quite easy and worked well. We are more than satisfied!
C Christoph K. Verified customer
Ease of use and support. Great to work with and very responsive. They listen to customer needs and adapt.
V Verified User Verified customer
The interface is super user-friendly and secure. It helps identify real users from bots, so companies get real data.
A Armando S. Verified customer

Ready to stop bots the smart way?

Join the teams protecting their forms without sacrificing privacy or user experience.

By clicking “Accept”, you agree to our use of cookies, including analytics cookies that help us understand how the site is used. Cookies essential to the captcha widget are always on. Cookie policy