TruForms supports two captcha providers: Cloudflare Turnstile and hCaptcha. Both are free to use and work the same way — add a widget to your form, and TruForms verifies the token on the server before accepting any submission.
When to use
Most forms don't need captcha right away. Every form already includes:
- A honeypot field that catches basic bots
- Per-IP and per-form rate limits
- Spam scoring based on submission patterns
Add captcha when:
- A specific form is being actively targeted by bots
- You need a visible "I'm not a robot" challenge for compliance
- You're seeing submissions from headless browsers that bypass the honeypot
Cloudflare Turnstile
Invisible most of the time — users rarely see a challenge. Free with no request limits.
Step 1 — Get your keys
- Go to Cloudflare Dashboard → Turnstile.
- Click Add site, name it, and add the domains where your form lives.
- Choose Managed (recommended) or Invisible widget mode.
- Copy the Site key and Secret key.
Step 2 — Add to your form in TruForms
- Open Forms → your form → Settings → Security.
- Set Captcha to Cloudflare Turnstile.
- Paste your Site key and Secret key, then click Save.
Step 3 — Add the widget to your HTML form
<form action="https://truforms.truenotech.com/api/submit" method="POST">
<input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
<input type="text" name="name" required />
<input type="email" name="email" required />
<textarea name="message" required></textarea>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Send</button>
</form>Cloudflare auto-injects a hidden cf-turnstile-response field — no extra JavaScript needed.
React
import { useState } from 'react';
import { Turnstile } from '@marsidev/react-turnstile';
export function ContactForm() {
const [token, setToken] = useState(null);
async function onSubmit(e) {
e.preventDefault();
const data = new FormData(e.currentTarget);
data.append('access_key', 'YOUR_ACCESS_KEY');
data.append('cf-turnstile-response', token);
await fetch('https://truforms.truenotech.com/api/submit', { method: 'POST', body: data });
}
return (
<form onSubmit={onSubmit}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<Turnstile siteKey="YOUR_SITE_KEY" onSuccess={setToken} />
<button type="submit" disabled={!token}>
Send
</button>
</form>
);
}Test keys (localhost only)
| Site key | Secret key | Behaviour |
|---|---|---|
1x00000000000000000000AA |
1x0000000000000000000000000000000AA |
Always passes |
2x00000000000000000000AB |
2x0000000000000000000000000000000AA |
Always blocks |
3x00000000000000000000FF |
1x0000000000000000000000000000000AA |
Forces interactive challenge |
hCaptcha
Shows a visible checkbox or image challenge. Free up to 1M requests/month.
Step 1 — Get your keys
- Sign up at hcaptcha.com.
- Add a site and copy the Site key (UUID format) and Secret key (
0x…).
Step 2 — Add to your form in TruForms
- Open Forms → your form → Settings → Security.
- Set Captcha to hCaptcha.
- Paste your Site key and Secret key, then click Save.
Step 3 — Add the widget to your HTML form
<form action="https://truforms.truenotech.com/api/submit" method="POST">
<input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
<input type="email" name="email" required />
<textarea name="message" required></textarea>
<script src="https://hcaptcha.com/1/api.js" async defer></script>
<div class="h-captcha" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Send</button>
</form>hCaptcha auto-injects an h-captcha-response field.
Vue
<script setup>
import { ref } from 'vue';
import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
const token = ref(null);
async function onSubmit(e) {
const data = new FormData(e.target);
data.append('access_key', 'YOUR_ACCESS_KEY');
data.append('h-captcha-response', token.value);
await fetch('https://truforms.truenotech.com/api/submit', { method: 'POST', body: data });
}
</script>
<template>
<form @submit.prevent="onSubmit">
<input name="email" type="email" required />
<textarea name="message" required />
<VueHcaptcha sitekey="YOUR_SITE_KEY" @verify="(t) => (token = t)" />
<button type="submit" :disabled="!token">Send</button>
</form>
</template>Test keys (localhost only)
| Site key | Secret key | Behaviour |
|---|---|---|
10000000-ffff-ffff-ffff-000000000001 |
0x0000000000000000000000000000000000000000 |
Always passes |
Sending the token manually (JSON / fetch)
If you POST JSON instead of a form, include the token in your request body under any of these field names (checked in this order):
cf-turnstile-response— what Cloudflare auto-injectsh-captcha-response— what hCaptcha auto-injectscaptcha_token— generic alias for manual use
await fetch('https://truforms.truenotech.com/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
access_key: 'YOUR_ACCESS_KEY',
name: 'Aanya',
email: '[email protected]',
'cf-turnstile-response': token,
}),
});The token is stripped from the stored payload and never appears in your dashboard, emails, or webhook deliveries.
Error responses
| Status | Error | Meaning |
|---|---|---|
403 |
CaptchaMissing |
Captcha is enabled but no token was in the payload |
403 |
CaptchaFailed |
Token is invalid, expired, or already used |
403 |
CaptchaNotConfigured |
Provider set in dashboard but no secret key saved |
403 |
CaptchaVerifyFailed |
Network error reaching the provider |
{
"statusCode": 403,
"error": "CaptchaFailed",
"message": "Captcha verification failed"
}Troubleshooting
CaptchaMissing even though I see the widget
Check your browser's Network tab — confirm cf-turnstile-response or h-captcha-response is in the POST body. When submitting via fetch with JSON, you must read the token from the widget callback and include it manually. Auto-inject only works for native <form> submissions.
CaptchaFailed in production but works locally
The token is bound to the domain registered in your provider dashboard. Add your production domain to the widget config. Tokens also expire after ~5 minutes — if your form sits idle, prompt the user to re-verify.
CaptchaNotConfigured
You set the provider in Settings → Security but didn't save a secret key, or the encryption key changed since you saved it. Re-paste the secret and save again.
Honeypot still triggering
Captcha and the honeypot run independently. If botcheck is non-empty the submission is silently rejected regardless of captcha result. Make sure your form doesn't auto-fill the honeypot field.