The token returned by CaptchaAI is short-lived and single-use. Most "the token is valid but my page rejected it" problems are caused by breaking one of the four constraints below.
When you call submit_turnstile or submit_recaptcha_v2, you tell
CaptchaAI:
sitekey— the public site key from the widget's HTMLpageurl— the page where that widget renders
The solver computes a token that is only valid for that exact pair, as Cloudflare/Google's verification will check both.
Common mistakes:
- Using
https://example.com/when the widget is onhttps://example.com/login. Use the canonical URL the widget actually loads on, not the homepage. - Using a sitekey scraped from a different environment (staging vs. prod often have different keys).
- Trailing-slash mismatch — try with and without.
Both Cloudflare Turnstile and Google reCAPTCHA tokens expire in ~120 seconds from issue. If you:
- batch tokens
- queue a token while polling another captcha
- pause the workflow at a breakpoint
- retry after a long wait
… the token will silently fail validation. Doctor measures the end-to-end time and includes it in the report so you can spot this.
The widget injects a hidden response field next to the challenge:
| Type | Field name |
|---|---|
| Turnstile | cf-turnstile-response |
| reCAPTCHA v2 | g-recaptcha-response |
You must write the token into that field before submitting the form.
Doctor's injector writes the value via eval_on_selector (so hidden
fields work) and dispatches both input and change events so any
React/Vue listeners notice.
Some integrations gate the submit button on a JS callback the widget
invokes when it has a token (e.g. data-callback="onTurnstileSuccess").
Just writing into the response field does not invoke that callback.
Doctor's injector.invoke_callback calls the named function with the
token as the argument. The list of names to try is configurable per
profile (detection.callback_candidates).
If steps 1-4 are all correct and the page still rejects the workflow,
the token reached your server but server-side siteverify rejected it.
That's almost always one of:
- Your server is using a different secret key than the sitekey was issued for.
- Your server passed a stale or wrong remoteip.
- The token was reused (already redeemed).
- Network/clock skew >>5 seconds.
Doctor cannot see your server, but the report's screenshot and the verifier output usually tells you which side rejected the token.