Skip to content

Repository files navigation

vendus

PyPI version Python versions CI License: MIT Typed Docs

vendus is an unofficial, fully-typed Python SDK for the Vendus invoicing & POS API — Portugal's AT-certified faturação platform (also Spain). Issue invoices (FT), simplified invoices (FS), invoice-receipts (FR), receipts (RG) and credit notes (NC) against the Vendus API in a few lines of Python, with matching sync and async clients.

Vendus handles all communication with the AT (Autoridade Tributária) — SAF-T, ATCUD, document hash and QR code come back ready on every document. This SDK talks to Vendus; Vendus talks to the AT.

📖 Documentation: https://vendus.bilouro.com/ · English
📖 Documentation: https://vendus.bilouro.com/pt/ · Português

Examples · API Reference

Community SDK — not affiliated with or endorsed by Vendus. For official integrations, visit vendus.pt.

Installation

pip install vendus      # or: uv add vendus

Quick Start

from decimal import Decimal
from vendus import ClientData, DocumentItem, TaxCategory, VendusClient

client = VendusClient(api_key="your-api-key")

# Issue an invoice (FT)
invoice = client.documents.create_invoice(
    register_id=1,
    client=ClientData(name="Acme Lda", fiscal_id="123456789"),
    items=[
        DocumentItem(
            description="Consulting hours",
            quantity=Decimal("10"),
            unit_price=Decimal("75.00"),  # gross (includes tax)
            tax_category=TaxCategory.NORMAL,
        ),
    ],
    external_reference="ORD-2026-001",   # enables safe POST retries
)

print(invoice.number)   # "FT 2026/123"
print(invoice.atcud)    # AT communication code
print(invoice.qrcode)   # AT QR code payload

Async Support

Every method has an async variant — same client, _async suffix:

invoice = await client.documents.create_invoice_async(
    register_id=1,
    client=ClientData(name="Acme Lda", fiscal_id="123456789"),
    items=[...],
)

Three client shapes (one API)

The same create_invoice / create_invoice_receipt handles all three cases:

# 1. Client with NIF (typical B2B)
client.documents.create_invoice(
    register_id=1, items=[...],
    client=ClientData(name="Acme Lda", fiscal_id="123456789"),
)

# 2. Client without NIF (B2C, customer gave name only)
client.documents.create_invoice(
    register_id=1, items=[...],
    client=ClientData(name="João Silva"),
)

# 3. Final consumer (anonymous, no identification at all)
client.documents.create_invoice(register_id=1, items=[...])

Do NOT pass fiscal_id="999999990" — the SDK rejects it. For final consumer, omit client.

Credit Notes

A credit note (NC) credits a previously issued invoice. It is also the only way to reverse a fiscal invoice — FT/FR cannot be cancelled. The SDK fetches the original and credits its full set of lines, so you pass only the id and a reason:

credit_note = client.documents.create_credit_note(
    reference_document_id=invoice.id,
    reason="Customer return",
    external_reference="REFUND-2026-001",
)

Error Handling

All errors inherit from VendusError with typed subclasses:

from vendus import (
    VendusClient,
    ValidationError,
    AuthenticationError,
    AuthorizationError,
    NotFoundError,
    RateLimitError,
    APIError,
    TransportError,
)

try:
    invoice = client.documents.create_invoice(...)
except ValidationError as e:
    # Local validation — invalid NIF, missing items, forbidden 999999990
    print(e)
except AuthenticationError:
    # API key rejected (401)
    ...
except RateLimitError:
    # 429 — back off
    ...
except APIError as e:
    # Other Vendus errors — inspect e.status_code and e.response_body
    ...
except TransportError:
    # Network failure — timeout, DNS, connection refused
    ...

Configuration

client = VendusClient(
    api_key="your-api-key",
    base_url="https://www.vendus.pt/ws",  # production (default)
    timeout=30.0,                          # seconds
    max_retries=3,                         # GET retries; POST only if external_reference present
)

# Or load from VENDUS_API_KEY
client = VendusClient.from_env()

Supported Documents

Document Code Method Status
Fatura FT client.documents.create_invoice ✅
Fatura Simplificada FS client.documents.create_simplified_invoice ✅
Fatura-Recibo FR client.documents.create_invoice_receipt ✅
Recibo RG client.documents.create_receipt ✅
Nota de Crédito NC client.documents.create_credit_note ✅
Orçamento OT — roadmap
Guia de Transporte GT — roadmap
Nota de Débito ND — roadmap

Validation status

The wire format of every operation is asserted by unit tests (respx mocks), and validated against the real Vendus API — in test mode (mode=tests, non-fiscal) where possible, and once in real mode for the operations that test mode can't reach:

Operation Unit Live
create_invoice (FT) ✅ ✅ test + real
create_simplified_invoice (FS) ✅ ✅ test + real (credited by NC)
create_invoice_receipt (FR) ✅ ✅ test + real (+ payment variations)
create_receipt (RG) ✅ ✅ test + real (references an invoice)
create_credit_note (NC) ✅ ✅ real (full + partial; credits FT/FR/FS)
cancel ✅ ✅ refuses FT/FR/NC; cancels a receipt (RG)
list_payment_methods / list_registers / list / get ✅ ✅ read-only

Test-mode documents ("Modo de Formação") are non-fiscal and never reported to the AT, but Vendus stores them in a separate space — they can't be retrieved or credited via /documents/{id}, so credit notes are validated in real mode. Fiscal invoices (FT/FR/NC) can't be cancelled (reverse them with a credit note); a receipt (RG) can — both paths are live-verified.

Why This SDK

  • Fully typed — mypy --strict passes, py.typed marker included. Full autocomplete in VS Code and PyCharm.
  • Sync + Async — one client, no separate packages. httpx powers both.
  • Decimal amounts — no floating-point surprises with money. Decimal("49.90"), not 49.8999.... Cent-precision matters for AT.
  • Safe retries — GET retries with exponential backoff + jitter. POST retries only when external_reference is set (Vendus's deduplication anchor). Without it, POST fails immediately to avoid duplicate fiscal documents.
  • PII redaction — fiscal_id, email, phone, address are automatically redacted from logs.
  • NIF validation — Portuguese NIF check digit verified locally before any API call.
  • Exception hierarchy — catch ValidationError for local issues, AuthenticationError for bad keys, RateLimitError for 429s, or VendusError for everything.
  • AT communication is opaque — Vendus is the certified party. Hash, ATCUD, and QR code come ready from Vendus; the SDK never talks to AT directly.

Development

git clone https://github.com/bilouro/vendus-python.git
cd vendus-python
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pre-commit install

Run checks (all must pass before a PR):

ruff check .          # lint
ruff format --check . # formatting
mypy src/             # type check (strict)
pytest                # unit tests + coverage (≥85% enforced)

Live integration tests hit the real Vendus API; they're excluded from pytest and auto-skip without credentials. Run them in test mode against a demo account:

export VENDUS_API_KEY=... VENDUS_REGISTER_ID=...
pytest -m integration --no-cov

Full developer guide — testing, the live-validation discipline, and how to add a document type — is on the Contributing page.

Contributing

See CONTRIBUTING.md. PRs welcome — especially for new document types.

Security

The SDK's security posture — API key handling, the test/real mode boundary, PII redaction, and fiscal-record integrity — is on the Security guide.

Report vulnerabilities privately — see SECURITY.md. Do not open public issues for security bugs.

License

MIT — use it however you want.

About

Unofficial Python SDK for the Vendus invoicing & POS API (Portugal/Spain) — issue invoices (FT), simplified invoices (FS), invoice-receipts (FR), receipts (RG) and credit notes (NC). Typed, sync + async; AT-certified faturação.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages