hCaptcha solving for AI agents.
When a computer-use or browser agent hits an hCaptcha gate, it should not try to
click through the challenge. Give it a tool. The agent reads the
data-sitekey and page URL, calls NoneCap, gets back a real
P1_ token, injects it as the form's h-captcha-response,
and carries on with the task.
The pattern: hCaptcha as one tool in the agent's toolbox
An autonomous agent already has tools for clicking, typing, navigating, and
reading the screen. An hCaptcha gate is just one more thing it can get stuck on,
so you hand it one more tool: solve_hcaptcha. When the agent lands on
a challenge it cannot pass, it calls that tool with the two values it can read off
the page, and gets a token to inject. The rest of the run is unchanged.
| Agent step | What the agent does | NoneCap |
|---|---|---|
| 1 · Detect | Sees the hCaptcha widget / "I am human" checkbox it can’t pass | (none) |
| 2 · Read | Reads data-sitekey off the widget and the page URL | sitekey, url |
| 3 · Call tool | Invokes the solve_hcaptcha tool with those two values | POST /v1/solves |
| 4 · Token | Receives the P1_ string from the tool result | token |
| 5 · Inject | Sets h-captcha-response and fires the widget callback | (none) |
| 6 · Continue | Submits the form and resumes the task | (none) |
1. Define the tool the model sees
The tool description is what routes the model to it, so be explicit
about when to call it. The inputs are the two values the agent can read
from the rendered page: the data-sitekey on the widget and the page
url.
{
"name": "solve_hcaptcha",
"description": "Mint a real hCaptcha token for a page that is blocked by an hCaptcha challenge. Call this when you see an hCaptcha widget or an 'I am human' checkbox you cannot pass. Returns a token to inject as h-captcha-response, then submit the form.",
"input_schema": {
"type": "object",
"properties": {
"sitekey": { "type": "string", "description": "The data-sitekey on the hCaptcha widget." },
"url": { "type": "string", "description": "The page URL where the challenge appears." }
},
"required": ["sitekey", "url"]
}
} 2. Wire the handler to POST /v1/solves
When the model invokes the tool, your handler creates a solve with
type: "hcaptcha", the sitekey, and the url.
Block with ?wait=90 so the token comes back inside a single tool turn,
and the model gets a plain string to work with:
import os
import requests
NONECAP_KEY = os.environ["NONECAP_KEY"]
def solve_hcaptcha(sitekey: str, url: str) -> str:
"""Tool handler: mint a real hCaptcha token and return the P1_ string."""
r = requests.post(
"https://api.nonecap.com/v1/solves",
headers={"Authorization": f"Bearer {NONECAP_KEY}"},
params={"wait": 90}, # block up to 90s for a terminal state
json={"type": "hcaptcha", "sitekey": sitekey, "url": url},
timeout=120,
)
r.raise_for_status()
solve = r.json()
if solve["status"] != "solved":
# Failed solves are auto-refunded, so the agent can just retry.
raise RuntimeError(f"solve {solve['status']}: {solve.get('error')}")
return solve["token"] # a real P1_… hCaptcha token
# In your agent loop, when the model calls the solve_hcaptcha tool:
def handle_tool_call(name: str, args: dict) -> str:
if name == "solve_hcaptcha":
return solve_hcaptcha(args["sitekey"], args["url"])
# ... your other tools (click, type, navigate, screenshot) ...
That is the whole integration on the NoneCap side. The token in
solve["token"] is a real P1_ string, the same value the
page would have produced if a human had solved the challenge.
3. Inject the token and continue
The agent then puts the token into the page it controls and submits. This step is
identical whether the agent drives the browser through CDP, Playwright, Puppeteer,
or a framework like browser-use: set the h-captcha-response field and,
for invisible widgets, fire the data-callback.
// The agent's "inject + submit" step, run in the page it controls
// (computer-use via CDP, Playwright, Puppeteer, browser-use, etc.):
(token) => {
for (const name of ["h-captcha-response", "g-recaptcha-response"]) {
const el = document.querySelector(`textarea[name="${name}"]`);
if (el) el.value = token; // drop-in hCaptcha reuses the old field name
}
// Invisible widgets submit from data-callback, fire it if present.
const cb = document
.querySelector("[data-callback]")
?.getAttribute("data-callback");
if (cb && typeof window[cb] === "function") window[cb](token);
} After this, the gate is cleared and the agent resumes whatever it was doing. For the full DOM-injection detail and the checkbox vs invisible vs enterprise cases, see solving hCaptcha in Playwright; the injection is the same regardless of which driver the agent uses.
Long agent loops: go async with a webhook
Blocking with ?wait is fine for a single gate. But agents often run
for minutes and may fan out across many pages, so for those loops you can submit
without blocking and let the result come to you. Drop ?wait, pass a
webhook_url, and you get a 202 with status
pending immediately. The agent keeps working, and the
solve.completed callback arrives when the solve finishes:
# Long-running agent: submit without blocking, get the token by webhook.
curl https://api.nonecap.com/v1/solves \
-H "Authorization: Bearer $NONECAP_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "hcaptcha",
"sitekey": "f5ab1c2d-7e8f-4a9b-b1c2-d3e4f5a6b7c8",
"url": "https://target.example/login",
"webhook_url": "https://your-agent.example/hooks/solve"
}'
# 202 ACCEPTED comes back with status "pending"; the solve.completed
# callback arrives at webhook_url when it reaches a terminal state.
You can also poll GET /v1/solves/{id} on your own cadence if a
webhook endpoint is awkward inside your agent host. Either way the agent is not
holding a request open for the duration of the solve.
Why this beats letting the agent fight the challenge
Asking a model to drag puzzle tiles is slow and burns agent tokens, and it still fails the hCaptcha behavioural score because the session is automated. NoneCap solves it separately, off the agent's machine, then hands back a finished token. Two properties matter for an autonomous loop:
- Failed attempts do not burn budget. Billing is per challenge round, charged only on success, and failed, cancelled, and expired solves are auto-refunded. An agent that retries a flaky gate pays for the round that worked, not the ones that did not.
- Concurrency scales with your plan. A single key allows
5 concurrent solves on the free trial, 25 on
Starter, 50 on Builder, and 100 on Scale, so a fleet of agents can keep that
many gates in flight at once. A
429 concurrency_limit_exceededjust means retry once one completes.
New accounts get 100 free credits, enough to wire the tool into your agent and confirm tokens are accepted before you top up. Credits are prepaid, never expire, and there is no subscription. See pricing for the packs.
Scope: hCaptcha gates only
This tool unlocks hCaptcha gates only: regular, invisible, and enterprise
rqdata. If your agent lands on reCAPTCHA v2/v3, Cloudflare Turnstile, or FunCaptcha, NoneCap does not solve those today (they are on the roadmap), so the tool will not help there. Scope the tool description to hCaptcha so the model does not reach for it on a gate it cannot clear. Theh-captchawidget and thehcaptcha.comscript are the tell that this is the right gate.
For enterprise sitekeys the agent also reads the per-challenge rqdata
blob off the page and sends it with type: "hcaptcha_enterprise". The
full request shape, every field, and language samples are in the
API reference.
Last updated June 2026.
Frequently asked
How does an agent know when to call the solve tool?
description does the routing. Describe solve_hcaptcha as the thing to call when the agent sees an hCaptcha widget or an "I am human" checkbox it cannot pass, and the model will pick it on its own when it gets stuck on a gate. Keep the inputs to the two values it can read off the page: the data-sitekey and the page url. Send both to POST /v1/solves.My agent loop runs for minutes. Should it block on the solve?
?wait=90 is simplest: the tool turn returns the token in one call. For long autonomous loops, or when several agents share a worker, submit without ?wait and pass a webhook_url. You get a 202 with status pending, the agent keeps working, and the solve.completed callback delivers the token. You can also poll GET /v1/solves/{id} on your own schedule.What happens to my budget when the agent retries a failed solve?
Can the agent use NoneCap for a reCAPTCHA or Turnstile gate it hits?
rqdata). If the agent lands on a reCAPTCHA or Cloudflare Turnstile gate, this tool does not apply (those are on the roadmap). Scope the tool description to hCaptcha so the model does not try to use it on a gate it cannot solve.