Python quickstart
Describes Python SDK 0.1.0
(legibl.__version__). Requires Python 3.8 or later. The SDK itself
has no runtime dependencies — it integrates with whichever HTTP
client you already use rather than bringing its own.
Install
The package is not yet on PyPI. Install it from a checkout, with the extra matching the client you use:
pip install '/path/to/legibl/sdk/python[httpx]' # or [requests]
The extras only pull in the HTTP client. pip install
/path/to/legibl/sdk/python with no extra installs the detection and
solver code alone, which is enough if you drive the API yourself.
Configure
export LEGIBL_API_KEY=your-key
With no key set the SDK runs in stub mode: it returns a
synthetic token and spends nothing. It also drops to stub mode when it detects a
test run — pytest or unittest imported,
PYTEST_CURRENT_TEST set, or APP_ENV/ENVIRONMENT
equal to test — because a suite that quietly bills a live solver on
every run is a common and expensive accident. Override with
LEGIBL_ALLOW_LIVE_IN_TESTS=1.
Solve a captcha with httpx
four lines
import httpx
from legibl.transport.httpx_transport import LegiblTransport
with httpx.Client(transport=LegiblTransport()) as client:
body = client.get("https://example.test/protected").text
LegiblTransport wraps whatever transport httpx would otherwise
use, so TLS, proxies, retries and connection pooling remain httpx's concern. It
takes an optional wrapped= transport if you already have a
configured one.
Solve a captcha with requests
five lines
import requests
from legibl.transport.requests_adapter import LegiblAdapter
session = requests.Session()
session.mount("https://", LegiblAdapter())
body = session.get("https://example.test/protected").text
LegiblAdapter is an HTTPAdapter subclass, mounted on
a session the ordinary way, so pooling, retries and TLS remain requests'
concern. Mount it on "http://" as well if you talk to plain-HTTP
targets.
A custom transport for httpx and an HTTPAdapter subclass for
requests — never a fork, never a monkey-patch. Public hooks are stable API
and internals are not, and that difference is the whole maintenance budget.
What gets inspected
Only bodies that can carry a challenge are examined. The requests adapter
skips any response whose Content-Type contains neither
html nor text; the httpx transport passes through any
body that will not decode as text. Binary payloads are never decoded
speculatively.
Confirm it is actually working
The SDK is deliberately quiet, so a broken integration and a clean run look identical. Attach an observer:
from legibl.config import Config
from legibl.interceptor import Interceptor
from legibl.observer import CallbackObserver
from legibl.transport.httpx_transport import LegiblTransport
observer = CallbackObserver(
lambda outcome, domain, secs: print(outcome.status, domain, round(secs, 2), outcome.reason),
lambda challenge, reason: print("solve failed:", challenge.captcha_type, reason),
)
transport = LegiblTransport(Interceptor.from_config(Config.from_environment(), observer))
The second callback is the one that matters when a solve does not happen: it
carries the service's errorCode in its message, and without an
observer that detail is discarded.
Log the resolved configuration too — it is built to be safe to log, reporting
api_key_set as a boolean and proxy domains only, never
credentials:
print(Config.from_environment().describe())
# {'mode': 'live', 'service_url': 'https://api.legibl.io', 'api_key_set': True, ...}
Detection without interception
If you drive requests yourself, the detector is a pure function over HTML. It never touches the network and never returns a token — tokens are issued by the challenge provider and cannot be derived from a saved page.
from legibl import detect
result = detect(html, "https://example.test/protected")
result.extractable # bool — False is a normal outcome, not an error
result.captcha_type # recaptcha_v2 | recaptcha_v2_invisible | turnstile | image_ocr
result.sitekey
result.pageurl
result.invisible # reCAPTCHA only — never set for Turnstile
result.submit_field
result.action # Turnstile
result.cdata # Turnstile
result.reason # why, when extractable is False
extractable is False with a captcha_type set means a
challenge was correctly identified and is out of reach of form injection.
Treating that as “no captcha here” is how a target silently stops
working — see
unreachable challenges.
Next
- Configuration & modes — every environment variable, the four modes, proxies and observability.
- Errors & outcomes — each typed error and its corrective action.
- HTTP API — for image OCR, which the SDK detects but does not yet solve for you.