Scarnox · Blog

Captcha token expired: causes and how to fix it

A captcha token is a short-lived proof that a challenge was passed. Scarnox's PopularCaptcha tokens expire about two minutes after they are issued. If you submit a token after that window, the site rejects the form, often with a message saying the captcha has expired or is invalid. The fix is almost always about timing: solve the captcha as close to the submit as you can, use each token once, and retry only when you know the first attempt was not accepted.

Why tokens expire

A token is not a permanent credential. The site wants to know that a person (or a solver acting for one) completed the challenge recently, so it gives the token a short life. The clock starts when the token is issued, not when you first call the API. Anything that happens between the solve and the submit eats into the two minutes.

Common causes

The fix: solve right before you submit

The simplest reliable pattern is to make the solve and the submit one step. Request a token, put it in the form, post the form, and do nothing slow in between:

import requests

API_KEY = "scarnox_YOURAPIKEY"
CREATE_URL = "https://api.scarnox.com/api/tasks/create"

def fresh_token(sitekey: str, page_url: str) -> str:
    r = requests.post(
        CREATE_URL,
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={
            "captcha_type": "PopularCaptchaTaskProxyless",
            "site_key": sitekey,
            "site_url": page_url,
        },
        timeout=185,  # the call blocks until the token is ready
    )
    data = r.json()
    if data.get("errorId") != 0:
        raise RuntimeError(f"{data.get('errorCode')}: {data.get('errorDescription')}")
    return data["solution"]["token"]

def submit_form(page_url: str, sitekey: str, form: dict) -> requests.Response:
    token = fresh_token(sitekey, page_url)  # solve right before posting
    return requests.post(
        page_url,
        data={**form, "h-captcha-response": token, "g-recaptcha-response": token},
        timeout=30,
    )

Notice that submit_form does not keep a token in a cache or a queue. It asks for one, posts it, and moves on. Failed solves are not charged, so a failed request costs you nothing on the solve side.

Retry safely

Some sites reject an expired token with a clear response. Others return a normal page with an error message inside it, so a status code alone will not tell you. Look at the response the site actually returns for an expired token and build your check around that. A retry loop could look like this:

def submit_with_retry(page_url: str, sitekey: str, form: dict, attempts: int = 2):
    for _ in range(attempts):
        resp = submit_form(page_url, sitekey, form)
        if resp.status_code < 400 and "captcha" not in resp.text.lower():
            return resp  # accepted, as far as this site's response shows
    raise RuntimeError("captcha token rejected after retries")

Be careful with retries on forms that create something. If the first attempt was actually accepted but your code misread the response, a retry can create a duplicate account, order or message. Only retry when you are sure the first post was not accepted, and make the check specific to the site you are working with.

Keep the timing honest

Common mistakes

Log enough to diagnose it

When a submit fails, record the time the token was issued, the time the form was posted, the HTTP status and the first few hundred characters of the response. With those four values you can tell at a glance whether the token aged out or the site rejected it for another reason. Do not log the full token in shared logs, since a valid token is a credential for that one challenge.

What to check when it still fails

  1. Print the time between the token being returned and the form post.
  2. Confirm the site_url matches the page that shows the captcha.
  3. Confirm the sitekey was read from the current page, not an old copy.
  4. Confirm the form field names. PopularCaptcha tokens go in h-captcha-response, and often g-recaptcha-response as well.

For the full list of error responses and how to handle each one, see captcha solver error handling.

Next steps