Troubleshooting

Cloudflare Turnstile 403 After Token Submission: Fix Guide

You solved the Turnstile CAPTCHA and got a valid token, but the target site still returns 403 Forbidden. This guide covers every cause and gives you a decision flow to identify which one applies to your integration in under five minutes.


30-second diagnostic flow

flowchart TD
    A[Got 403 after token submit] --> B{Does the response include<br/>Server: cloudflare?}
    B -- No --> C[Application-level rejection.<br/>Check the form field name.]
    B -- Yes --> D{Is there a cf_clearance cookie<br/>in the response Set-Cookie?}
    D -- Yes, but next request 403 --> E[Session reuse problem.<br/>Use the same Session for solve + submit.]
    D -- No, and body shows Turnstile widget again --> F{Solve-to-submit elapsed time?}
    F -- "> 240s" --> G[Token expired.<br/>Re-solve and submit within 240s.]
    F -- "< 240s" --> H{Is the IP / User-Agent the same<br/>between solve and submit?}
    H -- No --> I[IP or UA mismatch.<br/>Pin both end-to-end.]
    H -- Yes --> J{Is the page a full interstitial<br/>(no form, just a spinner)?}
    J -- Yes --> K[This is Cloudflare Challenge,<br/>not Turnstile. Use cloudflare_challenge method.]
    J -- No --> L[Header / Origin mismatch.<br/>Replay browser headers exactly.]

The five most common branches map 1:1 to the five causes below.


Why 403 After Valid Token

Cause Likelihood
Missing cf_clearance cookie Very common
Token expired Common
Wrong submission endpoint Common
Missing request headers Moderate
IP mismatch between solve and submit Moderate
Cloudflare Challenge (not Turnstile) Sometimes confused

Turnstile sets cookies during validation. If you don't include these cookies in your subsequent request, Cloudflare blocks you.

import requests

session = requests.Session()

# Step 1: Load the page to get initial cookies
session.get("https://example.com")

# Step 2: Solve Turnstile
token = solve_turnstile(
    api_key="YOUR_API_KEY",
    sitekey="TURNSTILE_SITEKEY",
    pageurl="https://example.com",
)

# Step 3: Submit token to the validation endpoint
# This sets cf_clearance cookie
resp = session.post("https://example.com/api/verify", data={
    "cf-turnstile-response": token,
}, headers={
    "Content-Type": "application/x-www-form-urlencoded",
    "Origin": "https://example.com",
    "Referer": "https://example.com/",
})

# Step 4: Now make your actual request WITH the session cookies
resp = session.get("https://example.com/protected-page")
print(resp.status_code)  # Should be 200 now

Cause 2: Token Expired

Turnstile tokens last ~300 seconds, but use them immediately for best results.

import time

# Solve
start = time.time()
token = solve_turnstile(...)
solve_time = time.time() - start

# Check if token is still fresh
if solve_time > 240:  # > 4 minutes is risky
    print("Token may be too old, solving again...")
    token = solve_turnstile(...)

# Submit immediately
submit_token(token)

Cause 3: Wrong Submission Method

Find how the site submits the Turnstile token:

# Some sites use a hidden form field
data = {
    "cf-turnstile-response": token,
    "username": "user",
    "password": "pass",
}

# Some sites use a custom header
headers = {
    "X-Turnstile-Token": token,
}

# Some sites use JSON body
json_data = {
    "turnstileToken": token,
    "email": "user@example.com",
}

How to find the correct field name:

  1. Open browser DevTools → Network tab
  2. Complete the Turnstile challenge manually
  3. Find the form submission request
  4. Look at the request body for the token field name

Cause 4: Missing Headers

Cloudflare checks request headers for consistency:

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
    "Origin": "https://example.com",
    "Referer": "https://example.com/login",
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "same-origin",
}

session.headers.update(headers)

Cause 5: Cloudflare Challenge vs Turnstile

Turnstile and Cloudflare Challenge are different systems:

Feature Turnstile Cloudflare Challenge
Widget Visible checkbox on page Full-page challenge screen
CaptchaAI method turnstile cloudflare_challenge
Token field cf-turnstile-response N/A (cookie-based)

If you see a full-page challenge, use method=cloudflare_challenge instead.


Complete Working Example

import requests
import time
import re


def solve_turnstile_and_access(target_url, api_key):
    """Complete flow: solve Turnstile and access protected page."""
    session = requests.Session()
    session.headers.update({
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    })

    # Load page, get cookies and sitekey
    resp = session.get(target_url)
    match = re.search(r'data-sitekey="([^"]+)"', resp.text)
    if not match:
        raise RuntimeError("Turnstile sitekey not found")

    sitekey = match.group(1)

    # Solve via CaptchaAI
    submit_resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": api_key,
        "method": "turnstile",
        "sitekey": sitekey,
        "pageurl": target_url,
        "json": 1,
    }, timeout=30)
    task_id = submit_resp.json()["request"]

    # Poll
    for _ in range(12):
        time.sleep(5)
        poll = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": api_key, "action": "get",
            "id": task_id, "json": 1,
        }, timeout=15)
        data = poll.json()
        if data.get("status") == 1:
            token = data["request"]
            break
    else:
        raise TimeoutError("Solve timeout")

    # Submit token using the same session
    form_resp = session.post(target_url, data={
        "cf-turnstile-response": token,
    }, headers={
        "Origin": f"https://{requests.utils.urlparse(target_url).netloc}",
        "Referer": target_url,
    })

    return session, form_resp


# Usage
session, resp = solve_turnstile_and_access(
    "https://example.com/login",
    "YOUR_API_KEY",
)
# session now has valid cookies for subsequent requests

Troubleshooting

Issue Cause Fix
403 despite valid token Missing session cookies Use same session for all requests
403 on subsequent pages cf_clearance not set Token validation must return cookie
Works once, then 403 Cookie expired Re-solve for fresh cookie
Always 403 Full-page challenge, not Turnstile Use cloudflare_challenge method

FAQ

How long does cf_clearance last?

Typically 30 minutes to 24 hours. If subsequent requests start failing, re-solve the Turnstile.

Do I need a proxy for Turnstile?

Often no — CaptchaAI's consistently high success rate on Turnstile usually works without proxies. Add a proxy only if the site checks IP consistency.

Can I pass cf_clearance to another session?

Yes, but it's tied to the User-Agent and may be tied to the IP. Keep both consistent.


Real support case: 403 only on the second request

Symptom: A scraping pipeline submits the Turnstile token, gets 200 OK on the form post, and the response body shows the protected page. Two minutes later the same session re-requests /api/orders and gets 403. The team blamed token expiration but the token had been thrown away after the first submit.

Root cause: The HTTP client was created per-request inside a helper, not reused. Every new request opened a fresh TCP connection with an empty cookie jar — cf_clearance set on the first response never made it to the second request.

Fix: Hoist requests.Session() out of the helper and pass it in. The same session must be used for every subsequent call against the protected origin. The token itself does not need to be replayed — only the cookie does.

# Wrong
def fetch(url):
    return requests.get(url)  # new session every time

# Right
session = requests.Session()
solve_and_submit(session, "https://example.com")
fetch_protected(session, "https://example.com/api/orders")

Lesson: After a successful Turnstile submit, treat the requests.Session (or Playwright BrowserContext) as the long-lived credential. The token is single-use; the resulting cookies are what carry you forward.


Pre-flight checklist before you blame the solver

Run through this list before opening a support ticket — eight out of ten reported 403s are caught here:

  • [ ] Same User-Agent string used during solve and submit
  • [ ] Same outbound IP used during solve and submit (or proxy pinned at session level)
  • [ ] Origin and Referer headers match the target page URL
  • [ ] Token submitted within 240 seconds of receiving it
  • [ ] Token submitted exactly once (Turnstile tokens are single-use)
  • [ ] Form field name verified in DevTools (cf-turnstile-response vs custom name)
  • [ ] requests.Session() (or persistent browser context) reused across solve → submit → follow-up
  • [ ] data-sitekey re-extracted on each run (sitekeys can rotate)
  • [ ] Verified the page actually serves Turnstile and not a full Cloudflare Challenge interstitial
  • [ ] Server responded with Set-Cookie: cf_clearance=... after submit

Part of the Turnstile Mastery series — nine practical guides covering Turnstile end-to-end:

  1. Cloudflare Turnstile Widget Modes Explained
  2. Cloudflare Turnstile Implementation Detection
  3. Cloudflare Turnstile Sitekey Extraction
  4. Cloudflare Turnstile Interception Methods
  5. Cloudflare Turnstile CaptchaAI Solving Guide
  6. Cloudflare Turnstile Token Handoff with CaptchaAI
  7. Cloudflare Turnstile Token Expiration and Timing
  8. Cloudflare Turnstile 403 After Token Fix — you are here
  9. Cloudflare Turnstile Errors and Troubleshooting
Comments are disabled for this article.