Metadata-Version: 2.4
Name: browsa
Version: 0.1.3
Summary: Official Python SDK for Browsa — anti-detect cloud browsers for AI agents (browsa.io)
Author-email: Browsa <support@browsa.io>
License: MIT
Project-URL: Homepage, https://browsa.io
Project-URL: Documentation, https://browsa.io/docs
Project-URL: Source, https://pypi.org/project/browsa/
Project-URL: Issues, https://browsa.io/docs
Keywords: browser-automation,ai-agents,browser-use,anti-detect,captcha-solver,scraping,playwright
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
Requires-Dist: ruff>=0.1; extra == "dev"
Dynamic: license-file

# browsa (Python)

Anti-detect browsers for AI agents — official Python SDK for [browsa.io](https://browsa.io).

[![PyPI version](https://img.shields.io/pypi/v/browsa.svg)](https://pypi.org/project/browsa/)
[![Python](https://img.shields.io/pypi/pyversions/browsa.svg)](https://pypi.org/project/browsa/)

## Install

```bash
pip install browsa
```

Zero dependencies. Pure stdlib. Single-file wheel.

## Five-line hello world

```python
from browsa import Client

c = Client(api_key="agt_live_...")
job = c.run_task(
    task="Go to news.ycombinator.com and return the top story title.",
    llm="claude-opus-4-7",
    llm_api_key="sk-ant-...",
)
print(job.final_result)
print("watch live:", job.live_url)
```

That runs a real Chromium fork (Phantom — our anti-detect browser) inside an isolated burner session, driven by [browser-use](https://browser-use.com) + Claude Opus. The agent navigates, extracts, and returns. A live noVNC stream URL is available the moment the burner spawns.

## Async

```python
import asyncio
from browsa import AsyncClient

async def main():
    async with AsyncClient(api_key="agt_live_...") as c:
        job = await c.run_task(
            task="Scrape the top 5 products on producthunt.com today.",
            llm="claude-opus-4-7",
            llm_api_key="sk-ant-...",
            country="US",
        )
        print(job.final_result)

asyncio.run(main())
```

## Webhooks

```python
from browsa import webhooks

# In your receiver (Flask/FastAPI/whatever):
ok = webhooks.verify_signature(
    body=raw_request_body,
    header=request.headers["X-Agents-Signature"],
    secret=os.environ["BROWSA_WEBHOOK_SECRET"],  # hex-encoded
)
if not ok:
    return 401
# Trust the payload only after this point.
```

## What you get

| Capability | Notes |
|---|---|
| Sync + async clients | `Client` + `AsyncClient`, same surface |
| Real `Job` envelope | Typed dataclass + `.raw` escape hatch |
| Long-poll waits | `wait_for_job(timeout=...)` uses server-side `?wait=30` |
| Webhook signature verify | HMAC-SHA256, constant-time compare, skew check |
| Canonical error envelope | `AuthError`, `InvalidIDError`, `RateLimitError`, etc. |
| `X-Request-Id` propagation | Every error carries the server's request id |
| Zero deps | Single wheel, audit-friendly, no transitive surface |

## Advanced

### Long-running tasks with progress callback

```python
def show(job):
    print(f"  step {job.step_count}/{job.progress_total}  status={job.status}")

job = c.run_task(
    task="Open-ended research: scrape arxiv for papers on test-time compute, return top 10 by citation.",
    llm="claude-opus-4-7",
    llm_api_key="sk-ant-...",
    max_steps=80,
    on_progress=show,
)
```

### Interactive (input_required) tasks

If your task can pause for human input (captcha, decision point), watch for status `input_required` and respond:

```python
job = c.create_task(task="...", llm="...", llm_api_key="...")
while not job.is_terminal:
    job = c.get_job(job.id, wait=30)
    if job.status == "input_required":
        answer = input(f"Agent asks: {job.input_question}\n> ")
        job = c.respond_to_job(job.id, answer)
```

### JA3-spoofed sessions

Some sites fingerprint TLS handshake shape. Set a profile to spoof macOS Chrome:

```python
job = c.run_task(
    task="Go to a TLS-fingerprint-fingerprinted site",
    llm="claude-opus-4-7", llm_api_key="sk-...",
    ja3_profile="macos_chrome_137",  # uses utls
)
```

## Docs

- API reference: <https://browsa.io/docs>
- Source: <https://github.com/mohasaaid/neout/tree/main/sdks/browsa-python>
- Issues: <https://github.com/mohasaaid/neout/issues>

## License

MIT.
