Metadata-Version: 2.4
Name: distyra
Version: 0.8.0
Summary: Python client for the Distyra Transaction Enrichment API (enrichment + SME underwriting)
Project-URL: Homepage, https://distyra.com
Project-URL: Documentation, https://api.distyra.com/docs
Author: Brightfield Software
License: MIT
Keywords: distyra,open banking,psd2,transaction enrichment,underwriting
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Office/Business :: Financial
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: attrs>=22.2.0
Requires-Dist: httpx<0.29.0,>=0.23.0
Requires-Dist: python-dateutil<3,>=2.8.0
Description-Content-Type: text/markdown

# distyra

Python client for the [Distyra Transaction Enrichment API](https://api.distyra.com/docs): transaction enrichment and SME cash-flow underwriting for EU lenders.

```bash
pip install distyra
```

**Base URL:** `https://api.distyra.com` (the SDK default). The API and CDN stay on `.com`; only the Distyra website and customer portal use `.eu`.

## Quick start

```python
import os
from distyra import Distyra

client = Distyra(api_key=os.environ["DISTYRA_API_KEY"])

result = client.enrich(
    descriptor="ALBERT HEIJN 1234 AMSTERDAM NL",
    amount=-12.34,
    currency="EUR",
    mcc="5411",
)
# merchant is None for non-merchant transactions (bank fees, transfers), so guard it.
print(result.merchant.name if result.merchant else "(no merchant)")  # "Albert Heijn"
print(result.category.primary)     # "Retail & E-commerce"
print(result.category.source)      # "catalog" (curated) or "llm" (model-proposed)
```

## Batch enrichment

```python
batch = client.enrich_batch([
    {"descriptor": "ALBERT HEIJN 1234 AMSTERDAM NL", "amount": -12.34, "currency": "EUR"},
    {"descriptor": "NS GROEP B.V.", "amount": -4.50, "currency": "EUR"},
    {"descriptor": "NETFLIX.COM", "amount": -13.99, "currency": "EUR"},
])
for item in batch.items:
    print(item)
```

Up to 100 items per call; a failed item occupies its slot with an error envelope, the batch itself never fails.

## Handling unknown merchants (eventual consistency)

The first time a brand-new merchant is enriched, the result may be unresolved
(`result.resolution.status` other than `resolved`). The system then discovers and confirms
that merchant in the background, so the same descriptor resolves shortly after, for every
caller, with no model call.

Read `result.resolution.retry_after`:

- A number (seconds) means a confirmed answer is likely coming. Retry the same descriptor
  once after that delay, or just let your next natural re-sync pick it up.
- `None` means the result is definitive; no retry will help.

```python
result = client.enrich(descriptor="TIMECHIMP UREN UTRECHT NL", country_hint="NL")
if result.resolution.status != "resolved" and result.resolution.retry_after is not None:
    # retry once after result.resolution.retry_after seconds, or wait for your next sync
    ...
```

`resolution.retryable` is a different signal: `True` only for a transient backend error
(`timed_out` / `transient_error`), which you retry within seconds. Full guide:
<https://api.distyra.com/docs>.

## Underwriting

Statement files in, one consolidated analysis out. Multiple files for the same applicant (the "last 6 months as 6 monthly PDFs" case) consolidate into one analysis over the merged window:

```python
analysis = client.analyze_statement(
    ["jan.pdf", "feb.pdf", "mar.pdf"],
    applicant_id="loan-2026-0142",
    input_mode="statement_pdf",     # one format per analysis
    enrich=True,                    # optional: enrich every transaction
)
print(analysis.id, analysis.features)
```

JSON mode, when transactions are already on hand:

```python
analysis = client.analyze(
    applicant_id="loan-2026-0142",
    input_mode="raw",
    accounts=[{
        "account_id": "main",
        "currency": "EUR",
        "opening_balance": 1500.0,
        "transactions": [
            {"date": "2026-05-01", "amount": 2500.0, "descriptor_raw": "INVOICE 1001 ACME BV"},
            {"date": "2026-05-15", "amount": -1200.0, "descriptor_raw": "RENT MAY OFFICE"},
        ],
    }],
)
```

Fetch a stored analysis later with `client.get_analysis(analysis_id)`.

Note: enrichment is self-serve; underwriting endpoints are enabled per organization. Contact sales for access.

## Errors

Every non-2xx raises `DistyraError` with the parsed envelope:

```python
from distyra import Distyra, DistyraError

try:
    client.enrich(descriptor="...")
except DistyraError as e:
    print(e.status_code, e.error, e.detail)
```

## Verifying webhooks

Distyra signs every webhook delivery with a Stripe-shape header:

```
X-Distyra-Signature: t=<unix>,v1=<hmac_sha256_hex>
```

`verify_webhook` checks it (HMAC-SHA256 over `"<t>.<raw_body>"`, keyed by the
endpoint's signing secret, with replay protection). Always verify against the
**raw request body**, before JSON parsing.

```python
from distyra import verify_webhook

# Flask
@app.post("/webhooks/distyra")
def distyra_webhook():
    raw = request.get_data()  # exact bytes, before request.get_json()
    if not verify_webhook(WEBHOOK_SECRET, raw, request.headers.get("X-Distyra-Signature")):
        abort(400)
    event = request.get_json()
    # ... handle event ...
    return "", 200
```

`verify_webhook(secret, raw_body, signature_header, *, tolerance_seconds=300)`
returns `True` only when the signature is valid and the timestamp is fresh. The
default replay window is 5 minutes. Stdlib only (`hmac`/`hashlib`).

## Beyond the wrapper

The full generated client (every endpoint, typed models) ships in the same distribution as `distyra_api_client`. The wrapper's `.raw` attribute is a ready-authenticated instance:

```python
from distyra_api_client.api.catalog import get_categories

categories = get_categories.sync(client=client.raw)
```

## Development

This package lives in the Distyra monorepo at `packages/saas-sdk-python/`. The `distyra_api_client` package is generated from the committed OpenAPI snapshot — do not edit it by hand:

```bash
npm run snapshot -w @brightfield/saas-api   # refresh openapi.json from source
bash scripts/generate-python-sdk.sh         # regenerate distyra_api_client
bash scripts/test-python-sdk.sh             # build wheel + smoke
```
