From Turnstile

Unlike the reCAPTCHA and hCaptcha paths, there is no Turnstile-shaped shim: PrivCaptcha does not expose a turnstile global and does not answer on a Cloudflare script path. A Turnstile migration is therefore three small changes rather than two, and this page is explicit about the third one so it does not surprise you in review.

1. The script

- <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
+ <script src="https://privcaptcha.com/1/api.js" async defer></script>

2. The container

- <div class="cf-turnstile" data-sitekey="0x4AAA..."></div>
+ <div class="privcaptcha" data-sitekey="pk_your_sitekey"></div>

The class is what our loader scans for, and the sitekey becomes your PrivCaptcha pk_... key. data-callback works the same way — see Installation.

3. The response field — this one is not a drop-in

Turnstile writes its token into a hidden input named cf-turnstile-response. Ours writes privcaptcha-response. If your handler reads the field by name, that name has to change:

- token = request.form["cf-turnstile-response"]
+ token = request.form["privcaptcha-response"]

This is the only backend edit. If you submit with fetch() instead of a normal form post, read the token with privcaptcha.getResponse() and nothing else changes.

4. The verify call

- https://challenges.cloudflare.com/turnstile/v0/siteverify
+ https://privcaptcha.com/recaptcha/api/siteverify

Turnstile's verify response is shaped like reCAPTCHA's, so our reCAPTCHA-shaped endpoint is the closest match: it returns {success, challenge_ts, hostname, error-codes} and most Turnstile server libraries accept that unchanged. Send your PrivCaptcha secret as secret and the token as response, exactly as before.

What you will not get back: Turnstile adds action and cdata to its verify response. We do not return those fields. If your code branches on them, remove that branch — the equivalent scoping in PrivCaptcha is a separate sitekey per surface, plus the domain allowlist on each site.

If you would rather use the first-party endpoint and a typed helper, see Verify a token and the SDKs.

What carries over unchanged

  • The widget position. It sits in front of the same submit button.
  • Your domain allowlist. Configure it per site in the dashboard; requests from an origin that is not on the list are refused with invalid-input-origin.
  • Accessibility. Screen-reader and keyboard visitors get the non-visual challenge as a first-class path rather than an audio fallback.

What is different by design

  • Every request is challenged. There is no invisible mode to port across. Difficulty is chosen per request from the risk score, between levels 1 and 5, and the visitor sees a puzzle on the visual path. The exception is the non-visual path used by screen-reader and keyboard visitors, where there is no puzzle and the proof-of-work is the whole challenge.
  • No IP addresses are stored. Where Turnstile is fronted by Cloudflare's network, our scoring keeps only a daily-rotating HMAC of the address, with counters expiring after 24 hours.