Token recusado quase nunca é token defeituoso. O cf-turnstile-response devolvido pela CaptchaAI costuma estar íntegro — o que quebrou é o contexto em volta dele: o relógio, a sitekey, o nome do campo no POST, a sessão ou os parâmetros action/cData declarados pelo widget.
São seis causas, e checá-las em ordem economiza tempo: comece pelo relógio, termine nos parâmetros opcionais. Os exemplos rodam contra staging.example.com — diagnosticar Turnstile é trabalho de QA autorizado sobre a sua própria aplicação.
Tabela de sintomas: do que você vê à causa provável
| O que você vê | Causa provável |
|---|---|
| 403 logo depois de enviar o token | O token expirou antes do envio |
| O formulário volta em silêncio, sem mensagem de erro | Nome de campo errado no POST |
| O token é aceito, mas a ação continua bloqueada | Sitekey incompatível — foi resolvido outro widget |
| Funciona na primeira vez e falha na repetição | Token já consumido (uso único) |
| Funciona no navegador e falha no script | Sessão ou cookies diferentes |
Sintoma fora da tabela? Percorra as seis causas na ordem: elas vão da mais provável para a mais rara.
Causa 1: o token expirou antes de chegar ao POST
Campeã absoluta de chamados. O token do Turnstile vive pouco — normalmente 300 segundos (5 minutos), e algumas implementações encurtam ainda mais essa janela. Um login intermediário, uma espera de fila ou um sleep generoso já bastam para ele chegar morto ao destino.
Cenário típico em times brasileiros: o worker roda em sa-east-1, o envio fica agendado numa fila e o intervalo entre resolver e postar passa de dez minutos. O log parece impecável (a API retornou status: 1), mas o site já tinha descartado o token.
Correção: use o token no mesmo bloco de código que o recebeu, sem fila intermediária e sem cache.
import requests
import time
API_KEY = "YOUR_API_KEY"
# Submit Turnstile task
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "turnstile",
"sitekey": "0x4AAAAAAADnPIDROz1234",
"pageurl": "https://staging.example.com/qa-login",
"json": 1
}).json()
task_id = submit["request"]
time.sleep(10)
# Poll for result
for _ in range(24):
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": task_id, "json": 1
}).json()
if result.get("status") == 1:
token = result["request"]
# USE TOKEN IMMEDIATELY — do not delay
response = requests.post("https://staging.example.com/qa-login", data={
"username": "user",
"password": "pass",
"cf-turnstile-response": token
})
break
time.sleep(5)
Como medir o intervalo real
Registre o timestamp da resposta da API e o do POST e acompanhe a mediana. Passando de 60 s, o problema está no desenho do fluxo, não na resolução.
Causa 2: a sitekey resolvida não é a do formulário
Cada widget do Turnstile tem a sua própria sitekey (a chave pública do widget). Páginas que juntam login e newsletter costumam trazer duas — e o token de uma delas é válido, só que para o widget errado.
Confirme a sitekey correta direto no console do navegador:
// In browser console on the target page
document.querySelectorAll('[data-sitekey]').forEach(el => {
console.log('Sitekey:', el.getAttribute('data-sitekey'));
console.log('Element:', el);
});
Havendo mais de uma, use a que está dentro do mesmo <form> que você envia. Se o valor vem de variável de ambiente, confira também se a sitekey de staging não vazou para produção: é um erro silencioso e frequente em pipelines de deploy.
Causa 3: o campo do POST não se chama cf-turnstile-response
O Turnstile espera o token em cf-turnstile-response. Reaproveitar um script de reCAPTCHA sem trocar o nome do campo produz a "falha silenciosa" da tabela: o servidor não recebe token, trata o envio como não verificado e devolve o formulário sem explicação.
# WRONG — this is for reCAPTCHA
data = {"g-recaptcha-response": token}
# CORRECT — this is for Turnstile
data = {"cf-turnstile-response": token}
Alguns sites renomeiam o campo. Antes de supor, veja o que o widget realmente preenche:
// Check what field the Turnstile widget populates
document.querySelector('[name*="turnstile"], [name*="cf-"]')
Causa 4: a sessão do envio não é a que carregou a página
A Cloudflare pode validar o token contra os cookies da sessão. Carregar a página em um contexto e enviar o formulário em outro — resolver no navegador e postar com requests — torna a recusa o comportamento esperado, não um defeito.
# Use the SAME session for page load and token submission
session = requests.Session()
# Load the page first to establish cookies
session.get("https://staging.example.com/qa-login")
# Then solve and submit using the same session
token = solve_turnstile(sitekey, pageurl)
session.post("https://staging.example.com/qa-login", data={
"cf-turnstile-response": token
})
É esse o motivo por trás do clássico "no navegador funciona, no script não": o token não mudou, mudou o contexto em volta dele.
Causa 5: o token já foi consumido
Tokens do Turnstile são de uso único. Se a camada de retentativa reenvia a mesma requisição — inclusive pelo backoff automático de uma biblioteca HTTP —, a primeira tentativa consome o token e as seguintes falham.
A correção é estrutural: a chamada de resolução fica dentro do laço de retentativa, nunca antes dele. Na revisão de código, procure três sinais:
- um token guardado em variável declarada fora do laço;
- um
Retryconfigurado na sessão HTTP que reenvia o mesmo corpo; - qualquer cache de token, mesmo com TTL curto.
Causa 6: action e cData ficaram de fora da chamada
Algumas integrações amarram os parâmetros action ou cData ao token. O widget envia esses valores; se a sua chamada à API não envia, o token gerado não corresponde ao que o servidor vai validar.
Procure data-action e data-cdata no elemento do widget e replique os valores na submissão:
submit = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": "turnstile",
"sitekey": "0x4AAAAAAADnPIDROz1234",
"pageurl": "https://staging.example.com/qa-login",
"action": "login", # If required by the site
"data": "custom_cdata_value", # If required by the site
"json": 1
}).json()
Árvore de decisão: a sequência em um minuto
Token solved but rejected
↓
Used within 5 minutes? → No → Solve again, submit immediately
↓ Yes
Correct sitekey? → No → Find the correct sitekey from the page
↓ Yes
Using cf-turnstile-response field? → No → Change field name
↓ Yes
Same session for page load + submit? → No → Use session persistence
↓ Yes
Token used only once? → No → Solve a new token per submission
↓ Yes
Site requires action/cData? → Check page source, add to API call
Chegou ao fim sem resolver? O problema deixou de ser o token e passou a ser a validação do lado do servidor.
Três hábitos que evitam a reincidência
- Meça o intervalo entre resolver e enviar. É o indicador que antecipa a Causa 1 antes de virar chamado.
- Fixe a sitekey por ambiente. Uma constante validada na inicialização elimina a Causa 2.
- Trate o token como descartável. Um token por envio: nada de cache, nada de reaproveitamento.
Sobre capacidade: a CaptchaAI cobra por thread concorrente, não por resolução, e cada plano inclui resoluções ilimitadas por thread — do BASIC (US$ 15/mês, 5 threads) ao VIP-3 (US$ 7.500/mês, 5.000 threads). Separe os dois problemas: thread resolve gargalo de concorrência; para token recusado, trocar de plano não muda nada.
Perguntas frequentes
Posso reaproveitar o mesmo token em uma nova tentativa?
Não. O token é de uso único: a primeira requisição o consome e a segunda é recusada, mesmo um milissegundo depois. Resolva um token por envio.
Um 403 sempre significa token expirado?
Não. Ele aparece na expiração, na sitekey incompatível e na sessão divergente. Use a idade do token como filtro: com menos de um minuto de vida, descarte a Causa 1 e vá direto para a sitekey.
Como sei se o problema é o token ou a validação do servidor?
Faça um envio isolado: um token recém-resolvido, os campos obrigatórios e nada mais, na mesma sessão que carregou a página. Se ele passa, a falha está no seu fluxo; se também é recusado, investigue a validação do lado do servidor.
Preciso enviar action e cData em toda integração?
Não. Só quando o widget declara esses atributos. Inspecione o elemento do Turnstile: sem data-action e sem data-cdata, enviá-los por precaução pode quebrar a validação.
Esses exemplos valem para testes com dados reais sob a LGPD?
Os exemplos usam dados fictícios em staging.example.com. Ao levar o fluxo para um ambiente com dados reais, considere as obrigações da LGPD quanto a registro e retenção de logs — para diagnosticar, timestamp e status costumam bastar, sem armazenar o token.
Resolva Cloudflare Turnstile com a CaptchaAI
Crie sua conta em captchaai.com e valide o primeiro token de Turnstile no seu ambiente de staging.