Metadata-Version: 2.5
Name: web-auditor
Version: 0.1.0
Summary: Official Python client for the Web Auditor public API: AI-visibility (AEO/GEO) audits of web pages.
Project-URL: Documentation, https://api.web-auditor.enfection.com/docs
Author: Enfection
License-Expression: MIT
License-File: LICENSE
Keywords: aeo,ai-search,api,geo,seo,web-auditor
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1,>=0.25
Provides-Extra: test
Requires-Dist: pytest>=8; extra == 'test'
Description-Content-Type: text/markdown

# Web Auditor Python SDK

The official Python client for the [Web Auditor public API](https://api.web-auditor.enfection.com/docs): audit how
well a page can be found, read and cited by AI answer engines.

```bash
pip install web-auditor
```

Requires Python 3.9+.

## Quick start

```python
from web_auditor import WebAuditor

client = WebAuditor(api_key="wa_test_…")  # or set WEB_AUDITOR_API_KEY

audit = client.url_audits.create(url="https://example.com/pricing")
audit = client.url_audits.wait(audit.id)

print(audit.status, audit.report.score)
for check in audit.report.checks:
    if check.status == "fail":
        print(check.id, check.fix)
```

Test keys (`wa_test_…`) return a realistic sample report immediately and cost nothing — build your integration with
one, then switch to a live key.

Responses are `ApiObject`s: dicts that also allow attribute access (`audit.report.score` or
`audit["report"]["score"]`). `to_dict()` returns plain dicts.

## What's covered

| Resource | Methods |
|---|---|
| `client.url_audits` | `create`, `retrieve`, `list`, `cancel`, `artifacts`, `report_pdf`, `wait` |
| `client.url_audit_batches` | `create`, `retrieve`, `list`, `cancel`, `wait` |
| `client.report_shares` | `create`, `list`, `revoke` |
| `client.site_audits` | `create`, `retrieve`, `list`, `list_pages`, `cancel`, `wait` |
| `client.brand_audits` | `create`, `retrieve`, `list`, `wait` |
| `client.monitors` | `create`, `retrieve`, `list`, `update`, `pause`, `resume`, `delete`, `run`, `list_runs` |
| `client.webhook_endpoints` | `create`, `retrieve`, `list`, `delete`, `send_test_event`, `rotate_secret` |
| `client.webhook_deliveries` | `list`, `retrieve`, `replay` |
| `client.usage`, `client.status` | `retrieve` |
| `client.checks` | `list` |

### GEO probes and batches

```python
audit = client.url_audits.create(
    url="https://example.com/pricing",
    geo={"engines": ["chatgpt", "gemini"], "prompt_count": 3},
)

batch = client.url_audit_batches.create(urls=["https://example.com/a", "https://example.com/b"])
batch = client.url_audit_batches.wait(batch.id)
for member in batch.audits:
    print(member.url, member.status, member.score)
```

### Site audits

```python
site = client.site_audits.create(url="https://example.com/docs/", max_pages=50)
site = client.site_audits.wait(site.id)
for page in client.site_audits.list_pages(site.id).auto_paging_iter():
    print(page.url, page.status, page.audit and page.audit.score)

print(site.report.score)
for issue in site.report.top_issues:
    print(issue.id, issue.failed, issue.examples)
```

Pages are found from sitemaps, respect robots.txt and are audited one at a time per site, a few seconds apart.

### Brand audits

```python
brand = client.brand_audits.create(
    brand="Acme Payroll",
    domain="acmepayroll.com",
    category="payroll software for small businesses",
    competitors=[{"name": "Gusto", "domain": "gusto.com"}],
)
brand = client.brand_audits.wait(brand.id)
print(brand.report.summary.brand.share_of_voice, brand.report.summary.gaps)
```

### Monitors

```python
monitor = client.monitors.create(url="https://example.com/pricing", cadence="weekly", score_drop_threshold=5)
client.monitors.update(monitor.id, geo=None)  # only the fields you pass change; None clears geo
client.monitors.pause(monitor.id)
```

Each run is compared with the previous completed run; `run.regressions` lists what got worse (and the `monitor.regression_detected` webhook announces it).

```python
for run in client.monitors.list_runs(monitor.id).auto_paging_iter():
    for regression in run.regressions or []:
        print(run.id, regression.type)  # score_drop, ai_bot_blocked, schema_removed, citation_lost, render_regression
```

### Pagination

`list()` returns one page (`page.data`, `page.has_more`); `auto_paging_iter()` walks all of them:

```python
for audit in client.url_audits.list(status="completed").auto_paging_iter():
    print(audit.id, audit.url)
```

## Errors, retries and idempotency

Network errors, `429` and `5xx` responses are retried twice (`max_retries=`) with backoff, honouring `Retry-After`.
A `Retry-After` longer than a minute — such as a site's hourly audit limit — is raised straight away so you can
decide. Every POST sends an `Idempotency-Key` (pass `idempotency_key=` to choose it), so a retry can never start a
second audit.

```python
from web_auditor import InsufficientCreditsError, RateLimitError, WebAuditorError

try:
    client.url_audits.create(url="https://example.com")
except InsufficientCreditsError:
    ...
except RateLimitError as error:
    print(error.code, error.retry_after)  # rate_limited | host_rate_limit | concurrent_audit_limit
except WebAuditorError as error:
    print(error.status, error.code, error.message, error.request_id)
```

| Exception | When |
|---|---|
| `InvalidRequestError` / `ConflictError` | 400, 409 (`param` names the field) |
| `AuthenticationError` | 401 |
| `InsufficientCreditsError` | 402 |
| `PermissionDeniedError` | 403 |
| `NotFoundError` | 404 |
| `RateLimitError` | 429 |
| `APIError` | 5xx |
| `APIConnectionError` / `APITimeoutError` | the API couldn't be reached |
| `PollTimeoutError` | `wait()` ran out of time (`error.last` is the latest state) |

## Webhooks

Verify every delivery with the endpoint's signing secret, over the **raw** request body:

```python
from web_auditor import webhooks

@app.post("/webhooks/web-auditor")
def receive(request):
    try:
        event = webhooks.construct_event(request.body, request.headers.get("WA-Signature"), WEBHOOK_SECRET)
    except webhooks.WebhookSignatureError:
        return Response(status=400)
    if event.type == "url_audit.completed":
        ...
    return Response(status=204)
```

Deliveries can arrive more than once (retries and replays keep the same `event.id`), so deduplicate on it.

## Configuration

```python
WebAuditor(
    api_key="wa_live_…",
    base_url="https://api.web-auditor.enfection.com",  # or WEB_AUDITOR_BASE_URL
    timeout=60,
    max_retries=2,
    http_client=None,  # your own httpx.Client (proxies, custom transport)
)
```

`WebAuditor` can be used as a context manager to close its connections.

## Development

```bash
pip install -e '.[test]'
pytest
```

`tests/test_contract.py` checks the SDK against `../openapi.json`, which the backend regenerates with
`python manage.py export_public_openapi`.

## License

MIT — see [LICENSE](LICENSE). Release notes are in [CHANGELOG.md](CHANGELOG.md).
