Skip to main content

Application-agnostic Holded API client for MisterPato projects.

Project description

misterpato-holded-client

Application-agnostic Python client package for the Holded legacy Invoice API v1.

The package exposes a small synchronous SDK around Holded's v1 Invoice API. It is intentionally independent of Django, Colegia, Celery, databases, or any app-specific idempotency layer.

Installation

Install from a checkout in editable mode:

python3 -m pip install -e .

Install with test dependencies for development:

python3 -m pip install -e .[test]
python3 -m pytest -q

Runtime dependency: requests.

Supported Python: >=3.9.

Development environment

A local virtual environment can be created with:

python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e .[dev]
python -m pytest -q

This checkout includes .python-version set to 3.10.12 for pyenv-compatible tooling. The package itself supports Python >=3.9.

Publishing

Build and upload instructions are documented in PUBLISHING.md. Short version:

source .venv/bin/activate
python -m pytest -q
rm -rf dist build *.egg-info src/*.egg-info
python -m build
python -m twine check dist/*
python -m twine upload dist/*

Do not commit real PyPI/TestPyPI tokens. .pypirc.example contains only placeholders.

Basic usage

from misterpato_holded import HoldedClient, VERSION_1

client = HoldedClient(api_key="holded-api-key", version=VERSION_1)

HoldedClient wires v1 resources directly on the client instance:

  • Generic resources: client.documents, client.contacts, client.treasuries, client.payment_methods
  • Typed document resources: client.invoices, client.credit_notes, client.sales_receipts, client.sales_orders, client.proformas, client.waybills, client.estimates, client.purchases, client.purchase_orders, client.purchase_refunds

MVP document resources support the full essential document action set for every document type, especially client.invoices and client.credit_notes: list, iter_all, list_all, create, get, update, approve, pay, delete, get_pdf_base64, and get_pdf.

VERSION_2 exists as a future-facing constant, but constructing a v2 client raises NotImplementedError until v2 is implemented.

Configuration

client = HoldedClient(
    api_key="holded-api-key",
    version=VERSION_1,
    base_url="https://api.holded.com/api",
    timeout=30,
    timezone="Europe/Madrid",
)

Notes:

  • The Holded v1 auth header is added automatically as key: <api_key>.
  • Blank API keys raise HoldedConfigError.
  • timezone controls how naive date and datetime values are serialized to Holded Unix timestamps.

Documents

Create an invoice with the typed resource:

from datetime import date
from misterpato_holded.v1.models import CreateDocumentRequest, DocumentItem

invoice = client.invoices.create(CreateDocumentRequest(
    date=date(2026, 1, 15),
    contact_name="Cliente Demo",
    currency="EUR",
    items=[DocumentItem(name="Service", units=1, subtotal="120.00")],
))

print(invoice.id, invoice.doc_number, invoice.raw)

The same operation can be done through the generic documents resource:

from misterpato_holded.v1.documents import DocumentType

invoice = client.documents.create(
    DocumentType.INVOICE,
    CreateDocumentRequest(date=date.today(), contact_name="Cliente Demo"),
)

Other document operations:

document = client.invoices.get("document-id")
updated = client.invoices.update("document-id", {"notes": "Updated notes"})
paid = client.invoices.pay("document-id", {"date": date.today(), "amount": "120.00"})
approved = client.invoices.approve("document-id")
pdf_base64 = client.invoices.get_pdf_base64("document-id")
pdf_bytes = client.invoices.get_pdf("document-id")
deleted = client.invoices.delete("document-id")

Raw mappings are accepted for pragmatic use. Known money and timestamp fields are serialized before sending, and locally known required fields are validated before the HTTP request.

Contacts

from misterpato_holded.v1.models import CreateContactRequest, UpdateContactRequest

contact = client.contacts.create(CreateContactRequest(
    name="Cliente Demo",
    email="cliente@example.com",
))

same_contact = client.contacts.get(contact.id)
updated_contact = client.contacts.update(contact.id, UpdateContactRequest(phone="+34 600 000 000"))
deleted = client.contacts.delete(contact.id)

Raw mapping payloads are also supported:

contact = client.contacts.create({"name": "Cliente Demo", "email": "cliente@example.com"})

Treasuries and payment methods

These resources are list-only in this v1 package:

treasuries = client.treasuries.list()
payment_methods = client.payment_methods.list()

Pagination

List resources support one-page list(page=N), lazy iter_all(...), and materialized list_all(...).

page_2 = client.invoices.list(page=2)

for invoice in client.invoices.iter_all(starttmp=date(2026, 1, 1), endtmp=date(2026, 1, 31)):
    print(invoice.id, invoice.doc_number)

all_contacts = client.contacts.list_all(max_pages=20)

iter_all() starts at page 1 by default and stops when Holded returns an empty list. If max_pages is reached before an empty page is observed, HoldedPaginationLimitError is raised instead of silently returning partial data.

Date and datetime serialization

Holded v1 expects integer Unix timestamps for document/payment dates and starttmp/endtmp filters. This package accepts int, date, and datetime values.

Rules:

  • Default timezone for naive date/datetime: Europe/Madrid.
  • Configure it with HoldedClient(..., timezone="UTC") or another IANA timezone name.
  • starttmp=date(...) uses local 00:00:00.
  • endtmp=date(...) uses local 23:59:59.
  • Naive datetime is interpreted in the client timezone.
  • Aware datetime preserves its instant.
from datetime import date, datetime, timezone

client = HoldedClient(api_key="holded-api-key", timezone="Europe/Madrid")

client.invoices.list(starttmp=date(2026, 1, 1), endtmp=date(2026, 1, 31))
client.invoices.create(CreateDocumentRequest(date=datetime(2026, 1, 15, 10, 30), contact_name="Demo"))
client.invoices.pay("document-id", {"date": datetime.now(timezone.utc), "amount": "120.00"})

Money serialization

Money values use Decimal(str(amount)), are quantized to 0.01 with ROUND_HALF_UP, then sent as JSON numbers.

DocumentItem(name="Service", units=1, subtotal="10.235")  # sends 10.24

Error handling

All package exceptions inherit from HoldedError.

from misterpato_holded.exceptions import (
    HoldedAuthError,
    HoldedConfigError,
    HoldedError,
    HoldedNotFoundError,
    HoldedPaginationLimitError,
    HoldedRequestValidationError,
    HoldedServerError,
    HoldedValidationError,
)

try:
    invoice = client.invoices.create(CreateDocumentRequest(contact_name="Missing date"))
except HoldedRequestValidationError:
    # Local validation failed before any HTTP request was sent.
    raise
except HoldedValidationError as exc:
    # Holded returned a remote 400/422 validation error.
    print(exc.status_code, exc.response_json)
except HoldedAuthError:
    print("Invalid or unauthorized Holded API key")
except HoldedNotFoundError:
    print("Document/contact not found")
except HoldedServerError as exc:
    if exc.retryable:
        print("Holded server error; safe for caller-controlled retry policy")
except HoldedPaginationLimitError:
    print("Pagination reached max_pages before an empty page")
except HoldedConfigError:
    print("Invalid local client configuration")
except HoldedError as exc:
    print(f"Holded client failure: {exc}")

Status mapping:

  • 400 and 422: HoldedValidationError
  • 401 and 403: HoldedAuthError
  • 404: HoldedNotFoundError
  • 409: HoldedConflictError
  • 5xx: HoldedServerError with retryable=True
  • Invalid successful JSON or unexpected response shape: HoldedResponseError

Development verification

python3 -m pytest -q
python3 -m compileall -q src tests
python3 - <<'PY'
from misterpato_holded import HoldedClient, VERSION_1, VERSION_2
client = HoldedClient(api_key="x", version=VERSION_1)
print(client.version, client.invoices.doc_type.value)
try:
    HoldedClient(api_key="x", version=VERSION_2)
except NotImplementedError as exc:
    print(type(exc).__name__, exc)
PY

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

misterpato_holded_client-0.1.0.tar.gz (19.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

misterpato_holded_client-0.1.0-py3-none-any.whl (19.6 kB view details)

Uploaded Python 3

File details

Details for the file misterpato_holded_client-0.1.0.tar.gz.

File metadata

  • Download URL: misterpato_holded_client-0.1.0.tar.gz
  • Upload date:
  • Size: 19.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for misterpato_holded_client-0.1.0.tar.gz
Algorithm Hash digest
SHA256 a6c8a734559804451e2378745d1983d5f8503a72ef644139c323476268209310
MD5 c1fc0d9aeec911503101c9433a29b957
BLAKE2b-256 ebf6f198be9e32650a7bf52f734fab49a5c67bcf2baedfc02f4ab7a417b90c31

See more details on using hashes here.

File details

Details for the file misterpato_holded_client-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for misterpato_holded_client-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 15b74c90dc52e776a9ec542f06c9781c1d13ac98ac8bde47b32f43d2f5564c86
MD5 ed90603c374aef10a92a2173aa188d06
BLAKE2b-256 1330e2e575df1981665218da5eab8bc7ef533a15479843a5f40ca26f01f1b477

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page