Metadata-Version: 2.5
Name: verisec
Version: 1.0.0
Summary: The official Python library for the VeriSec API: identity (KYC) and Financial checks.
Project-URL: Homepage, https://verisec.mmose.me
Project-URL: Repository, https://github.com/verisec-labs/verisec-python
Project-URL: Issues, https://github.com/verisec-labs/verisec-python/issues
Author: VeriSec Labs
License-Expression: MIT
License-File: LICENSE
Keywords: aml,card,cheque,document,face,identity verification,kyc,micr,verisec
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
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: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# VeriSec Python SDK

The official Python library for the VeriSec API: identity (KYC) and Financial checks, with typed answers, images
straight from files, safe retries and webhook verification.

```sh
pip install verisec
```

Python 3.9 or later. No runtime dependencies: only the standard library.

## Configure

```python
import os
from verisec import VeriSec

client = VeriSec(api_key=os.environ["VERISEC_API_KEY"])  # sk_test_… or ak_live_…
```

Create keys in the console under **API Keys**. A test key (`sk_test_…`) is used as it is. A live key (`ak_live_…`)
also needs the client id and secret issued with it; the library exchanges them at `POST /v2/oauth/token` for a
short-lived token, caches it, and renews it for you:

```python
client = VeriSec(
    api_key=os.environ["VERISEC_API_KEY"],  # ak_live_…
    client_id=os.environ["VERISEC_CLIENT_ID"],
    client_secret=os.environ["VERISEC_CLIENT_SECRET"],
)
```

With no arguments, `VeriSec()` reads `VERISEC_API_KEY`, `VERISEC_CLIENT_ID`, `VERISEC_CLIENT_SECRET` and
`VERISEC_BASE_URL`.

| Option | Default | |
|---|---|---|
| `base_url` | `https://verisec.mmose.me` | |
| `timeout` | `300` | Seconds to wait for an answer, per attempt. A deep cheque read can take about 250 s. |
| `max_retries` | `2` | Retries where nothing was charged (see Retries). |
| `default_headers` | none | Headers sent with every request. |

A client is safe to share between threads; they share one token exchange.

## KYC: images from files

Every API is a method, grouped as the API groups them: `client.face`, `client.documents`, `client.mrz`,
`client.phone.otp`, `client.screening`, … (all 69, with prices, are in `../spec/README.md`). Methods take the API's
fields as keyword arguments, spelled exactly as the API spells them (`image_front`, `detectAll`, `minAge`).

An image can be a file path (`str` or `pathlib.Path`), the file's bytes, an open binary file, a `data:` URI, or
base64 you already have. The library sends it the way the API expects.

```python
result = client.face.verify(image1="selfie.jpg", image2=id_photo_bytes, threshold=0.8)
print(result.match, result.similarity)

document = client.documents.read(image_front="id-front.jpg", image_back=Path("id-back.jpg"))

# lists take the images themselves; the index and ids are filled in for you
match = client.face.match(images=["selfie.jpg", "passport.jpg"])
found = client.face.search(probe="selfie.jpg", gallery=["alice.jpg", {"image": "bob.jpg", "id": "bob"}])

screening = client.screening.sanctions(name="Jane Example", countries=["RW"])
```

A KYC call returns the answer's `data`. The rest of the response is on `last_response`:

```python
print(result.last_response.request_id)  # quote it to support
print(result.last_response.warnings, result.last_response.latency_ms, result.last_response.status)
```

Answers are dicts whose keys also read as attributes: `result.similarity` and `result["similarity"]` are the same.
A key that is a Python keyword or a `dict` method (`pass`, `items`) is read with brackets. Each answer has a type in
`verisec.types` (`FaceVerify`, `DocumentMrz`, …) for your editor and type checker.

The APIs that return the engine's full result (`face.match`, `face.analyze`, `documents.read`,
`screening.sanctions`, `contact.dossier` and the other "open" ones) give it as an open dict, with every field the
engine returns:

```python
for pair in match["results"]:
    print(pair["similarity"], pair.get("match"))
```

## Financial: cheques, cards, BIN, banks

Cheque and card photos are uploaded as files: a path, bytes, an open binary file, or `(filename, bytes)`.

```python
cheque = client.cheques.read(image="cheque.jpg")
print(cheque.status, cheque.micr.zones_detected)
if cheque.vision:
    print(cheque.vision.payee, cheque.vision.amount)

line = client.cheques.micr_line(image=cheque_bytes, format="e13b")
card = client.cards.check(number="4242 4242 4242 4242", expiry="09/29")
bin_record = client.bin.lookup("424242")
bank = client.banks.lookup(name="Harbor Point Bank", country="US")
```

A Financial call returns the answer object as it is (`cheque.object == "cheque_read"`), with the same
`last_response`.

## Credits

Each call costs its API's price in credits, charged only when it succeeds. Refused calls (no credits, no permission,
rate limited) and calls that could not run are free.

## Errors

```python
from verisec import APIConnectionError, APIError, InsufficientCreditsError, InvalidRequestError, RateLimitError

try:
    client.face.verify(image1="selfie.jpg", image2="id-photo.jpg")
except InsufficientCreditsError:
    print("Not enough credits: top up in the console.")
except InvalidRequestError as e:
    print(e.code, e.param, e.message)  # e.g. image_unreadable image2 …
except RateLimitError as e:
    print(f"Wait {e.retry_after} s.")
except APIError as e:
    print(e.status, e.code, e.request_id)
except APIConnectionError as e:
    print(e.message)  # says whether the call may have run
```

| Class | When |
|---|---|
| `InvalidRequestError` | 400, 413, 415, 422: a field is missing or not valid, or the file is too large or not an image |
| `AuthenticationError` | 401: no key, or the key (or the client secret) is not valid |
| `InsufficientCreditsError` | 402: not enough credits, or the key reached its spend cap. Nothing was charged. |
| `PermissionError` | 403: the key may not call this API (`verisec.PermissionError`, not the built-in) |
| `NotFoundError` | 404 |
| `RateLimitError` | 429, with `retry_after` in seconds |
| `APIServerError` | 5xx: the check could not run. Nothing was charged. |
| `APIConnectionError`, `APITimeoutError` | no answer |

All of them inherit from `VeriSecError` and carry `status`, `code`, `message`, `param`, `request_id`, `retry_after`
and `headers`. An image that cannot be read (a path that does not exist) raises `VeriSecError` naming the field,
before anything is sent.

## Retries and timeouts

The library retries rate limits (429), server errors (5xx) and connections that never opened, up to `max_retries`
times with backoff (or the `Retry-After` the API sends): none of those were charged. It never sends a call again
after a timeout, or after the connection dropped once the request was sent, because the check may already have run
and been charged; you get an `APITimeoutError` or `APIConnectionError` saying so.

Every method also takes `timeout`, `max_retries` and `extra_headers` for that call:

```python
client.cheques.read(image="cheque.jpg", timeout=280, max_retries=0, extra_headers={"X-Trace": "t-42"})
```

## Webhooks

Deliveries are signed as Standard Webhooks (`webhook-id`, `webhook-timestamp`, `webhook-signature`). Pass the raw
body, the headers and your endpoint's signing secret (`whsec_…`):

```python
from flask import Flask, request
from verisec import SignatureVerificationError, Webhook

app = Flask(__name__)

@app.post("/webhooks/verisec")
def verisec_webhook():
    try:
        event = Webhook.construct_event(request.get_data(), request.headers, os.environ["VERISEC_WEBHOOK_SECRET"])
    except SignatureVerificationError:
        return "", 400
    if event.type == "verification.completed":
        print(event.data)
    return "", 200
```

`Webhook.verify_signature(...)` returns `True` or `False` instead. Signatures older than 300 seconds are refused
(pass `tolerance=` to change it). `Webhook.generate_test_headers(body, secret)` signs a body for your own tests. The
same helpers are on `client.webhooks`.

## A call the library does not wrap yet

```python
result = client.http.request("POST", "/v1/some/new-api", body={"field": "value"}, envelope="data")
```

## Development

```sh
python -m venv .venv
.venv/Scripts/python -m pip install -e ".[dev]"   # Windows; .venv/bin/python elsewhere
.venv/Scripts/python scripts/generate.py          # after the spec changes
.venv/Scripts/python -m pytest                    # unit + integration tests
```

`scripts/generate.py` writes `src/verisec/_resources.py` and `src/verisec/types/` from `../spec/openapi.json`
(standard library only; `--check` tells whether they are up to date). Everything else in `src/verisec/` is written by
hand: the transport, authentication, errors, images and uploads, webhooks and the answer objects.

The integration tests start the shared test server (`node ../test-server/server.mjs --port 4021`), call every
operation through the library, and stop it when they end.
