Metadata-Version: 2.5
Name: beliq
Version: 0.3.0
Summary: Official beliq SDK: generate, validate, parse, and convert EU-compliant e-invoices (XRechnung, ZUGFeRD, Factur-X, Peppol BIS) against authority-pinned, drift-checked rules.
Project-URL: Homepage, https://beliq.eu
Project-URL: Repository, https://github.com/beliq-eu/beliq-sdk-python
Project-URL: Issues, https://github.com/beliq-eu/beliq-sdk-python/issues
Author-email: beliq <hello@beliq.eu>
License-Expression: MIT
License-File: LICENSE
Keywords: beliq,compliance,e-invoice,einvoice,en16931,factur-x,peppol,xrechnung,zugferd
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Office/Business :: Financial :: Accounting
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# beliq

Official Python SDK for the [beliq](https://beliq.eu) e-invoicing compliance API. Generate, validate, parse, and convert EN 16931 invoices (XRechnung, ZUGFeRD, Factur-X, Peppol BIS) against authority-pinned, nightly-drift-checked rules.

beliq produces and checks the compliant document. Transmission (Peppol, PDP, KSeF, SDI), archiving, and tax-authority reporting stay with your access point.

## Install

```bash
pip install beliq
```

Requires Python >= 3.10.

## Quick start

```python
from beliq import Beliq

beliq = Beliq(api_key="blq_...")

# Account, plan, and quota context (no quota cost).
account = beliq.me()

# Generate an XRechnung document from an EN 16931 invoice.
generated = beliq.generate(
    standard="xrechnung",
    verify=True,
    invoice={
        "number": "INV-2026-001",
        "issueDate": "2026-01-15",
        "currencyCode": "EUR",
        # The XRechnung CIUS asks for more than plain EN 16931: a seller contact
        # (BR-DE-2), payment instructions (BR-DE-1), a VAT breakdown (BR-CO-18)
        # and an electronic address per party. examples/invoice.json is this
        # same shape.
        "buyerReference": "04011000-12345-06",
        "seller": {
            "name": "Seller GmbH",
            "vatId": "DE123456789",
            "contactName": "Anna Muster",
            "email": "billing@seller.example",
            "phone": "+49 30 1234567",
            "address": {"street": "Hauptstr. 1", "city": "Berlin", "postalCode": "10115", "countryCode": "DE"},
            "peppol": {"schemeId": "9930", "id": "DE123456789"},
        },
        "buyer": {
            "name": "Buyer GmbH",
            "vatId": "DE987654321",
            "email": "ap@buyer.example",
            "address": {"street": "Marktweg 2", "city": "Munich", "postalCode": "80331", "countryCode": "DE"},
            "peppol": {"schemeId": "9930", "id": "DE987654321"},
        },
        "lines": [
            {"description": "Consulting", "quantity": 10, "unitCode": "HUR", "unitPrice": 100, "lineTotal": 1000, "vatRate": 19, "vatCategoryCode": "S"}
        ],
        "taxSummary": [{"vatCategoryCode": "S", "vatRate": 19, "taxableAmount": 1000, "taxAmount": 190}],
        "paymentMeans": {"typeCode": "58", "iban": "DE89370400440532013000"},
        "totalNetAmount": 1000,
        "totalTaxAmount": 190,
        "totalGrossAmount": 1190,
    },
)
print(generated.xml, generated.meta.schematron_version)

# Validate any document against authority-pinned rules.
result = beliq.validate(generated.xml, format="auto")
if not result.valid:
    for issue in result.errors:
        print(issue.rule_id, issue.message)
```

## Authentication

Create an API key in the beliq dashboard under API Keys:

```python
Beliq(api_key="blq_...")                    # sends X-API-Key (default)
Beliq(api_key="blq_...", auth="bearer")      # sends Authorization: Bearer
Beliq(api_key="blq_...", base_url="https://staging.beliq.eu")
```

## Timeouts and retries

The client retries transient failures for you, so you do not have to reimplement
backoff around it.

```python
Beliq(
    api_key="blq_...",
    timeout=90.0,     # per-attempt deadline in seconds (default)
    max_retries=3,    # extra attempts after the first (default)
)
```

Only `429`, `502` and `503` are retried, honouring the server's `Retry-After`
with jitter. beliq refunds the document's quota unit on a `503`, so a retry never
costs you a second document.

`504` and a client-side timeout are deliberately **not** retried: both mean the
work may still be running on beliq's side, so retrying risks producing a second
document rather than recovering the first.

A `429` carrying `QUOTA_EXCEEDED` is not retried either. `RATE_LIMITED` and
`ACCOUNT_THROTTLED` clear on their own, but a spent monthly allowance only returns
when your billing window turns, so the error is raised straight away and names the
cause instead of sleeping against it.

The default deadline is generous because beliq runs the full Schematron rule set
over each document, and a generate or validate can legitimately take tens of
seconds. If you lower it, keep it above the latency you actually see: a deadline
shorter than the server's own turns completed work into an unknown outcome. Pass
`max_retries=0` to handle retrying yourself. If you supply your own
`httpx.Client`, its timeout is used as-is and `timeout` is ignored.

## Async

`AsyncBeliq` mirrors the sync client with `await`:

```python
import asyncio
from beliq import AsyncBeliq

async def main():
    async with AsyncBeliq(api_key="blq_...") as beliq:
        result = await beliq.validate(open("invoice.xml", "rb").read(), format="auto")
        print(result.valid)

asyncio.run(main())
```

## API

| Method | Endpoint | Input | Returns |
|---|---|---|---|
| `me()` | GET /v1/me | none | `AccountInfo` (no quota cost) |
| `generate(...)` | POST /v1/generate | EN 16931 invoice dict | `GenerateResult` |
| `validate(document, ...)` | POST /v1/validate | XML or PDF | `ValidationResult` |
| `parse(document, ...)` | POST /v1/parse | XML or PDF | `ParseResult` |
| `convert(document, ...)` | POST /v1/convert | XML or PDF | `ConvertResult` |

`document` accepts a `str`, `bytes`, or `bytearray`. The content type is sniffed from the bytes (PDF vs XML) unless you pass `content_type=`. `generate` and `convert` return the raw document `content` (bytes) plus the response-header metadata: `meta.schematron_version`, `meta.pdf_kind`, `meta.source_format`/`meta.target_format`, `meta.lost_elements`, `meta.conversion_tools`, the ruleset fingerprint `meta.ruleset_sha256` / `meta.ruleset_artifacts`, and `meta.livemode`. For an XML output, `generate` also decodes `xml`.

JSON responses are Pydantic models. Any field not explicitly typed (such as the per-country authority versions on a validation result) is preserved and accessible. Errors raise `BeliqApiError` with a typed `.code`, HTTP `.status`, and any `.details`:

```python
from beliq import BeliqApiError

try:
    beliq.validate("not xml")
except BeliqApiError as err:
    print(err.code, err.status, err.message)
```

## The seal: verify it yourself

Pass `seal=True` to `generate` to get the document back as a JSON envelope: the decoded `content` (bytes) plus its `sha256` and the full `validation_result`. Hashing the returned bytes reproduces the returned hash, so you can prove which ruleset the document passed.

```python
import hashlib

sealed = beliq.generate(standard="xrechnung", verify=True, invoice=invoice, seal=True)
assert hashlib.sha256(sealed.content).hexdigest() == sealed.sha256
print(sealed.validation_result.valid, sealed.meta.ruleset_sha256)
```

Without `seal`, `generate` returns the raw document body (unchanged, the default). The ruleset fingerprint (`meta.ruleset_sha256`, `meta.ruleset_artifacts`) is present in both modes.

## Sandbox and live keys

A `blq_test_` key is a sandbox key; a `blq_live_` key is live. The client derives the mode from the key prefix before any request:

```python
beliq = Beliq(api_key="blq_test_...")
beliq.livemode  # False for a sandbox key, True for a live key
```

Each `generate` / `convert` response also carries the authoritative mode from the server as `meta.livemode`.

## Generate presets

`LIVE_GENERATE_PRESETS` is the curated set of public generate targets (matching beliq.eu's own generator). NLCIUS is a Peppol BIS profile rather than a standalone standard, so it is reachable here:

```python
from beliq import LIVE_GENERATE_PRESETS

nlcius = next(p for p in LIVE_GENERATE_PRESETS if p.id == "nlcius")
beliq.generate(standard=nlcius.standard, profile=nlcius.profile, output=nlcius.output, invoice=invoice)
```

## Which profiles a standard accepts

`profile` is pinned per standard, and the API answers a pair outside its table with `422 PROFILE_STANDARD_MISMATCH`. `LIVE_PROFILES` is one flat list for the Factur-X family, so offering it for every standard offers values that cannot succeed: none of them is legal on XRechnung or Peppol BIS, and `extended-ctc-fr` is Factur-X only. Build a per-standard choice from `profiles_for_standard` instead:

```python
from beliq import is_profile_allowed_for_standard, profiles_for_standard

profiles_for_standard("zugferd")                               # ('basicwl', 'en16931', 'extended')
is_profile_allowed_for_standard("zugferd", "extended-ctc-fr")  # False
```

An unknown standard returns `()` and is allowed, so the API stays the authority on values this table does not carry.

## Development

```bash
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
ruff check src tests
bash scripts/scrub-check.sh              # no em-dash in any tracked file
mypy
pytest                                   # unit tests (no network)
BELIQ_API_KEY=blq_xxx pytest tests/test_integration.py   # hits the live API; draws quota
```

`tests/test_spec_contract.py` reads the vendored `openapi.json` and fails if the error-code set, the core validate/seal fields, or the public option lists drift from the spec. Refresh the vendored spec with `python scripts/sync_spec.py`. A weekly workflow (`scripts/check_live_drift.py`) flags when the vendored spec falls behind the deployed API. `python scripts/check_profile_drift.py` checks `LIVE_PROFILES_BY_STANDARD` against the engine's own table; it needs a `beliq-engine` checkout beside this repo (or `BELIQ_ENGINE_PATH`) and fails without one, so it is run by hand, not in CI.

## Publishing

Released to PyPI as [`beliq`](https://pypi.org/project/beliq/). Releases run from `.github/workflows/release.yml` via PyPI Trusted Publishing (OIDC, with attestations): push a `v*.*.*` tag to publish. No token is stored in the repo.

## License

MIT
