TruForms
Guides

Captcha

Stop bots with Cloudflare Turnstile or hCaptcha — verified server-side before submissions are accepted.

Last updated

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

  1. Go to Cloudflare Dashboard → Turnstile.
  2. Click Add site, name it, and add the domains where your form lives.
  3. Choose Managed (recommended) or Invisible widget mode.
  4. Copy the Site key and Secret key.

Step 2 — Add to your form in TruForms

  1. Open Forms → your form → Settings → Security.
  2. Set Captcha to Cloudflare Turnstile.
  3. 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

  1. Sign up at hcaptcha.com.
  2. Add a site and copy the Site key (UUID format) and Secret key (0x…).

Step 2 — Add to your form in TruForms

  1. Open Forms → your form → Settings → Security.
  2. Set Captcha to hCaptcha.
  3. 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):

  1. cf-turnstile-response — what Cloudflare auto-injects
  2. h-captcha-response — what hCaptcha auto-injects
  3. captcha_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.

Captcha — TruForms