SurSolver

BlogVercel Security Checkpoint vs BotID and the _vcrcs cookie

Vercel Security Checkpoint vs BotID and the _vcrcs cookie

· Updated · 8 min read

Two Vercel features get called "the Vercel captcha", and only one of them is something a cookie can clear. By the end of this post you will be able to tell the Security Checkpoint from BotID, know why a plain HTTP script gets blocked on a Vercel-hosted site, and send a VercelToken task and replay its _vcrcs cookie.

Two protections, one platform

Security Checkpoint BotID
What the visitor sees An interstitial page that must be completed before the site loads Nothing; it is invisible
How it works A security challenge served in front of the site by Vercel Firewall / Attack Mode A client-side machine-learning challenge, powered by Kasada according to Vercel's documentation
Where the decision is made In front of the site, by Vercel Firewall On the site's own server, which calls checkBotId() for the routes it protects
Who switches it on The site owner, with Attack Mode or firewall custom rules The site owner, typically for high-value routes
Addressed by _vcrcs Yes No

Vercel documents BotID at a Basic level and a Deep Analysis level. Neither shows a challenge to the visitor, so there is nothing to see and nothing to click.

What scrapers actually hit

With Attack Mode on, Vercel challenges all traffic initiated by web browsers, API traffic included. Known bots such as search engines and webhooks pass through. Vercel also states that standalone APIs and other non-browser clients may not be able to pass the challenge and could be blocked.

That last sentence describes a plain requests or curl script exactly: a client that never runs the challenge is the kind of client Vercel warns about.

Site owners can also challenge arbitrary traffic with firewall custom rules, so a checkpoint can guard one route and not the next, and a URL that is challenged today may not be tomorrow. Always test the exact URL you plan to send.

Tell them apart

The sources behind this post do not document any reliable marker for the checkpoint page: no page title, HTML fragment or response header is published for it. The dependable test is the visible one.

  1. Open the exact URL in a normal browser through the proxy you plan to use.
  2. If an interstitial security page appears before the site content, you are facing the Security Checkpoint.
  3. If the page loads normally but your client is rejected on certain routes, BotID may be guarding those routes. What a rejected BotID request looks like on the wire is not documented in the sources, so confirm it in the Network panel rather than assuming.

Passing the checkpoint leaves a cookie named _vcrcs on the protected origin. Its value is JWT-shaped, a long string that begins eyJ. It is replayed as a cookie, not typed into a form.

What nobody documents: how long it lives, whether it covers one route or the whole origin, and whether it is bound to the IP or user agent that earned it. Do not plan around a fixed lifetime.

Which result key holds _vcrcs

A SurSolver VercelToken result has exactly three keys: token, cookies and userAgent. Neither the catalog nor the research behind this post says which of them carries the _vcrcs value. It is not guessed here.

The helper below looks for _vcrcs inside cookies, accepting either an object keyed by cookie name or a list of {name, value} entries. If it is not there, it stops and tells you the shape of every key, so you can see where the value actually is after your first solve and then hardcode that one location.

websiteURL (the exact page that shows the checkpoint) and proxy are required; userAgent is an optional parameter, and the docs do not say whether the solve uses it, so do not count on it shaping the result. A four-part proxy string is read as user:password:host:port; see Proxies for all accepted formats.

Poll getTaskResult every 3 seconds. API errors come back as HTTP 200 with errorId: 1, and an unexpected fault is an HTTP 500 without errorId. The docs set no polling ceiling, so the script picks its own.

solve.py
import os
import time
from urllib.parse import urlparse

import requests

API = 'https://api.sursolver.com'
API_KEY = os.environ['SURSOLVER_API_KEY']
POLL_SECONDS = 3
DEADLINE_SECONDS = 180
REQUEST_TIMEOUT = 30
COOKIE_NAME = '_vcrcs'
PREVIEW_CHARS = 12

TARGET_URL = 'https://www.example.com/'
USER_AGENT = (
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
    '(KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36'
)
PROXY = 'http://user:password@203.0.113.10:8000'


def call(path, body):
    response = requests.post(f'{API}{path}', json=body, timeout=REQUEST_TIMEOUT)
    response.raise_for_status()  # HTTP 500 carries no errorId
    data = response.json()
    if data.get('errorId'):
        raise RuntimeError(f"{data['errorCode']}: {data.get('errorDescription')}")
    return data


def solve_vercel():
    created = call('/createTask', {
        'clientKey': API_KEY,
        'task': {
            'type': 'VercelToken',
            'websiteURL': TARGET_URL,
            'userAgent': USER_AGENT,
            'proxy': PROXY,
        },
    })
    task_id = created['taskId']
    deadline = time.monotonic() + DEADLINE_SECONDS
    while time.monotonic() < deadline:
        time.sleep(POLL_SECONDS)
        result = call('/getTaskResult', {'clientKey': API_KEY, 'taskId': task_id})
        if result['status'] == 'ready':
            return result['solution']
    raise TimeoutError(f'task {task_id} still processing after {DEADLINE_SECONDS}s')


def find_vcrcs(solution):
    cookies = solution.get('cookies')
    if isinstance(cookies, dict) and cookies.get(COOKIE_NAME):
        return cookies[COOKIE_NAME]
    if isinstance(cookies, list):
        for item in cookies:
            if isinstance(item, dict) and item.get('name') == COOKIE_NAME:
                return item['value']
    shapes = {
        key: f'{type(value).__name__}: {str(value)[:PREVIEW_CHARS]}'
        for key, value in solution.items()
    }
    raise LookupError(f'{COOKIE_NAME} not found under cookies; solution is {shapes}')


solution = solve_vercel()
vcrcs = find_vcrcs(solution)

session = requests.Session()
session.proxies = {'http': PROXY, 'https': PROXY}
session.headers['User-Agent'] = solution.get('userAgent', USER_AGENT)
session.cookies.set(COOKIE_NAME, vcrcs, domain=urlparse(TARGET_URL).hostname)

response = session.get(TARGET_URL, timeout=REQUEST_TIMEOUT)
print(response.status_code)

userAgent in the result is conditional. When it is present, replay with it; when it is absent, the result does not say which browser the solve presented, so the script keeps your own client's user agent and you should confirm the first replay works before relying on it. Replaying through the same proxy is the cautious choice: the binding is undocumented, so keep it identical until you have measured otherwise.

If your own deadline expires, the task is still charged and still readable for the retention window (30 minutes from creation by default); fetch it later with the same taskId rather than creating a new one.

What the sources do not cover

Little about this protection is documented from the solving side, so several practical answers are simply unknown. Plan for them instead of filling the gaps with folklore:

  • the lifetime of _vcrcs, and whether it covers one route or the whole origin;
  • whether the cookie is bound to the IP or user agent that earned it;
  • which of token, cookies and userAgent carries the value in a SurSolver result;
  • what a request rejected by BotID looks like, and whether clearing the checkpoint is enough on a route that BotID also guards;
  • how the API reports a task for a URL that shows no checkpoint at all, since no error code specific to this type is documented.

The last point is why the browser check comes first: a task is charged when it is accepted, so measure on a handful of real URLs before you scale up.

Common failures

  • No checkpoint at that URL. The task is charged when it is accepted and a task that finishes as failed automatically returns its original charge once, so confirm the interstitial in a browser first.
  • No proxy. This type has no proxyless form; every task needs proxy details.
  • Treating BotID like the checkpoint. A _vcrcs cookie addresses only the visible interstitial. If a route is also gated by BotID, clearing the checkpoint may not be enough, and the sources do not say whether it ever is.
  • Replaying from somewhere else. A different network or user agent than the solve used is the first thing to rule out when a fresh solve is refused.
  • Reusing the cookie after it stops working. With the lifetime undocumented, when the checkpoint reappears, run a new solve rather than retrying the old value.
  • Expecting a bare script to pass. Vercel says non-browser clients may be blocked under Attack Mode. Solving the challenge and replaying _vcrcs is the documented route; the sources do not promise that a bare script then passes every request.

Next steps

Confirm the checkpoint in a browser, then send a VercelToken task with websiteURL, proxy and optionally userAgent, and look at which result key holds _vcrcs on your first solve. The Vercel guide covers recognition and parameters, and the public catalog (GET /v1/task-types) lists the accepted names and result keys. For the proxy and header side of replaying cookies, read proxies for captcha solving and browser headers and user agents.

More articles