Skip to main content

nonecap

CI PyPI Python versions License: MIT

Official Python client for the NoneCap hCaptcha solving API.

Submit a captcha, get back a token. The client handles the polling, the timeouts, and the error cases so you don't write the request loop yourself. Sync and async, fully typed.

Install

pip install nonecap

Python 3.9+. The only dependency is httpx.

Quick start

Grab an API key from dashboard.nonecap.com, then:

from nonecap import NoneCap

nc = NoneCap(api_key="nc_live_...")

solve = nc.solve(
    type="hcaptcha",
    sitekey="10000000-ffff-ffff-ffff-000000000001",
    url="https://example.com/login",
)

print(solve.token)  # the hCaptcha token, ready to submit
print(solve.resp_key)  # hcaptcha.getRespKey() equivalent, for sites that verify the pair

solve() submits the captcha and waits until it's done, using the API's long-poll so you aren't hammering it with requests. It returns the solved solve, or raises if the solve fails or your timeout runs out.

Async

Same surface, awaited. Use it as an async context manager so the connection pool gets cleaned up:

import asyncio
from nonecap import AsyncNoneCap

async def main() -> None:
    async with AsyncNoneCap(api_key="nc_live_...") as nc:
        solve = await nc.solve(type="hcaptcha", sitekey="...", url="https://example.com")
        print(solve.token)

asyncio.run(main())

Cancelling a solve

solve() is the simple path when you just want a token. When you need to hold a reference you can cancel — for clean shutdown, freeing a worker slot, or stopping early — use solves.start(). It submits the solve and hands back a SolveHandle right away, with the id already populated, instead of blocking on the result:

handle = nc.solves.start(type="hcaptcha", sitekey=sitekey, url=url)
print(handle.id)  # available immediately

# Wait for it, just like solve():
solve = handle.result(timeout=120)
print(solve.token)

Holding the handle lets you stop a solve you no longer need — say, on shutdown or when a parallel attempt already won — instead of waiting on its result:

handle = nc.solves.start(type="hcaptcha", sitekey=sitekey, url=url)

# ... elsewhere / later, to stop it:
handle.cancel()

handle.result() long-polls until the solve finishes and returns it, raising SolveFailedError / SolveTimeoutError like solve() does. The terminal outcome is memoized, so once the solve has settled, calling result() again replays it for free; a SolveTimeoutError is not memoized, so you can call result() again with a larger timeout to keep waiting. handle.cancel() stops a pending or in-flight solve and returns its final state — if the solve already finished, that's not an error, you just get the completed solve back.

The async client mirrors this — await the start, the result, and the cancel:

async with AsyncNoneCap(api_key="nc_live_...") as nc:
    handle = await nc.solves.start(type="hcaptcha", sitekey=sitekey, url=url)
    print(handle.id)

    # Either wait for the outcome ...
    solve = await handle.result(timeout=120)
    print(solve.token)

    # ... or, on another handle, cancel it instead of awaiting:
    other = await nc.solves.start(type="hcaptcha", sitekey=sitekey, url=url)
    await other.cancel()

Cancelled solves are never charged, and a solve you simply abandon expires uncharged at the server deadline — nothing is billed unless a solve actually succeeds. So cancel() is for cleanup and early-stop, not cost protection.

Cleaning up after a solve() timeout

solve() blocks until the solve settles, so it gives you no handle to cancel a solve while it's still running — for that, use solves.start() above. The one thing the solve() path offers is cleanup after a timeout: when solve() raises SolveTimeoutError, the error usually carries the in-flight solve_id (and last-known solve), so you can cancel the solve the wait gave up on. Guard on solve_id being present — if the very first submission times out at the transport level, no id has been assigned yet, so solve_id is None.

from nonecap import SolveTimeoutError

try:
    nc.solve(type="hcaptcha", sitekey=sitekey, url=url)
except SolveTimeoutError as err:
    if err.solve_id is not None:
        nc.solves.cancel(err.solve_id)
    else:
        raise

Handling failures

Every error this library raises extends NoneCapError, so you can catch the whole family or pick out the one you care about.

from nonecap import (
    NoneCap,
    SolveFailedError,
    InsufficientCreditsError,
    RateLimitError,
)

try:
    solve = nc.solve(type="hcaptcha", sitekey=sitekey, url=url)
except SolveFailedError as err:
    print("Could not solve it:", err.solve.error.code if err.solve.error else "?")
except InsufficientCreditsError:
    print("Out of credits. Top up at dashboard.nonecap.com")
except RateLimitError:
    print("Too many solves in flight, back off and retry")

The subclasses are AuthenticationError (401), PermissionDeniedError (403), InsufficientCreditsError (402), ValidationError (422/400, with a param naming the bad field), NotFoundError (404), ConflictError (409), RateLimitError (429), APIError (5xx), APIConnectionError and APITimeoutError (the request never landed), and SolveTimeoutError (your solve() budget ran out). SolveFailedError carries the full solve so you can read the underlying error code and the timings.

Enterprise captchas

For hcaptcha_enterprise, rqdata is required. The @overload signatures enforce that in mypy and pyright, so leaving it out fails your type check, and a runtime check backs it up before any network call:

solve = nc.solve(
    type="hcaptcha_enterprise",
    sitekey=sitekey,
    url=url,
    rqdata="...",  # required for enterprise
)

Proxies

Pass a proxy as a dict or a URL string. The solve runs through it, and the bytes are metered back on the solve.

nc.solve(
    type="hcaptcha",
    sitekey=sitekey,
    url=url,
    proxy={"scheme": "http", "host": "1.2.3.4", "port": 8080, "username": "u", "password": "p"},
    # or: proxy="http://u:p@1.2.3.4:8080"
    # scheme can be http, https, socks5, socks5h, or socks4 (default http)
    # e.g. proxy="socks5://u:p@1.2.3.4:1080"
)

Reporting token acceptance

A token that hCaptcha blessed can still be refused by the site you send it to. Tell us what happened and we tune minting against your real acceptance rate — it's free, and it's the fastest way to get a regression on your sitekey noticed.

Keep the solve_id next to the token you submit downstream, then report the verdict:

solve = nc.solve(type="hcaptcha", sitekey=sitekey, url=url)
ok = submit_to_the_site_you_are_automating(solve.token)

nc.feedback.report(
    solve.id,
    outcome="accepted" if ok else "rejected",
    reason=None if ok else "session invalidated",  # optional, the code your target returned
    context=None if ok else "3rd retry, rotating residential pool",  # optional, anything you think would help us diagnose it
)

At volume, buffer the verdicts and flush them in one call. Reports over 500 are split across requests for you:

batch = nc.feedback.report_many([
    {"solve_id": "solve_01J...", "outcome": "accepted"},
    {"solve_id": "solve_01J...", "outcome": "rejected", "reason": "...", "context": "..."},
])

# Items resolve independently, so the call succeeds even when some are
# rejected — check `failed` rather than relying on a raised error.
if batch.failed:
    print([r for r in batch.results if r.status == "error"])

outcome is one of accepted, rejected, unknown (submitted, verdict unclear), unused (never submitted), or error (downstream broke for a non-token reason). Only accepted and rejected count toward the acceptance rate.

Reporting the same solve again corrects the earlier verdict, so retries and late fixes are safe. You can report any of your own solved solves within ~30 days of the solve; corrections to something you already reported are never cut off by that window. On AsyncNoneCap both methods are coroutines.

Lower-level API

solve() is the convenient path. When you want control over submission and polling, the resource methods map one to one to the REST API:

# Submit without waiting: returns immediately with a pending solve
pending = nc.solves.create(type="hcaptcha", sitekey=sitekey, url=url)

# Submit and hold the connection up to 30s for it to finish
maybe_done = nc.solves.create(type="hcaptcha", sitekey=sitekey, url=url, wait=30)

# Poll one solve, long-polling up to 30s
solve = nc.solves.retrieve(pending.id, wait=30)

# Cancel a pending or in-flight solve
nc.solves.cancel(pending.id)

# List a page of solves
page = nc.solves.list(limit=50, status="solved")

# Or iterate every solve, newest first
for s in nc.solves.list_all():
    print(s.id, s.status)

# Your account and credit balance
me = nc.me()
print(me.credits_balance)

On AsyncNoneCap the same methods are coroutines, and list_all() is an async iterator (async for s in nc.solves.list_all()).

Configuration

NoneCap(
    api_key="nc_live_...",              # required
    base_url="https://api.nonecap.com", # override if you need to
    timeout=100.0,                      # per HTTP request, seconds
    http_client=my_httpx_client,        # inject your own httpx.Client
)

solve() takes its own timeout (seconds, default 180) for the overall wait.

Typing

The package ships a py.typed marker and full inline annotations. Solves come back as frozen dataclasses with the exact field names the API uses (solve.token, solve.credits_charged, solve.queue_ms), so what you read in the API reference is what you get in code.

License

MIT, see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nonecap-0.5.0.tar.gz (25.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nonecap-0.5.0-py3-none-any.whl (20.1 kB view details)

Uploaded Python 3

File details

Details for the file nonecap-0.5.0.tar.gz.

File metadata

  • Download URL: nonecap-0.5.0.tar.gz
  • Upload date:
  • Size: 25.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for nonecap-0.5.0.tar.gz
Algorithm Hash digest
SHA256 499baf407bce9d4f85bca4f61b0185881c096588c5289c4881d763e55e368048
MD5 fd4a1296602742bf516352ec76783c41
BLAKE2b-256 5ba2602aedb4c77ae5cbff737a42fec5284ccbd13c0aab78fcfc10eae1e229bc

See more details on using hashes here.

Provenance

The following attestation bundles were made for nonecap-0.5.0.tar.gz:

Publisher: ci.yml on nonecap/nonecap-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nonecap-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: nonecap-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 20.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for nonecap-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 36777ef4add7de0a90d2c27dfded49b5f4a920bca484b05afef2d7bb0467d854
MD5 297d84d00b62efa6047dd71ec3846482
BLAKE2b-256 42d040341683a9ad5755d644808d131cc8e898652e2460b047c35f4e13e76984

See more details on using hashes here.

Provenance

The following attestation bundles were made for nonecap-0.5.0-py3-none-any.whl:

Publisher: ci.yml on nonecap/nonecap-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.6.1

2 files

0.6.0

2 files

This release

0.5.0 This release

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page