Skip to main content

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.

PyPI version Python License: MIT

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 httpx for 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.

  1. Bump version in pyproject.toml and add a CHANGELOG.md entry.
  2. 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

undetecta-0.5.0.tar.gz (88.7 kB view details)

Uploaded Source

Built Distribution

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

undetecta-0.5.0-py3-none-any.whl (17.0 kB view details)

Uploaded Python 3

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

Hashes for undetecta-0.5.0.tar.gz
Algorithm Hash digest
SHA256 768d45ea8e1367590340be477994d2eb9c05209f5e5a2f813634bff5fb1e30d3
MD5 28a0ca910de8e69f874ca2d02c7f76da
BLAKE2b-256 0374c970b1d0e0ae4083941182d33582045479fde06ffeb841b3a7e69db6acc5

See more details on using hashes here.

Provenance

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

Publisher: publish-sdks.yml on mikipavlov/undetecta

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

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

Hashes for undetecta-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f17de91e938ffd2924c1c1c9c88bc8163ff8d3b21396853fca3f1acb351c4859
MD5 0944f99d11db46fcecd286ac0a4630f7
BLAKE2b-256 e127e00ef330dc50aa76475327976bd13a6120909e60aa667980bf9444cede93

See more details on using hashes here.

Provenance

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

Publisher: publish-sdks.yml on mikipavlov/undetecta

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

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page