# Python SDK

Official osintverse package for the OSINTOne API — install, auth, create, wait, bulk, unlock, premium, and errors.

URL: https://osintverse.com/docs/python

Official client for the **OSINTOne API** on osintverse.com. Same providers, prices, and prepaid wallet as the SearchIn UI.

```bash
pip install osintverse
```

Requires &#x2A;*Python 3.10+**. Package: [osintverse on PyPI](https://pypi.org/project/osintverse/). Source: [osintverse-pythonsdk](https://github.com/dheerajydv19/osintverse-pythonsdk).

It talks to `https://apiv1.osintverse.com`. It is not affiliated with other products that reuse the OSINTverse name.

> **OSINTOne API only.** This SDK covers SearchIn lookups (create, poll, bulk, LeakRadar unlock, OSINT Industries premium). GraphIn — seed, canvas, path, share — stays in the [GraphIn](https://osintverse.com/graphin) UI. Create keys in [Dashboard → API keys](https://osintverse.com/dashboard/api); there is no key-management API.

## Authenticate

Sign in once from a terminal. It shows a code, you approve it at [/device](https://osintverse.com/device) while signed in, and a new key is saved to the config file the JS CLI also reads (`%APPDATA%\osintverse` on Windows, `~/.config/osintverse` elsewhere), so `npx osintverse login` works for both:

```bash
python -m osintverse login     # --json for agents, --no-browser on servers
python -m osintverse whoami
python -m osintverse logout    # forgets the saved key; revoke it in the dashboard
```

Or create a key (prefix `ov_`) and copy it once:

```bash
export OSINTVERSE_API_KEY=ov_your_api_key
```

```python
from osintverse import OSINTverse

client = OSINTverse()  # api_key="ov_…", else OSINTVERSE_API_KEY, else the saved key
```

`device_login()` runs the same sign-in from code. Requires `osintverse` 0.2.0 or later.

Close the HTTP client when you are done, or use a context manager:

```python
with OSINTverse() as client:
    search = client.search.create(
        provider="leakradar",
        input_type="email",
        query="user@example.com",
        wait=True,
    )
```

`client.health()` and `client.providers.list()` work without a key. Search methods require one. Optional: `OSINTverse(base_url="https://apiv1.osintverse.com", timeout=60)`.

To integrate against sample data, point `base_url` at the [API sandbox](https://osintverse.com/developers/sandbox) and pass `api_key="ov_sandbox"`. Those routes return fixed results and do not bill.

## First search

```python
search = client.search.create(
    provider="leakradar",
    input_type="email",
    query="user@example.com",
    wait=True,
)
print(search.id, search.status, search.cost_usd, search.balance_after)
if search.result:
    for match in search.result.matches:
        print(match)
```

Quote first when cost matters. `quote()` never charges:

```python
quote = client.search.quote(provider="leakradar", input_type="email", count=2)
if not quote.sufficient_balance:
    print(f"Add ${quote.shortfall_usd} at {quote.top_up_url}")
```

Every `create` sends an `Idempotency-Key`: a fresh UUID unless you pass `idempotency_key=`. Pass your own and reuse it when you retry after a timeout, and the API returns the original search instead of charging again.

Pick a valid `provider` + `input_type` from `client.providers.list()` or the [pricing matrix](https://osintverse.com/docs/pricing). Invalid pairs raise `ValidationError` (HTTP **422**).

Most providers finish in the create response. FaceCheck image jobs, Hunter Email Verifier, and OSINT Industries lookups can return `running` — `wait=True` polls every \~2 seconds (default timeout 120s). OSINT Industries `result.matches` grows while the job is still running.

Hunter.io **name** queries must be `Full Name example.com` (person name, then company domain).

## Methods

| SDK                                       | HTTP                           |
| ----------------------------------------- | ------------------------------ |
| `client.health()`                         | `GET /health`                  |
| `client.providers.list()`                 | `GET /v1/providers`            |
| `client.search.create(...)`               | `POST /v1/search`              |
| `client.search.bulk(...)`                 | `POST /v1/search/bulk`         |
| `client.search.retrieve(id)` / `.get(id)` | `GET /v1/search/{id}`          |
| `client.search.wait(id)`                  | poll retrieve until terminal   |
| `client.search.unlock(id)`                | `POST /v1/search/{id}/unlock`  |
| `client.search.premium(id)`               | `POST /v1/search/{id}/premium` |

`create` / `bulk` kwargs: `premium=False`, `wait=False`, `poll_interval=2`, `timeout=120`.

Stop an in-flight OSINT Industries job with `POST /v1/search/{id}/cancel` (HTTP; not a dedicated SDK method yet). Modules already returned stay on the job and the original charge is kept.

Without `wait`, inspect `search.status` and poll yourself:

```python
search = client.search.create(
    provider="facecheck",
    input_type="image",
    query="https://example.com/photo.jpg",
)
if search.running:
    search = client.search.wait(search.id)
```

Each poll counts toward the **1,000 requests/day/key** limit. See [authentication](https://osintverse.com/docs/authentication).

## Results

A `Search` object mirrors the API job: `id`, `status`, `provider`, `input_type`, `query`, `cost_usd`, `balance_after`, `error`, `poll_url`, `result`.

* `result.matches` — normalized rows to consume in a pipeline (shape varies by provider).
* `result.sources` — adapter metadata (counts, paging).
* Provider field notes: [provider guides](https://osintverse.com/docs/providers).
* Status meanings: [search workflow](https://osintverse.com/docs/api/search).

## Bulk

```python
batch = client.search.bulk(
    input_type="email",
    queries=["alice@example.com", "bob@example.com"],
    providers=["leakradar", "dehashed"],
    wait=True,
)
print(batch.batch_id, batch.estimated_cost_usd, batch.summary)
for job in batch.searches:
    print(job.provider, job.query, job.status)
```

Limits: **50** queries, **100** jobs (`queries × providers`). Partial success is normal. Bulk `wait=True` polls every `running` job and **does not** raise if a sibling is `failed` or `refunded` — read `batch.summary`. The bulk call counts once toward the daily key quota; each poll that `wait=True` makes counts once too.

## LeakRadar unlocks and OSINT Industries premium

```python
client.search.unlock(search.id)    # leftover LeakRadar credentials; $0.50 / 1k
client.search.premium(search.id)   # premium modules after a basic OSINT Industries search; +$1.80
```

Or at create: `client.search.create(..., premium=True)` (OSINT Industries only; ignored on other providers). Insufficient extra balance raises `PaymentRequiredError` (HTTP **402**). Already-unlocked / already-premium jobs return the current search with no extra charge.

## Async

```python
from osintverse import AsyncOSINTverse

async with AsyncOSINTverse() as client:
    search = await client.search.create(
        provider="leakradar",
        input_type="email",
        query="user@example.com",
        wait=True,
    )
```

Same method names, all `await`ed.

## Errors

HTTP `{ "detail": "…" }` maps to:

| Status | Exception                                                       |
| ------ | --------------------------------------------------------------- |
| 401    | `AuthenticationError`                                           |
| 402    | `PaymentRequiredError`                                          |
| 403    | `ForbiddenError`                                                |
| 404    | `NotFoundError`                                                 |
| 409    | `ConflictError` (idempotency key reused for a different search) |
| 422    | `ValidationError`                                               |
| 5xx    | `APIError`                                                      |

Every `APIError` has `status_code`, `detail`, and, when the API sends them, `code` (for example `insufficient_balance`) and `hint` (what to do next).

A **200** job can still fail. `wait=True` on **create** raises:

* `SearchFailedError` — usually insufficient balance (`status: failed`, not charged)
* `SearchRefundedError` — upstream failed after billing (`status: refunded`)
* `WaitTimeoutError` — still `running` after `timeout`

```python
from osintverse import SearchFailedError, ValidationError

try:
    search = client.search.create(
        provider="leakradar",
        input_type="email",
        query="user@example.com",
        wait=True,
    )
except ValidationError as exc:
    print("Bad provider/input_type:", exc)
except SearchFailedError as exc:
    print(exc.search.error)
```

Top up at [Billing](https://osintverse.com/dashboard/billing) before you automate. Full HTTP table: [errors](https://osintverse.com/docs/api/errors).

- [Get an API key](https://osintverse.com/dashboard/api)
- [OpenAPI spec](https://osintverse.com/openapi.json) and [Swagger UI](https://apiv1.osintverse.com/docs)
- [API sandbox (free, mock data)](https://osintverse.com/developers/sandbox)
- [Pricing](https://osintverse.com/pricing.json)
