How to Solve CAPTCHAs in a Pipedream Code Step (Node.js)

pipedream captcha - How to Solve CAPTCHAs in a Pipedream Code Step (Node.js)

A Pipedream captcha solve is easier than the same job in Zapier or Make.com, because a code step is a real Node.js v20 runtime with npm imports. You install nothing, you write ordinary JavaScript, and the CapSkip SDK works exactly as it does on your laptop. Two things are different, and both are about where the code runs. Pipedream executes in its own cloud, so the solver has to be reachable from the internet. And a workflow execution has a time limit that is shorter than a reCAPTCHA solve, which decides whether you write one step or two.

What you need

  • A Pipedream workflow with a code step. Node.js is the runtime used below. There is a Python version further down.
  • CapSkip running in Server mode, on a machine reachable from the internet, with a static public IP.
  • The sitekey and the page URL of the site you are automating.
  • Two Pipedream environment variables holding the solver’s address and its key.

Why the loopback address cannot work here

Pipedream workflows run on Pipedream’s own infrastructure, in the AWS us-east-1 network. A request to 127.0.0.1 from inside a code step resolves to the container the step is running in, not to your desk. Nothing is listening there, and the SDK raises a NetworkException.

CapSkip has two connection modes and the second one is the answer. Local binds to 127.0.0.1 and serves that device only, which is right when the automation and the solver share a machine. Server binds to your network address or public IP, so a hosted platform can reach the same Windows machine over the API. A static public IP is recommended, because a residential address that rotates will break the workflow at three in the morning without telling you. Both modes are set up under connection settings.

Server mode changes where the solver runs and nothing else. It is still your hardware and it is still unmetered, so a workflow that fires ten thousand times a month costs the same as one that fires ten.

Step 1: put the address in an environment variable

Do not hardcode the public IP in the step. Pipedream has workspace environment variables, and a code step reads them from the ordinary process environment. Create two.

Variable nameValue
The one named CAPSKIP_HOSTYour solver machine’s public IP, with no scheme and no port
The one named CAPSKIP_API_KEYThe key you generated in the CapSkip app for this workflow

Give this workflow its own key rather than sharing one. Revoking a key that leaked into a shared workspace should not take the rest of your automation down with it.

Step 2: the whole solve in one code step

Pipedream installs an npm package the moment you import it, so there is no install step and no package file. The import specifier is also where you pin a version, which is worth doing on anything that runs unattended.

// npm install capskip - Pipedream installs it from this import.
// The package is CommonJS, so take the default and destructure.
import capskip from "capskip";

const { CapSkip } = capskip;

export default defineComponent({
  async run({ steps, $ }) {
    const solver = new CapSkip({
      host: process.env.CAPSKIP_HOST,
      port: 8080,
      apiKey: process.env.CAPSKIP_API_KEY,
    });

    const result = await solver.recaptcha(
      "YOUR_SITEKEY",
      "https://example.com/page-with-recaptcha"
    );

    return result.code;   // the token, for the next step
  },
});

That is the whole thing. Every reCAPTCHA variant is the same method with an options object: invisible set to 1, enterprise set to 1, or version set to v3 with an action name. Turnstile and GeeTest have their own methods with the same shape, and the full parameter list is in the CapSkip API documentation.

Whatever the step returns lands in the workflow’s exports, so a later step reads the token from the step’s own name. If you would rather give it a label, use the export helper.

// A named export reads better downstream than a bare return.
$.export("token", result.code);

// The next step then reads steps.solve_captcha.token

The execution timeout, and when one step stops being enough

Here is the constraint that shapes everything else. A Pipedream execution has a default time limit of 30 seconds for HTTP and email triggers, and 60 seconds for cron triggers. You can raise it in the workflow settings, up to 300 seconds on the free tiers and 750 seconds on paid ones.

A reCAPTCHA v2 solve usually lands well inside 30 seconds, but usually is not always, and the SDK’s own ceiling is recaptchaTimeout at 300 seconds. So the one-step version above is exactly as reliable as your timeout setting. If the workflow limit is lower than the solve, the execution is killed halfway through the poll and you get a failed run with nothing useful in it.

Your situationWhat to do
Low volume, and you can raise the execution limit to 300 secondsKeep the one-step version. Raise the timeout in workflow settings
High volume, or you are paying for execution timeSplit it, and use the rerun helper described below
Turnstile challenge pages or GeeTest, which take longerSplit it. These are the types most likely to outrun a short limit

Step 3: split it with the rerun helper

Pipedream has a polling primitive that most automation platforms lack. The flow rerun helper ends the current step, waits, and runs that same step again with a piece of state you hand it. The workflow is not executing while it waits, so a slow solve costs you nothing and cannot hit the limit.

Three things make it work. The run counter starts at 1 and increments on each rerun. The context object you pass is readable on the next pass. And exceeding the retry ceiling moves the workflow on to the next step rather than failing, so throw if that is not what you want.

// No SDK here. The raw endpoints suit a step that exits
// between polls, because nothing has to stay in memory.
const MAX_RETRIES = 20;
const DELAY = 15000;   // 15s, the recommended first wait for v2

export default defineComponent({
  async run({ steps, $ }) {
    const { run } = $.context;
    const base = `http://${process.env.CAPSKIP_HOST}:8080`;
    const key = process.env.CAPSKIP_API_KEY;

    if (run.runs === 1) {
      const params = new URLSearchParams({
        key,
        method: "userrecaptcha",
        googlekey: "YOUR_SITEKEY",
        pageurl: "https://example.com/page-with-recaptcha",
        json: "1",
      });
      const submitted = await fetch(`${base}/in.php?${params}`);
      const { request: id } = await submitted.json();

      // The id survives into the next run through the context.
      return $.flow.rerun(DELAY, { id }, MAX_RETRIES);
    }

    const { id } = $.context.run.context;
    const polled = await fetch(
      `${base}/res.php?key=${key}&action=get&id=${id}&json=1`
    );
    const data = await polled.json();

    if (data.request !== "CAPCHA_NOT_READY") {
      return data.request;   // the token
    }
    if (run.runs === MAX_RETRIES + 1) {
      throw new Error("Solve did not finish in time");
    }
    return $.flow.rerun(DELAY, { id }, MAX_RETRIES);
  },
});

The spelling of that pending response catches people out. It is CAPCHA_NOT_READY, missing a T, and it is not an error: it means the answer is not finished yet and you should poll again. Treating it as a failure is the most common bug in a hand-written polling loop, and there is a full write-up of the CAPCHA_NOT_READY response.

One more thing about that endpoint. A result is readable exactly once. If you log the response body and then read it again in a later step, the second read comes back empty and looks like the solve failed.

Send the token straight away

A reCAPTCHA token is good for about two minutes. In a workflow that is easier to lose than it sounds, because a delay step, a slow HTTP call or a rerun that waited too long all eat into the same budget. Put the step that submits the token immediately after the step that produced it, and do not stage tokens for later. The full picture is in the guide to reCAPTCHA token expiration.

The Python version

Pipedream also runs Python 3.12 code steps, and it installs pip packages from your imports the same way. The SDK does not read environment variables by itself, so the step below reads CAPSKIP_HOST, CAPSKIP_PORT and CAPSKIP_API_KEY and passes them to the constructor.

# pip install capskip - Pipedream installs it from this import
import os
from capskip import CapSkip

def handler(pd: "pipedream"):
    # Your Pipedream environment variables. The SDK does not read
    # them by itself, so pass them to the constructor.
    solver = CapSkip(
        host=os.environ["CAPSKIP_HOST"],
        port=int(os.environ["CAPSKIP_PORT"]),
        apiKey=os.environ["CAPSKIP_API_KEY"],
    )

    page_url = pd.steps["trigger"]["event"]["body"]["url"]
    result = solver.recaptcha(sitekey="YOUR_SITEKEY", url=page_url)

    # Downstream steps read pd.steps["solve"]["token"]
    return {"token": result["code"]}

Set CAPSKIP_PORT to 8080 alongside the other two if you go this route. One limitation to know before you commit: the rerun and delay helpers are documented for Node.js, so a Python step is the one-shot version only. If you need the split polling shape, write that one step in Node.

Locking down the port you just opened

Server mode puts a listener on the public internet, so treat it like any other exposed service. Three things are worth doing on day one.

  • Turn on API key validation in the CapSkip app and give this workflow its own key. Without it any string is accepted as a key.
  • Put the solver behind a firewall rule rather than leaving 8080 open to everything.
  • Decide how you will express that rule. Pipedream’s ordinary outbound traffic comes from the standard AWS us-east-1 ranges, which are far too broad to allowlist usefully. If you need a narrow rule, Pipedream offers a VPC with a dedicated static outbound IP per workspace, and that is the address you allowlist.

Common errors and what they mean

What you seeCauseFix
NetworkException, or a connection refused on port 8080The solver is in Local mode, or the host variable is wrongSwitch to Server mode and set the host variable to the public IP
The execution is killed partway through the solveThe workflow time limit is shorter than the solve tookRaise it in workflow settings, or move to the rerun version
The step returns the literal pending response as its resultThe polling branch treated CAPCHA_NOT_READY as an answerTest for it explicitly and rerun rather than returning it
ERROR_WRONG_USER_KEY from the raw endpointThe key variable is empty, so an empty string was sentCheck the environment variable name, including its case
The token is empty on the second readA result can be read only onceRead it once, keep it in a variable, and pass that along
A valid token is rejected by the target siteIt expired between the solve and the submitMove the submit step directly after the solve step
Cannot use import statement outside a moduleThe step mixes import and requirePick one style per step. Code steps are ES modules

FAQ

Can I use CapSkip from Pipedream without exposing anything?

Not directly, because Pipedream’s compute is Pipedream’s. Something has to accept an inbound connection. Server mode with key validation and a firewall rule is the straightforward answer. If your policy forbids an open port entirely, the alternative is to keep the CAPTCHA work on a machine you control and have Pipedream call that machine’s own endpoint instead, which moves the exposure rather than removing it.

Does a rerun count as a separate execution?

The step runs again, so the code executes more than once, which is exactly why the counter exists. The point of the helper is that the workflow is not held open across the wait, so a slow solve does not push a single execution past the time limit. Check your plan’s own accounting if billing matters, but the timeout problem is solved either way.

Should I use the SDK or the raw endpoints?

The SDK when one step does the whole job, because it handles the polling, backs off from 250 milliseconds rather than sleeping a flat interval, and gives you typed errors. The raw endpoints when you split across reruns, because the step exits between polls and there is no client left in memory to hold the task. Both talk to the same service on the same port.

How is this different from doing it in n8n or Zapier?

Mostly the runtime. Pipedream gives you real Node.js with npm imports, so the SDK drops straight in, and the rerun helper handles slow solves cleanly. Zapier’s code step cannot install packages, so the Zapier CAPTCHA guide is built around its timeout. Make.com has no code step at all, which is why the Make.com walkthrough is assembled from HTTP modules. n8n can be self-hosted next to the solver, so the n8n guide often gets to keep Local mode.

The short version

Put CapSkip in Server mode, keep its address and key in Pipedream environment variables, and import the SDK straight into a Node.js code step. If the workflow’s time limit is comfortably above your slowest solve, one step is the whole integration. If it is not, split the step: submit to the raw endpoint, hand the id to the rerun helper, and poll on the way back in. Submit the token immediately after you get it, because it expires in about two minutes.

The Node.js side is covered on the Node.js CAPTCHA solver page, the checkbox challenge on the reCAPTCHA v2 solver page, and the equivalent calls in Python, PHP and C# on the CAPTCHA solving SDK page.

Worth knowing before you wire this into something that runs all day: CapSkip does captcha bypass on hardware you already own, so a workflow that fires constantly and one that fires occasionally cost exactly the same.