Metadata-Version: 2.4
Name: techrecon
Version: 0.2.0
Summary: Python client for the TechRecon JA4 + web-crawl technology intelligence API
Author: TechRecon
License: MIT
Project-URL: Homepage, https://techrecon.whoisxmlapi.com
Project-URL: Documentation, https://techrecon.whoisxmlapi.com/api/v1/docs
Keywords: techrecon,ja4,technology-detection,fingerprinting
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Dynamic: license-file

# techrecon (Python client)

Python client for the [TechRecon](https://techrecon.whoisxmlapi.com) JA4 fingerprinting +
web-crawl technology intelligence API. Generated from `docs/openapi.yaml`
(spec version 1.1.0).

- **Zero runtime dependencies** — built on `urllib.request` from the standard
  library only.
- **Typed** — one method per endpoint, `TypedDict` response shapes, typed
  exceptions per HTTP status code.
- Requires Python **3.10+**.

> **Version notes:** avoid 0.1.0 — it was built from a stale tree and its
> default base URL points at the dead `api.techrecon.io` host, so it fails out
> of the box unless you pass `base_url` explicitly; 0.1.1 and later default to
> the live `/api` edge. WhoisXML API account keys need 0.2.0 or later, which
> sends them as `?apiKey=` by default (below). See [`CHANGELOG.md`](./CHANGELOG.md).

## Install

```bash
pip install techrecon
```

Or install from a local checkout:

```bash
cd sdk/python
pip install -e .
```

## 5-minute quickstart

### 1. Get an API key

Every endpoint except health checks and docs requires an API key. There are
two kinds, and they are sent differently:

| Key                                                                                                   | How it is sent          | Client option        |
| ----------------------------------------------------------------------------------------------------- | ----------------------- | -------------------- |
| **WhoisXML API account key** — from your WhoisXML API account, billed in credits                      | `?apiKey=` query string | `auth_mode="query"`  |
| **TechRecon-issued key** (`tr_live_…`) — minted in the TechRecon dashboard or `POST /v1/account/keys` | `X-API-Key` header      | `auth_mode="header"` |

The default, `auth_mode="auto"`, picks by the key: one starting with `tr_` goes
in the header, any other key in the query string, so both kinds work without
setting it (`client.auth_mode` shows which was chosen). A WhoisXML API account
key sent as a header is rejected. Account-scoped methods (account, usage, API
keys, watchlist, exports) currently need a TechRecon-issued key.

In query mode the key is part of the request URL, so proxies and server
access logs along the way can record it. The client keeps it out of its own
`repr` and error messages.

### 2. Create a client

```python
from techrecon import TechRecon

# WhoisXML API account key: sent as ?apiKey=
client = TechRecon(api_key="<YOUR_WHOISXML_API_KEY>")

# TechRecon-issued key (tr_live_...): sent as the X-API-Key header
client = TechRecon(api_key="<YOUR_TECHRECON_API_KEY>")
```

Point at a local dev server instead of production with `base_url`. A local
server has no account platform behind it, so use one of its own keys
(`JA4_API_KEY`) in header mode, which such a key (no `tr_` prefix) needs
explicitly:

```python
client = TechRecon(api_key="<YOUR_LOCAL_KEY>", auth_mode="header", base_url="http://localhost:8080")
```

### 3. Your first lookup

Look up a domain's detected technology stack:

```python
domain = client.get_domain("wordpress.org")
print(domain["tech_count"], "technologies detected")
for tech in domain["technologies"]:
    print(f"  {tech['technology']} ({tech['confidence']:.2f} confidence)")
```

```text
4 technologies detected
  WordPress (0.99 confidence)
  PHP (0.95 confidence)
```

Or look up a JA4 TLS fingerprint directly:

```python
result = client.lookup("t13d1516h2_8daaf6152771_b0da82dd1658", ip="93.184.216.34")
if result["ja4_tech_match"]:
    print(result["ja4_tech_match"]["technology"])  # "nginx"
else:
    print("no mapping for this fingerprint yet")
```

### 4. Handle errors

Every non-2xx response raises a typed exception carrying the status code
and the API's `{"error": "..."}` message:

```python
from techrecon import NotFoundError, RateLimitError, TechRecon

client = TechRecon(api_key="<YOUR_WHOISXML_API_KEY>")
try:
    client.get_domain("does-not-exist.example")
except NotFoundError as exc:
    print(f"not found: {exc.message}")
except RateLimitError as exc:
    print(f"rate limited (429): {exc.message}")
```

See [Exceptions](#exceptions) below for the full hierarchy.

### 5. That's it

You now have a working client. See [Endpoints covered](#endpoints-covered)
below for the full method list, or browse the interactive spec at
<https://techrecon.whoisxmlapi.com/api/v1/docs> (`GET /v1/docs` relative to
the API base).

## More examples

**Bulk domain lookup** (up to 100 domains per call):

```python
result = client.bulk_domain_lookup(["example.com", "wordpress.org"])
for r in result["results"]:
    print(r["domain"], r["tech_count"])
```

**Search by technology:**

```python
page = client.search(technology="WordPress", min_confidence=0.8, limit=50)
for hit in page["results"]:
    print(hit["domain"])
if page["has_more"]:
    next_page = client.search(technology="WordPress", cursor=page["next_cursor"])
```

**Change feed** (technology adds/removes/updates in the last N days):

```python
changes = client.get_changes(since="2026-06-01T00:00:00Z", technology="WordPress")
for event in changes["changes"]:
    print(event["domain"], event["change_type"], event["technology"])
```

**Reverse pivot from a JA4 hash to the domains that present it:**

```python
page = client.pivot_ja4("t13d1516h2_8daaf6152771_02713d6af862")
print(page["summary"]["total_domains"], "domains observed with this fingerprint")
```

**Create a Slack alert on any technology change for a domain:**

```python
alert = client.create_alert(
    channel="slack",
    endpoint="https://hooks.slack.com/services/XXX/YYY/ZZZ",
    domain="example.com",
)
print(alert["id"])
```

## Endpoints covered

This client covers the stable, public surface of `docs/openapi.yaml`.
Endpoints marked `x-internal: true` in the spec — self-serve
signup/email-verification (beta, not GA), Stripe billing (beta, being
replaced by the NAF gateway), and the privacy admin routes — are
intentionally **not** wrapped here; they're beta or admin-only per the
spec's own descriptions. Everything else is covered:

| Area           | Methods                                                                                                             |
| -------------- | ------------------------------------------------------------------------------------------------------------------- |
| Health         | `health`, `readiness`                                                                                               |
| JA4 Lookup     | `lookup`, `bulk_lookup`, `stats`, `top_unknown`                                                                     |
| Domain         | `get_domain`, `bulk_domain_lookup`, `get_domain_history`, `get_domain_changes`                                      |
| Change feed    | `get_changes`                                                                                                       |
| Search         | `search`                                                                                                            |
| Technologies   | `get_tech_stats`, `get_tech_trend`                                                                                  |
| Exports        | `create_export`, `get_export_status`, `download_export`                                                             |
| System         | `get_system_stats`, `get_system_health`                                                                             |
| Ingestion      | `ingest_crawl_results` (system identity only — 403 for tenant keys)                                                 |
| Crawl requests | `create_crawl_request`, `list_pending_crawl_requests`, `get_crawl_request_status`                                   |
| Priority crawl | `submit_priority_crawl`, `get_priority_crawl_status`                                                                |
| IP lookup      | `ip_batch_lookup`                                                                                                   |
| Pivots         | `pivot_ja4`, `pivot_jarm`, `pivot_favicon`, `pivot_analytics_ga`, `pivot_analytics_gtm`, `pivot_analytics_fb_pixel` |
| Alerts         | `create_alert`, `list_alerts`, `delete_alert`                                                                       |
| Auth           | `login`, `logout`                                                                                                   |
| Account        | `get_account`, `list_api_keys`, `issue_api_key`, `revoke_api_key`, `get_account_usage`                              |

## Exceptions

All exceptions inherit from `techrecon.TechReconError`. HTTP errors raise
`techrecon.APIError` subclasses, each carrying `.status_code`, `.message`
(the API's `error` field), and `.body` (raw response text). A WhoisXML API
account key that the account platform rejects or throttles gets a nested
`{"error": {"code": ..., "message": ...}}` body; `.message` is then
`"<code>: <message>"`, e.g. `"auth_failed: API key rejected by ..."`.

| Exception                 | HTTP status                                                                                                              |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `BadRequestError`         | 400                                                                                                                      |
| `AuthenticationError`     | 401                                                                                                                      |
| `ForbiddenError`          | 403                                                                                                                      |
| `NotFoundError`           | 404                                                                                                                      |
| `ConflictError`           | 409                                                                                                                      |
| `PayloadTooLargeError`    | 413                                                                                                                      |
| `RateLimitError`          | 429 (rate limiting **and** quota exhaustion — the spec has no 402; 429 is used uniformly, e.g. alert quota, API-key cap) |
| `ServiceUnavailableError` | 503 (a required backend isn't configured; for a crawl submission, nothing on this deployment would process it)           |

## Development

```bash
cd sdk/python
python3 -m py_compile src/techrecon/*.py tests/*.py   # syntax check
python3 -m pytest tests/                               # run tests (network-free)
```

Tests use a fake in-memory transport (`tests/fake_transport.py`) — no
network access or live API key is required to run them.

## License

MIT — see [`LICENSE`](./LICENSE).
