How to Solve CAPTCHA in Postman and Poll for the Token

solve captcha in postman - How to Solve CAPTCHA in Postman and Poll for the Token

You can solve captcha in Postman without writing a line of application code. The API is 2captcha compatible and lives on your own machine, so it is two requests: POST the task to /in.php and keep the ID, then poll /res.php until the answer arrives. A short post-response script turns that second request into a loop, and a collection variable carries the ID between them. This guide has the exact fields, both scripts, and the one setting that stops the loop running forever.

What you need

  • The Postman desktop app. It talks to 127.0.0.1 directly with no agent involved. The web app is covered below and needs one extra piece
  • CapSkip running and reachable. Local mode listens on 127.0.0.1 port 8080 for a client on the same machine, and Server mode listens on your network or public IP so a laptop, a teammate or a hosted runner can reach it. Both are in the connection settings
  • A sitekey and a page URL to solve against

Nothing else. No SDK, no dependency, no account. Key validation is off by default, so any non-empty string works as the key value, and sending nothing at all returns ERROR_WRONG_USER_KEY rather than a solve.

Set up three collection variables first

Put these on the collection rather than in an environment. A collection variable travels with the export, so the whole thing still works when somebody else imports it.

VariableInitial valueWhy
baseUrlhttp://127.0.0.1:8080One place to change when the solver moves to a server
apiKeycapskipAny non-empty string. Validation is off by default
captchaIdemptyThe submit script writes it, the poll request reads it

Request 1: submit the task

A POST to {{baseUrl}}/in.php with a form body. Choose x-www-form-urlencoded, not raw JSON: the API reads form fields.

KeyValue
key{{apiKey}}
methoduserrecaptcha
googlekeyYOUR_SITEKEY
pageurlhttps://example.com/page-with-recaptcha
json1

The json=1 field matters more in Postman than it does at a terminal. Without it the reply is the plain string OK followed by a pipe and the ID, which you then have to split by hand. With it you get an object and the pretty printer works.

Add this to the Scripts tab, under Post-response. Older Postman versions call the same tab Tests.

// Post-response script on the submit request.
const body = pm.response.json();

pm.test('task accepted', function () {
    pm.expect(body.status).to.eql(1);
});

// Hand the ID to the polling request.
pm.collectionVariables.set('captchaId', body.request);

The response shape is the same for every CAPTCHA type. Status is 1 when the task was accepted, and the interesting value is always in the request field.

{"status": 1, "request": "2122988149"}

Watch the parameter name. reCAPTCHA takes googlekey and Turnstile takes sitekey, and sending the wrong one is the usual cause of an ERROR_GOOGLEKEY reply. The Turnstile solver page has the full field list for challenge pages, which also need the cData and chlPageData values scraped from the page.

The same request, for the other eight types

Only the submit request changes. Five method values cover every type CapSkip supports, and the variants are extra fields on the same form body rather than separate endpoints. Duplicate Request 1, apply the last column, and leave the rest of the request alone.

TypeSet method toThen change the body
Image, uploaded filemethod = postSwitch the body to form-data and attach the image as file
Image, base64method = base64Drop googlekey and pageurl, send body with the encoded image
reCAPTCHA v2 checkboxmethod = userrecaptchaNothing. This is the request shown above
reCAPTCHA v2 Invisiblemethod = userrecaptchaAdd invisible with the value 1
reCAPTCHA Enterprisemethod = userrecaptchaAdd enterprise with the value 1
reCAPTCHA v3method = userrecaptchaAdd version set to v3, and an action
Turnstile widgetmethod = turnstileRename googlekey to sitekey
Turnstile challenge pagemethod = turnstileRename googlekey to sitekey, then add data and pagedata
GeeTest v3method = geetestSend gt and challenge in place of googlekey

Two of those need care in Postman specifically. An uploaded image is the only one that cannot use x-www-form-urlencoded, because a file needs a multipart body, so switch that one request to form-data. And GeeTest runs on a deadline: its challenge value expires in about a minute, so fetch it immediately before you send rather than reusing one you saved earlier.

The polling request never changes. That is the argument for building this as a collection at all: one Request 2 serves every type on the list, because the answer always comes back in the same shape.

Request 2: poll until the answer arrives

A POST to {{baseUrl}}/res.php, using the same form body style.

KeyValue
key{{apiKey}}
actionget
id{{captchaId}}
json1

While the job is running this returns CAPCHA_NOT_READY, spelled exactly like that, missing letter included. It is a status and not an error, and the only correct response to it is to ask again.

// Post-response script on the polling request.
const body = pm.response.json();
const tries = Number(pm.collectionVariables.get('tries') || 0);

if (body.request === 'CAPCHA_NOT_READY' && tries < 20) {
    // Run this same request again.
    pm.collectionVariables.set('tries', tries + 1);
    pm.execution.setNextRequest(pm.info.requestId);
} else {
    pm.collectionVariables.set('captchaToken', body.request);
    pm.collectionVariables.set('tries', 0);
    pm.execution.setNextRequest(null);
}

Three things in that script are worth calling out, because each one is a way to lose an afternoon.

The loop only runs in the Collection Runner. Postman’s own documentation is explicit that setNextRequest has no effect when you send an individual request, so clicking Send on the polling request just sends it once and the script appears dead. Run the collection, the Postman CLI or Newman.

The counter is not optional. Without a ceiling, a task that never resolves loops until you stop the run by hand. Twenty tries at five seconds apart is over a hundred seconds of waiting, which comfortably clears the 15 to 20 seconds a reCAPTCHA v2 job needs.

Reference the request by ID. Passing pm.info.requestId points the loop at the currently running request, so renaming it later does not silently break the chain.

Give the runner a delay, or you will hammer the endpoint

The script above loops as fast as the runner can go, and polling a job that has been running for two seconds is wasted work. The Collection Runner has a Delay field in its run configuration, in milliseconds, applied before each request. Set it to 5000.

How long to wait before the first poll depends on what you submitted:

TypeNot ready before
Image1 second
reCAPTCHA v215 to 20 seconds
reCAPTCHA v310 to 15 seconds
GeeTest v3about 5 seconds

In Newman the same setting is a command line flag, so a collection that works in the app works unchanged in CI.

# npm install -g newman
newman run captcha.postman_collection.json --delay-request 5000

Reading the plain text reply instead

Drop json=1 and the body comes back as text, which is occasionally what you want. Two helpers cover it.

// Plain text mode: OK|2122988149
const id = pm.response.text().split('|')[1];

// Turnstile also returns the user agent, as a header.
const ua = pm.response.headers.get('X-Turnstile-User-Agent');

That header is not a curiosity. Cloudflare binds a Turnstile token to the browser fingerprint that produced it, so the token has to be submitted with the same user agent or the site rejects it while the token itself is perfectly valid.

One more rule that catches Postman users specifically, because the app makes it so easy to click Send twice: a result is readable only once. The second read of the same ID comes back empty, which looks exactly like a failed solve. Store the token in a variable on the first read.

The agent question, and pointing Postman at a server

If you use the Postman web app rather than the desktop app, requests go through an agent, and the choice of agent decides whether 127.0.0.1 is reachable at all. The Cloud Agent runs in Postman’s infrastructure and cannot reach anything on a private network. The Desktop Agent runs on your machine and can.

How you run PostmanCan it reach a local solver?
Desktop appYes, no agent needed
Web app with the Desktop AgentYes, the agent routes through your machine
Web app with the Cloud AgentNo, it cannot see a private network

Server mode changes that calculation. Run CapSkip on a box that listens on your network or a public IP, edit the baseUrl variable to point at it, and every request in the collection follows. Nothing else changes, because the only thing that moved is the address.

# The collection variable is the only edit.
baseUrl = http://YOUR_SERVER_IP:8080

A static public IP is recommended, and the steps are in the connection settings. Server mode is still your own hardware and still unmetered: it changes where the solver listens, not who owns it.

Common errors and what they mean

What you seeCauseFix
ERROR_WRONG_USER_KEYThe key field arrived empty because {{apiKey}} did not resolveDefine apiKey on the collection, not in an environment you have to remember to select
ERROR_WRONG_METHODA typo in method, or in the action valueBoth are form fields, not HTTP verbs: submit takes a method field, polling takes an action field set to get
ERROR_GOOGLEKEYA Turnstile sitekey was sent to userrecaptchaMatch the method to the field name
ERROR_PAGEURLThe page URL has no scheme, or was truncatedInclude https, and use a form body rather than a query string
Empty response bodyThat ID was already read onceStore the token on first read; resubmit for a new one
Could not send request, connection refusedNothing is listening at that addressStart the solver, or check the Desktop Agent is selected
The script runs but nothing loopsYou clicked Send instead of running the collectionsetNextRequest only works in a collection run

The exact wording of every code the API can return is in the API documentation.

Frequently asked questions

Can I do the whole thing in one request?

Technically yes, with pm.sendRequest in a pre-request script, and it is rarely worth it. The sandbox is built for short scripts, so a blocking poll makes the request look hung with no feedback while it waits. Two requests plus the runner shows you each attempt, which is the reason to be in Postman rather than in code.

Will this run in CI through Newman?

Yes. setNextRequest works in Newman and in the Postman CLI exactly as it does in the app, so an exported collection runs unchanged. The one thing to sort out is reachability: a hosted runner has its own loopback address, so the solver needs to be in Server mode at an address the runner can route to.

Is there a rate limit on how many I can queue?

No. Submit as many tasks as you like and poll each ID independently. The work happens on your own hardware rather than in somebody else’s shared queue, so the ceiling is how fast your machine gets through them, not a quota or a balance.

When to graduate out of Postman

Postman is the right tool for proving the API works, exploring a new CAPTCHA type, and handing a colleague something they can import and run. It is the wrong tool for production, mostly because of the polling: the runner waits out its fixed delay every single time, while the official SDKs start polling at 250 milliseconds and back off, so a fast solve returns fast. They also turn the error strings into typed exceptions.

For the same two calls in a shell script, see the cURL walkthrough. Either way the calls go to your own machine, which is what makes an unlimited captcha solver worth pointing a collection at: you can run the collection twenty times while you get the fields right, and it costs nothing but your own CPU.