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.4.0.tar.gz (86.1 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.4.0-py3-none-any.whl (16.0 kB view details)

Uploaded Python 3

File details

Details for the file undetecta-0.4.0.tar.gz.

File metadata

  • Download URL: undetecta-0.4.0.tar.gz
  • Upload date:
  • Size: 86.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.0

File hashes

Hashes for undetecta-0.4.0.tar.gz
Algorithm Hash digest
SHA256 b01d16958f30c718426630da8eec8dabf3dab38b17720776420c8fc30824c832
MD5 7d9df44fe2bfe203afe53147be0660a2
BLAKE2b-256 a2f990c61cfbd956fb5618748d11fec80d314737b1d058cd12b4cbd715649c6e

See more details on using hashes here.

File details

Details for the file undetecta-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: undetecta-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 16.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.0

File hashes

Hashes for undetecta-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 38f4c929f472448af82f6f62651cf12252f7cf2175e0b890f9f610b17dcf7955
MD5 5189a1f49ebfe0d17422222d6181eafc
BLAKE2b-256 84915c6fdff96b40543dcb68329599e05ef8939ff33c865930fffe4b68e37b87

See more details on using hashes here.

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