Python client for the Undetecta API - Anti-detection web scraping made simple
Project description
undetecta
Python client for the Undetecta API - Anti-detection web scraping made simple.
Features
- Full Type Hints - Comprehensive Pydantic v2 types for all API responses and requests
- Web Scraping - Scrape URLs with advanced options (screenshots, branding extraction, actions)
- Web Search - Search the web with result scraping capabilities
- Automatic Retries - Built-in retry logic with exponential backoff for transient failures
- Error Handling - Custom error classes for different error types
- Async First - Built on
httpxfor async/await operations - Context Manager - Automatic resource cleanup with async context manager support
Installation
pip install undetecta
Or using uv:
uv add undetecta
Quick Start
import asyncio
from undetecta import UndetectaClient
async def main():
async with UndetectaClient(api_key="your-api-key") as client:
result = await client.scrape(url="https://example.com")
print(result.markdown)
asyncio.run(main())
Configuration
from undetecta import UndetectaClient
client = UndetectaClient(
api_key="your-api-key", # Required
base_url="https://api.undetecta.com", # Optional, defaults to production
timeout=60000, # Optional, default 60 seconds (ms)
max_retries=3, # Optional, default 3 retries
)
API Reference
Scraping
scrape(url, formats=None, **kwargs)
Scrape a URL and return the results.
result = await client.scrape(
url="https://example.com",
formats=["markdown", "screenshot"],
wait_for_selector=".main-content",
)
Scrape Options:
| Option | Type | Default | Description |
|---|---|---|---|
url |
str |
Required | URL to scrape |
formats |
list[str] |
["markdown"] |
Output formats: html, rawHtml, markdown, links, screenshot, branding |
headless |
bool |
True |
Run browser in headless mode |
proxy |
str |
- | Proxy URL to use |
wait_for_selector |
str |
- | CSS selector to wait for |
wait_for |
int |
- | Time to wait in ms |
wait_until |
str |
- | Wait until page load event (load, domcontentloaded, networkidle) |
timeout |
int |
30000 |
Request timeout in ms |
mobile |
bool |
- | Use mobile viewport |
include_response_headers |
bool |
False |
Include main document response headers |
include_cookies |
bool |
False |
Include cookies affecting the final URL |
actions |
list[Action] |
- | Browser actions to perform |
screenshot_options |
ScreenshotOptions |
- | Screenshot configuration |
Browser Actions:
result = await client.scrape(
url="https://example.com",
actions=[
{"type": "click", "selector": ".cookie-accept"},
{"type": "fill", "selector": "input[name='email']", "value": "test@example.com"},
{"type": "wait", "options": {"duration": 1000}},
{"type": "scroll", "options": {"direction": "down", "amount": 500}},
],
)
Response:
class ScrapeJobResponse(BaseModel):
id: str
status: JobStatus # pending, running, completed, failed, stopped
created_at: str
completed_at: str | None
metadata: ScrapeMetadata | None
html: str | None
raw_html: str | None
markdown: str | None
links: list[str] | None
screenshot: str | None # base64 encoded image
branding: BrandingProfile | None
response_headers: dict[str, str] | None
cookies: list[ScrapeCookie] | None
error: ScrapeJobError | None
Search
search(query, limit=None, **kwargs)
Perform a web search and return the results.
result = await client.search(
query="web scraping tools",
limit=10,
scrape_options={"formats": ["markdown"]},
)
for item in result.web:
print(item.title)
print(item.markdown) # Scraped content
Search Options:
| Option | Type | Default | Description |
|---|---|---|---|
query |
str |
Required | Search query |
limit |
int |
10 |
Number of results |
categories |
list[str] |
- | github, research, pdf |
lang |
str |
- | Language code |
country |
str |
- | Country code |
scrape_options |
SearchScrapeOptions |
- | Options for scraping results |
timeout |
int |
- | Timeout in ms |
Response:
class SearchJobResponse(BaseModel):
id: str
status: JobStatus
created_at: str
completed_at: str | None
web: list[WebSearchResult] | None
news: list[NewsSearchResult] | None
images: list[ImageSearchResult] | None
error: SearchJobError | None
Health Check
health()
Check the API health status.
health = await client.health()
print(health.status) # 'ok'
Error Handling
The client provides custom error classes for different error types:
from undetecta.errors import (
ApiKeyError,
RateLimitError,
TimeoutError,
NetworkError,
ValidationError,
NotFoundError,
UndetectaError,
)
async with UndetectaClient(api_key="your-api-key") as client:
try:
result = await client.scrape(url="https://example.com")
except ApiKeyError as e:
print(f"Invalid API key: {e.message}")
except RateLimitError as e:
print(f"Rate limit exceeded: {e.message}")
except TimeoutError as e:
print(f"Request timed out: {e.message}")
except NetworkError as e:
print(f"Network error: {e.message}")
except ValidationError as e:
print(f"Validation error: {e.message}")
except NotFoundError as e:
print(f"Resource not found: {e.message}")
except UndetectaError as e:
print(f"API error: {e.code} - {e.message}")
if e.status_code:
print(f"Status: {e.status_code}")
Error Classes
| Error Class | Status Code | Description |
|---|---|---|
ApiKeyError |
401 | Invalid or missing API key |
ValidationError |
400 | Request validation failed |
NotFoundError |
404 | Resource not found |
RateLimitError |
429 | Rate limit exceeded |
UndetectaError |
Variable | Base error class (includes status code for server errors) |
TimeoutError |
- | Request timed out |
NetworkError |
- | Network connection failed |
Advanced Examples
Extract Branding
result = await client.scrape(
url="https://example.com",
formats=["branding"],
)
print(result.branding.colors.primary)
print(result.branding.typography.font_families)
print(result.branding.components.button_primary)
Screenshot with Custom Options
from undetecta.types import ScreenshotOptions, ScreenshotClip, ScreenshotViewport
result = await client.scrape(
url="https://example.com",
formats=["screenshot"],
screenshot_options=ScreenshotOptions(
full_page=False,
format="png",
quality=90,
clip=ScreenshotClip(x=0, y=0, width=1920, height=1080),
selector=".main-content",
viewport=ScreenshotViewport(width=1920, height=1080),
),
)
# Save screenshot
import base64
with open("screenshot.png", "wb") as f:
f.write(base64.b64decode(result.screenshot))
Search with Result Scraping
result = await client.search(
query="typescript web scraping",
limit=5,
scrape_options={
"formats": ["markdown", "links"],
"only_main_content": True,
},
)
for item in result.web or []:
print(item.title)
print(item.url)
if item.markdown:
print(item.markdown[:200] + "...")
Custom Error Handling with Retry
import asyncio
async def scrape_with_retry(url, max_retries=3):
async with UndetectaClient(api_key="your-api-key") as client:
for attempt in range(max_retries):
try:
return await client.scrape(url=url)
except RateLimitError as e:
if attempt < max_retries - 1:
delay = 2 ** attempt # Exponential backoff
await asyncio.sleep(delay)
continue
raise
Development
Install development dependencies:
uv sync
Run linting and formatting:
uv run ruff check src
uv run ruff format src
Run type checking:
uv run pyright src
Run tests:
uv run pytest
Publishing
Published to PyPI as undetecta.
- Bump
versioninpyproject.tomland add aCHANGELOG.mdentry. - Build and publish (provide a PyPI API token — starts with
pypi-):
cd sdks/python
uv build # -> dist/undetecta-<version>.{tar.gz,whl}
UV_PUBLISH_TOKEN=pypi-XXXX uv publish # or: uv publish --token pypi-XXXX
Create the token at https://pypi.org/manage/account/token/. Verify with
uv pip index versions undetecta.
Note: publishing a version is permanent — PyPI does not allow re-uploading the same version (only yanking).
Automated (CI): after bumping the version, push a tag sdk-py-v<version>
(e.g. sdk-py-v0.3.0) to trigger .github/workflows/publish-sdks.yml. Auth is
OIDC Trusted Publishing — no token/secret. One-time setup: add a GitHub
publisher for the undetecta project on PyPI (Settings → Publishing → owner
mikipavlov, repo undetecta, workflow publish-sdks.yml). The workflow
verifies the tag matches pyproject.toml.
Requirements
- Python: >= 3.10
- httpx: >= 0.27.0
- pydantic: >= 2.0.0
Schema Documentation
For information about how the Pydantic models relate to the JSON schemas and how to regenerate types, see SCHEMAS.md.
License
MIT © Undetecta
Links
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file undetecta-0.5.0.tar.gz.
File metadata
- Download URL: undetecta-0.5.0.tar.gz
- Upload date:
- Size: 88.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
768d45ea8e1367590340be477994d2eb9c05209f5e5a2f813634bff5fb1e30d3
|
|
| MD5 |
28a0ca910de8e69f874ca2d02c7f76da
|
|
| BLAKE2b-256 |
0374c970b1d0e0ae4083941182d33582045479fde06ffeb841b3a7e69db6acc5
|
Provenance
The following attestation bundles were made for undetecta-0.5.0.tar.gz:
Publisher:
publish-sdks.yml on mikipavlov/undetecta
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
undetecta-0.5.0.tar.gz -
Subject digest:
768d45ea8e1367590340be477994d2eb9c05209f5e5a2f813634bff5fb1e30d3 - Sigstore transparency entry: 2146408830
- Sigstore integration time:
-
Permalink:
mikipavlov/undetecta@e831c8e80884cb2e681af5313c3293938fa0ef91 -
Branch / Tag:
refs/tags/sdk-py-v0.5.0 - Owner: https://github.com/mikipavlov
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-sdks.yml@e831c8e80884cb2e681af5313c3293938fa0ef91 -
Trigger Event:
push
-
Statement type:
File details
Details for the file undetecta-0.5.0-py3-none-any.whl.
File metadata
- Download URL: undetecta-0.5.0-py3-none-any.whl
- Upload date:
- Size: 17.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f17de91e938ffd2924c1c1c9c88bc8163ff8d3b21396853fca3f1acb351c4859
|
|
| MD5 |
0944f99d11db46fcecd286ac0a4630f7
|
|
| BLAKE2b-256 |
e127e00ef330dc50aa76475327976bd13a6120909e60aa667980bf9444cede93
|
Provenance
The following attestation bundles were made for undetecta-0.5.0-py3-none-any.whl:
Publisher:
publish-sdks.yml on mikipavlov/undetecta
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
undetecta-0.5.0-py3-none-any.whl -
Subject digest:
f17de91e938ffd2924c1c1c9c88bc8163ff8d3b21396853fca3f1acb351c4859 - Sigstore transparency entry: 2146408834
- Sigstore integration time:
-
Permalink:
mikipavlov/undetecta@e831c8e80884cb2e681af5313c3293938fa0ef91 -
Branch / Tag:
refs/tags/sdk-py-v0.5.0 - Owner: https://github.com/mikipavlov
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-sdks.yml@e831c8e80884cb2e681af5313c3293938fa0ef91 -
Trigger Event:
push
-
Statement type: