Integrations

Solving CAPTCHAs From No-Code App Builders: Bubble, Softr, Webflow and Glide

A no-code app can solve a CAPTCHA in one situation only: a server-side HTTP call submits the task to CaptchaAI, collects the token, and posts it to an endpoint that accepts it as a form field. The builder never drives a browser, so anything that needs a live page belongs in Playwright or an extension. Bubble gets the complete build here (a private API Connector key, a self-rescheduling backend workflow, and the recursion and workload settings that break it), and Softr, Webflow after the Logic sunset, and Glide get their own version of the same contract. Everything below assumes forms and endpoints you own or are authorized to submit to.

Which CAPTCHA jobs an app builder can handle

Bubble's API Connector, the Call API action in Softr Workflows and Glide's Call API send requests from the platform's servers and hand back JSON. That works when the target verifies the token server-side, because the token is just a string in your next request. It fails when something has to happen inside a rendered page.

Situation From a builder? Why
A form endpoint verifies a reCAPTCHA v2/v3 or Cloudflare Turnstile token server-side, and you can reproduce its POST Yes The token travels as g-recaptcha-response or cf-turnstile-response in your own HTTP call
You already hold a CAPTCHA image and need its text Yes The image method takes method=base64 with the image in body, sent as a POST
The token has to be injected into a live page, or the site runs a JavaScript callback after the solve No That needs a real browser; see browser extension vs API or Playwright
Cloudflare Challenge pages No The cf_clearance answer is bound to the User-Agent and the proxy IP used for the solve, while a builder's HTTP action sends from the platform's own servers
reCAPTCHA v2 or v3 Enterprise Only if your next call can send the returned User-Agent The answer arrives in result together with user_agent, and a mismatched User-Agent fails validation

CaptchaAI does not currently solve hCaptcha or FunCaptcha (Arkose Labs), and GeeTest v4 support is coming soon but not available yet, so no builder configuration changes those cases.

If you own the target, first ask whether it can accept your app's requests another way, such as an API key or an allowlisted server. Solving is worth it when the target is a vendor-hosted or legacy form you may submit to but cannot change.

The contract every builder has to implement

Every platform in this guide implements the same five rules. The first three and the fifth come from CaptchaAI's API docs; the 120-second cap is this guide's choice, because the docs only say to keep waits and retries bounded:

  1. Submit. Send key, method, the type's fields and json=1 to https://ocr.captchaai.com/in.php. A good answer is {"status":1,"request":"<task id>"}. A status of 0 puts an error code in request.
  2. Wait before the first poll. CaptchaAI's docs give 15-20 seconds for reCAPTCHA v2 and 10-15 seconds for Turnstile.
  3. Poll every 5 seconds. Call https://ocr.captchaai.com/res.php with key, action=get, id and json=1. CAPCHA_NOT_READY (spelled without a T after CAP) means keep going. status 1 means the token is in request. Anything starting with ERROR_ means stop, apart from ERROR_INTERNAL_SERVER_ERROR, a transient server error you retry after about 10 seconds.
  4. Cap the wait at 120 seconds and treat a timeout as a failed task.
  5. Expect no callback. CaptchaAI documents no webhook or callback delivery, so the builder has to own the waiting, which is exactly what one-request-one-response no-code tools are bad at.

Use GET with query parameters for the token types. CaptchaAI's API takes query strings, URL-encoded form fields or multipart forms, not a JSON body, while Softr's and Glide's request bodies are JSON-oriented. CaptchaAI's own reCAPTCHA v2 and Turnstile guides use GET too. The 32-character key travels as key on every call, and keeping it out of the visitor's browser shapes every platform section below.

Branch on the request value like this:

request value Meaning What the workflow does
CAPCHA_NOT_READY Still solving Wait 5 seconds, poll the same ID
ERROR_WRONG_USER_KEY, ERROR_KEY_DOES_NOT_EXIST Bad key Stop the chain. Repeated bad-key calls earn an IP_BANNED answer for 5 minutes
ERROR_ZERO_BALANCE No free thread, or no active plan Back off and submit again later (see the threads section)
ERROR_PAGEURL, ERROR_WRONG_GOOGLEKEY, ERROR_BAD_TOKEN_OR_PAGEURL, ERROR_WRONG_SITEKEY (Turnstile) Wrong inputs Stop and fix the sitekey or page URL. ERROR_BAD_TOKEN_OR_PAGEURL usually means the widget sits in an iframe on another domain, so use the iframe's URL
ERROR_SERVER_ERROR, ERROR_INTERNAL_SERVER_ERROR Transient server error Retry after about 10 seconds, with a bounded number of attempts
ERROR_CAPTCHA_UNSOLVABLE This task failed Stop polling that ID; one fresh submission at most

The full list lives in the error codes reference. Before you touch a builder, run the same two calls from a terminal. If this script cannot get a token for your page, no workflow will:

#!/usr/bin/env bash
# Smoke-test the in.php -> res.php contract your builder will implement.
# Needs curl and jq. Export CAPTCHAAI_KEY, SITEKEY and PAGEURL for a page you may automate.
set -euo pipefail
: "${CAPTCHAAI_KEY:?export CAPTCHAAI_KEY}"
: "${SITEKEY:?export SITEKEY (the data-sitekey value)}"
: "${PAGEURL:?export PAGEURL (the page that renders the widget)}"
API="https://ocr.captchaai.com"

case "${CAPTCHA_TYPE:-turnstile}" in
  turnstile) fields=(--data-urlencode "method=turnstile" --data-urlencode "sitekey=$SITEKEY") ;;
  recaptcha) fields=(--data-urlencode "method=userrecaptcha" --data-urlencode "googlekey=$SITEKEY") ;;
  *) echo "CAPTCHA_TYPE must be turnstile or recaptcha" >&2; exit 2 ;;
esac

submit=$(curl -sS -G "$API/in.php" --data-urlencode "key=$CAPTCHAAI_KEY" "${fields[@]}" \
  --data-urlencode "pageurl=$PAGEURL" --data-urlencode "json=1")
echo "in.php -> $submit"
[ "$(jq -r '.status' <<<"$submit")" = "1" ] || exit 1
id=$(jq -r '.request' <<<"$submit")

sleep 15
for attempt in $(seq 1 21); do
  poll=$(curl -sS -G "$API/res.php" --data-urlencode "key=$CAPTCHAAI_KEY" \
    --data-urlencode "action=get" --data-urlencode "id=$id" --data-urlencode "json=1")
  case "$poll" in ERROR_*|IP_BANNED) echo "res.php -> $poll" >&2; exit 1 ;; esac
  if ! status=$(jq -er '.status' <<<"$poll" 2>/dev/null); then
    echo "non-JSON reply, retrying: ${poll:0:60}" >&2; sleep 5; continue
  fi
  request=$(jq -r '.request' <<<"$poll")
  if [ "$status" = "1" ]; then echo "token after $attempt poll(s): ${request:0:40}..."; exit 0; fi
  if [ "$request" != "CAPCHA_NOT_READY" ]; then echo "res.php -> $poll" >&2; exit 1; fi
  sleep 5
done
echo "no answer within 120 s" >&2
exit 1

The 15-second first wait plus 21 polls at 5 seconds is the 120-second budget. The non-JSON branch is there because the API can answer in plain text even with json=1, and in rare cases returns an HTML 500 or 502 page, which the docs say to retry after 5 seconds.

Bubble: two API Connector calls with a private key

Install Bubble's API Connector plugin and add a collection named CaptchaAI (collection names ship in client-side code, so never put anything secret in one).

Set Authentication to Private key in URL, with Key name key and your CaptchaAI key as Private key. Bubble then appends the key on its server to every call in the collection, and it never reaches a browser. Two alternatives are worse. Private key in header would be tidier, but CaptchaAI documents the key only as a key request parameter, never as a header. And typing the key into each call under None or self-handled authentication is the setup Bubble's security dashboard flags as an unsafe API call configuration when a call is used as data.

Add two calls, both with Use as set to Action and Data type set to JSON, so they appear as actions in the workflow editor rather than as data sources:

  1. Submit task. GET https://ocr.captchaai.com/in.php?method=userrecaptcha&googlekey=[googlekey]&pageurl=[pageurl]&json=1. The square brackets make googlekey and pageurl parameters. Bubble marks new parameters Private by default, so untick Private on both, because each task fills them from the database. For Turnstile, use method=turnstile&sitekey=[sitekey] instead. An invisible reCAPTCHA v2 also needs invisible=1, and reCAPTCHA v3 needs version=v3&action=[action] with the page's action name (verify if you cannot find it).
  2. Get result. GET https://ocr.captchaai.com/res.php?action=get&id=[id]&json=1.

On both calls, tick Include errors in response and allow workflow actions to continue, so an HTTP-level failure lets the chain decide what to do instead of halting it. Bubble requires re-initializing a call after you change that box.

Initialize call on Submit task sends a real task, so point it at a page and sitekey you are allowed to use, and remember that initialization values become part of the call's schema in your app's source code. Copy the returned task ID, wait about 20 seconds, and initialize Get result with it. Success and error answers share the same two fields, status and request, so one initialization maps them all. In the response field list, set request to text: it holds a task ID on submit, and a token, CAPCHA_NOT_READY or an error code on poll.

Bubble: a CaptchaTask record and two backend workflows

The polling belongs in backend workflows for two reasons. A page workflow dies when the user closes the tab, and a single workflow cannot wait out a solve: Bubble's Add a pause before next action has no effect on server-side actions such as API calls and data changes, and any workflow times out after 300 seconds. So each poll is its own short workflow run that schedules the next one. Budget for a paid Bubble plan: Bubble's pricing docs list backend workflows, and deploying to live, among what the first paid tier adds. Turn on the backend editor under Settings > API with Enable Workflow API and backend workflows, and leave Expose as a public API workflow unticked on both workflows below, because only your own app schedules them.

Create a data type CaptchaTask with these fields: sitekey (text), pageurl (text), task_id (text), status (text), token (text), error (text) and deadline (date). Then build two API workflows:

captcha_submit takes one parameter, task (CaptchaTask):

  1. CaptchaAI - Submit task, with googlekey = task's sitekey and pageurl = task's pageurl.
  2. Make changes to task: task_id = Result of step 1's request, status = polling, deadline = Current date/time +(seconds): 120. Only when Result of step 1's status is 1.
  3. Schedule API workflow captcha_poll with task = task and attempt = 1, Scheduled date = Current date/time +(seconds): 20 (15 is enough for Turnstile). Same Only when condition.
  4. Make changes to task: status = failed, error = Result of step 1's request. Only when Result of step 1's status is not 1.

captcha_poll takes task (CaptchaTask) and attempt (number):

  1. CaptchaAI - Get result, with id = task's task_id.
  2. When Result of step 1's status is 1: Make changes to task (status = solved, token = Result of step 1's request), then run the downstream call in the same run (next section).
  3. When the poll is retryable, attempt < 24 and Current date/time < task's deadline: Schedule API workflow captcha_poll with attempt = attempt + 1 at Current date/time +(seconds): 5. Retryable means Result of step 1's request is CAPCHA_NOT_READY, or the error object's has returned error is yes and its body contains neither ERROR_ nor IP_BANNED. That second case is the rare HTML 500/502 page, which the docs say to retry after 5 seconds, while a plain-text error code ends the chain instead of burning 24 polls. Treat ERROR_INTERNAL_SERVER_ERROR as retryable too, but schedule it +10 seconds out.
  4. In every other case, which Bubble expresses as a second Only when that negates both conditions above: Make changes to task, status = failed, error = Result of step 1's request, or "timeout" when the deadline or the attempt cap was reached.

Start the chain from the page: the button workflow creates the CaptchaTask and schedules captcha_submit at Current date/time. A group whose data source is that task shows the status as the backend changes it.

Three Bubble settings break this chain in ways that look like CaptchaAI failures:

  • Infinite recursion protection. Apps created on or after July 1, 2024 stop any chain at a depth of 10 by default. This chain is about 25 workflows deep at the 24-attempt cap, so the default kills it after eight or nine polls with no CaptchaAI error. The termination shows up only under Workflow errors in the server logs and in a once-a-day admin email. Raise the limit under Settings > API to 30 or more, separately for development and live (Bubble's recursion docs).
  • Don't loop through your own Workflow API. Calling your own backend workflow through the API Connector is capped at a depth of 3. Use Schedule API workflow.
  • Treat +5 seconds as a minimum. Scheduled workflows go through Bubble's scheduler queue, which is why the 120-second cap lives in a stored deadline rather than in a count of polls. The attempt counter is only a backstop against runaway recursion.

Bubble: spending the token and the workload units

Use the token in the same captcha_poll run that saw status 1. Google documents a reCAPTCHA response token as valid for two minutes and verifiable once; Cloudflare gives a Turnstile token 300 seconds, single-use. A token parked for a user to act on later is likely dead by then. Build the downstream POST, with the token in g-recaptcha-response (or cf-turnstile-response) beside the form fields, in a second API Connector collection, and make it the last action of the solved branch. Keep it out of the CaptchaAI collection: that collection's authentication appends key to every call in it, so your CaptchaAI key would travel to the target's server. If the target rejects the token, submit a new task instead of resending it. The reCAPTCHA token lifecycle guide covers expiry and replay in more depth.

Two privacy notes. A scheduled workflow runs as the user who scheduled it, with that user's privacy rules, unless you tick Ignore privacy rules when running the workflow. And add a privacy rule on CaptchaTask so the token field is visible only to its creator.

Polling is where Bubble's workload units (WU) go. These estimates come from Bubble's activity-type table (0.6 WU per server-side workflow action, 0.1 per outbound API call, 0.1 per workflow added to the scheduler, 0.5 per thing written or modified), before the small per-lookup and per-condition costs:

Run What it does Approximate WU
captcha_submit API call, one write, one schedule 2.5
captcha_poll, not ready API call, one schedule 1.4
captcha_poll, solved API call, one write, downstream call 2.5

Turnstile typically solves in under 10 seconds, so it is usually ready at the first poll and costs about 5 WU. A reCAPTCHA v2 solve that takes the full 60-second ceiling costs about 16 WU with a 20-second first wait. The levers: pass attempt as a workflow parameter instead of writing it to the record on every poll, start reCAPTCHA v2 polls at 20 seconds (the top of the documented 15-20 second window), and never poll faster than every 5 seconds. The Server logs show the WU charged per action, so check your real numbers after the first day.

Softr: build it in Softr Workflows, not on a button

Softr has two things called Call API, and only one of them can solve a CAPTCHA. The button action sends POST, PUT, PATCH or DELETE from list-item and list-details buttons, needs the Professional plan or higher, and gives later steps nothing to poll with. Softr's docs also don't say whether that request leaves from the visitor's browser, so keep the CaptchaAI key out of it.

Use Softr Workflows instead. The Workflows Call API action also needs the Professional plan or above, but it supports GET, takes query parameters, and exposes the response body to later steps. Start the workflow from a Run custom workflow button (Softr Apps > Run custom workflow), a Submit form trigger, or a Softr Databases Record added trigger. On app-triggered runs, toggle Show wait screen so the user sees progress; database triggers run in the background and cannot show one. Then chain these steps:

  1. Call API, GET https://ocr.captchaai.com/in.php with query parameters key, method, sitekey (or googlekey), pageurl and json=1. Softr's request body is a JSON payload, which CaptchaAI's API does not take, so everything goes in the query parameters.
  2. Branch on the response body's status being 1. On the no path, write the error to the record and end.
  3. Wait before the first poll: 15 seconds for Turnstile, 20 for reCAPTCHA v2. Softr's docs list Wait as a flow-control step but don't state its shortest or longest duration, so check the step's settings before you rely on those numbers.
  4. Call API, GET https://ocr.captchaai.com/res.php with key, action=get, id (the first response's request) and json=1.
  5. Branch: on status 1, use the token right away in a Call API to the target endpoint. On CAPCHA_NOT_READY, add another Wait and poll, three or four rounds deep.

Unroll the polls rather than using a Loop. Softr's Loop runs its actions once per list item, and its docs describe no early exit. Unrolling also costs less: every Call API counts toward your monthly Workflow action limit, while Wait, Branch and Filter steps don't. For reCAPTCHA v2, a few polls spaced 10-15 seconds apart use fewer actions than polling every 5 seconds through a solve that can take up to 60.

Don't try to wait inside Run Custom Code (a paid-plan action). Softr's Run Custom Code reference lists the limits: Python has no network access, JavaScript has no fetch (only the http and https modules), and a returned promise has to settle within five seconds. It is fine for turning a response into clean fields, but useless as a sleep. Keep the key in the Call API step's query parameters, where the workflow sends it from Softr's side, and never paste it into code; Softr's own advice is to keep secrets out of the script.

Webflow after Logic: send the form somewhere that can wait

Webflow disabled Logic on all sites on June 27, 2025: existing flows stopped, and forms attached to them became standard Webflow forms. Two routes still work.

An automation platform. Trigger on the Webflow form submission and run the same submit, wait, poll and forward sequence with HTTP modules and sleep steps. The Make, n8n and Zapier guides cover the platform side.

A webhook to a Cloudflare Worker. Webflow can POST a form_submission event to your URL, and two limits shape the design. Webflow's webhook docs expect a swift 200 and count a slow answer as a failure. A failed delivery is retried up to three more times at 10-minute intervals, and repeated failures get the webhook deactivated, with an email to tell you. A Worker's ctx.waitUntil() extends work only 30 seconds past the response. So the fetch handler verifies the signature, queues the submission and answers 200, and a Cloudflare Queue consumer, with up to 15 minutes of wall time, runs the solve. One job makes at most 23 subrequests (a submit, up to 21 polls, a forward), under the Workers Free limit of 50.

The first half of src/index.js is the CaptchaAI client, which uses the same contract as the curl script:

// src/index.js (part 1): CaptchaAI submit + poll for a Turnstile token
const API = "https://ocr.captchaai.com";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function captchaai(path, params, env) {
  const url = new URL(path, API);
  const query = { ...params, key: env.CAPTCHAAI_KEY, json: 1 };
  for (const [name, value] of Object.entries(query)) url.searchParams.set(name, String(value));
  const text = (await (await fetch(url)).text()).trim();
  try {
    return JSON.parse(text);
  } catch {
    // Plain-text error codes and rare HTML 5xx pages are not JSON.
    return { status: 0, request: /^(ERROR_\w+|IP_BANNED)$/.test(text) ? text : "NON_JSON" };
  }
}

async function solveTurnstile(env, sitekey, pageurl) {
  const submit = await captchaai("/in.php", { method: "turnstile", sitekey, pageurl }, env);
  if (submit.status !== 1) throw new Error(`in.php: ${submit.request}`);
  const deadline = Date.now() + 120000;
  await sleep(15000);
  while (Date.now() < deadline) {
    const poll = await captchaai("/res.php", { action: "get", id: String(submit.request) }, env);
    if (poll.status === 1) return poll.request;
    if (!["CAPCHA_NOT_READY", "NON_JSON"].includes(poll.request)) {
      throw new Error(`res.php: ${poll.request}`);
    }
    await sleep(5000);
  }
  throw new Error("no answer within 120 s");
}

The second half holds the two entry points. Webhooks created through the Webflow API carry x-webflow-timestamp (epoch milliseconds) and x-webflow-signature, a hex HMAC-SHA256 of timestamp:body. With a site token, the key is the secretKey returned when you create the webhook; an OAuth app uses its client secret instead. Webhooks created in the dashboard carry no signature headers, so create yours through the API. crypto.subtle.verify does the comparison in constant time, and the 5-minute timestamp window is the one Webflow recommends against replays.

// src/index.js (part 2): webhook intake and queue consumer
const FATAL = /ERROR_(WRONG_USER_KEY|KEY_DOES_NOT_EXIST|PAGEURL|WRONG_SITEKEY|BAD_PARAMETERS)|IP_BANNED/;

async function signedByWebflow(request, rawBody, secret) {
  const ts = request.headers.get("x-webflow-timestamp");
  const sig = request.headers.get("x-webflow-signature") || "";
  if (!ts || !/^[0-9a-f]{64}$/i.test(sig)) return false;
  if (Math.abs(Date.now() - Number(ts)) > 300000) return false; // older than 5 minutes
  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey("raw", enc.encode(secret),
    { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
  const sigBytes = new Uint8Array(sig.match(/../g).map((h) => parseInt(h, 16)));
  return crypto.subtle.verify("HMAC", key, sigBytes, enc.encode(`${ts}:${rawBody}`));
}

export default {
  async fetch(request, env) {
    if (request.method !== "POST") return new Response("POST only", { status: 405 });
    const rawBody = await request.text();
    if (!(await signedByWebflow(request, rawBody, env.WEBFLOW_WEBHOOK_SECRET))) {
      return new Response("bad signature", { status: 401 });
    }
    const { payload } = JSON.parse(rawBody);
    await env.CAPTCHA_JOBS.send({ fields: payload.data, formId: payload.formId });
    return new Response("queued"); // 200 now; the solve runs in queue()
  },

  async queue(batch, env) {
    for (const msg of batch.messages) {
      try {
        const token = await solveTurnstile(env, env.TARGET_SITEKEY, env.TARGET_PAGEURL);
        const body = new URLSearchParams({ ...msg.body.fields, "cf-turnstile-response": token });
        const res = await fetch(env.TARGET_URL, { method: "POST", body });
        if (!res.ok) throw new Error(`target answered ${res.status}`);
        msg.ack();
      } catch (err) {
        console.error(`job ${msg.id}, attempt ${msg.attempts}: ${err.message}`);
        if (FATAL.test(err.message)) msg.ack(); // resending a bad key or bad input only makes it worse
        else msg.retry({ delaySeconds: 30 });
      }
    }
  },
};

Name the Webflow form fields the way the target endpoint expects, because the consumer forwards payload.data as-is. Each retry submits a fresh task, so a rejected token is never reused. The configuration goes in wrangler.json:

{
  "name": "webflow-captcha-relay",
  "main": "src/index.js",
  "compatibility_date": "2026-09-01",
  "queues": {
    "producers": [{ "queue": "captcha-jobs", "binding": "CAPTCHA_JOBS" }],
    "consumers": [{ "queue": "captcha-jobs", "max_batch_size": 1, "max_retries": 2 }]
  },
  "vars": {
    "TARGET_URL": "https://intake.example.com/submit",
    "TARGET_PAGEURL": "https://intake.example.com/register",
    "TARGET_SITEKEY": "YOUR_TURNSTILE_SITEKEY"
  }
}

Create the queue, store the secrets and register the webhook:

npx wrangler queues create captcha-jobs
npx wrangler secret put CAPTCHAAI_KEY
npx wrangler deploy

# Needs a Webflow site token with the sites:write and forms:read scopes.
curl -sS -X POST "https://api.webflow.com/v2/sites/$WEBFLOW_SITE_ID/webhooks" \
  -H "Authorization: Bearer $WEBFLOW_SITE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"triggerType":"form_submission","url":"https://webflow-captcha-relay.YOUR_SUBDOMAIN.workers.dev","filter":{"name":"Registration"}}'

# Paste the secretKey from that response when prompted:
npx wrangler secret put WEBFLOW_WEBHOOK_SECRET

max_batch_size: 1 keeps one solve per consumer invocation, so a slow reCAPTCHA can't hold a whole batch past its deadline. At the time of writing, Cloudflare includes 10,000 Queues operations a day on the Workers Free plan. The Turnstile API guide covers finding the sitekey and pageurl for the target.

Glide, and builders with their own guides

Glide's Call API needs the Business or Enterprise plan. It supports GET, POST, PUT, PATCH and DELETE, takes Query String values, and marks every custom value you type as secret, so the key stays hidden. It stores the response body in a column that a Query JSON column can parse. Build it as actions in a workflow: Call API to in.php (GET, key as a secret query value), a Wait action, Call API to res.php, then a condition on the parsed status. Glide's docs say each Call API run uses 1 update, so a solve that needs six polls costs seven updates.

Don't use Call API as a computed column for res.php. In that mode Glide caches the response for the Refresh After period (30 days when blank) and refreshes only when someone views the data, so you'd keep reading a stale CAPCHA_NOT_READY. Glide describes its Wait action as a short delay between actions, so if a reCAPTCHA v2 solve needs more waiting than your workflow comfortably holds, hand the job to Make or n8n and write the token back.

Builders with a code runtime have their own pages: Budibase, FlutterFlow cloud functions and Retool.

Threads, concurrency and what actually costs money

A CaptchaAI thread is one in-flight task; in the Bubble build, every CaptchaTask in polling holds one. BASIC ($15/month, 5 threads) handles five simultaneous submissions, which is roughly 300 reCAPTCHA v2 solves an hour even if each took the full 60-second ceiling, and a Turnstile solve (under 10 seconds) frees its thread far sooner. For bursts above five, STANDARD ($30/month, 15 threads) is the next step; the guide to how thread pricing works explains the plan ladder.

Past the thread cap, a submit can come back as ERROR_ZERO_BALANCE, which in CaptchaAI's thread model means no free thread, or no active plan. Saturation may also show up only as a task that stays at CAPCHA_NOT_READY longer, which is one more reason for the stored deadline. A GET to res.php?action=threadsinfo returns {"threads":"5","working_threads":5}. If working_threads equals threads, back off and resubmit; if it is lower, the account has no active plan and retrying won't help. In Bubble, the cheapest guard is a condition on captcha_submit: when a search for CaptchaTasks with status polling returns a count of five or more, reschedule the submit 10 seconds later. Each reschedule adds a level of recursion depth, so leave headroom above 25 in the limit.

Plans include unlimited solves per thread, so at a fixed thread count the CaptchaAI bill doesn't grow with the number of solves. The bill that does grow is the builder's: Bubble WU per poll, Softr workflow actions per Call API, Glide updates per run, Make credits per module run. That is why every platform section above spends its effort on fewer, well-timed polls.

Troubleshooting

Symptom Platform Cause and fix
Chain stops after eight or nine polls, task stuck in polling, no CaptchaAI error Bubble Infinite recursion protection (default depth 10 on apps created on or after July 1, 2024). Raise it under Settings > API for development and live
Security dashboard flags an unsafe API call configuration Bubble Authentication is None or self-handled on a Data call. Switch the collection to Private key in URL and use the calls as actions
Target rejects a token that CaptchaAI returned Bubble, Glide The token aged out while waiting for a user or a later workflow. Call the target in the same run that received it
ERROR_ZERO_BALANCE only during bursts Any More simultaneous tasks than plan threads. Add the concurrency guard on captcha_submit, or space out parallel runs, and confirm with threadsinfo
Step fails with "Async script timed out after 5000ms" Softr A sleep or polling loop inside Run Custom Code. Move the waiting into Wait steps
in.php returns an error code although every field is filled in Softr, Glide The fields went into a JSON body, which CaptchaAI's API does not take. Send them as query parameters on GET
res.php column shows CAPCHA_NOT_READY for hours Glide Call API used as a computed column with Refresh After caching. Use the Call API action in a workflow
Webflow webhook goes inactive Webflow The endpoint kept failing or answering slowly, so Webflow deactivated it. Acknowledge immediately and do the solve in the queue consumer
Worker times out mid-poll Cloudflare The solve ran in ctx.waitUntil(), which stops 30 seconds after the response. Move it to a queue consumer

FAQ

Can CaptchaAI call my Bubble backend workflow when the solve is ready, instead of me polling?

No. CaptchaAI documents no callback or webhook delivery, and token results come only from polling res.php. The self-rescheduling captcha_poll workflow is the replacement for that missing callback.

Can Bubble handle image CAPTCHAs the same way?

Yes, with one change to the submit call. Image tasks go to in.php as a POST with method=base64 and the base64-encoded image in body, using the API Connector's Form-data body type, with key also added as a Private body parameter so it travels with the form fields. Images must be 100 bytes to 100 KB in jpg, png or gif. Image answers usually arrive within seconds, so poll after 5 seconds rather than 20.

How many CaptchaAI threads does a typical Bubble app need?

Match threads to peak simultaneous solves, not daily volume. A contact or registration form that rarely sees more than five submissions within the same minute fits BASIC ($15/month, 5 threads). Keep the concurrency guard on captcha_submit so a burst waits its turn instead of failing. Compare plans on the CaptchaAI pricing page.

Comments are disabled for this article.