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"),
)

Pass approve=True to approve a document during creation, which sends Holded's approveDoc: true field. Pass custom_fields=[...] to send Holded's customFields array:

custom_fields = [{"field": "project", "value": "MisterPato"}]

proforma = client.proformas.create(
    CreateDocumentRequest(date=date.today(), contact_name="Cliente Demo"),
    approve=True,
    custom_fields=custom_fields,
)

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 string Unix timestamps for document/payment dates and starttmp/endtmp filters. This package accepts int, str, date, and datetime values and emits canonical decimal strings on the wire.

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.2.2.tar.gz (20.4 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.2.2-py3-none-any.whl (20.0 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: misterpato_holded_client-0.2.2.tar.gz
  • Upload date:
  • Size: 20.4 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.2.2.tar.gz
Algorithm Hash digest
SHA256 35537cb8fcb67188c87f7f9dd14c57076cfbaf3af24e1437c2524748cdbff043
MD5 fd90a79ee5ae8c409224618f9ae4e5ad
BLAKE2b-256 aabb950b72ad3937e92c7985db3c4d457dadfb4fbc036ccf379423da830bbb46

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for misterpato_holded_client-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 1bda79f355a676ab9f6b0b6e503d29776c0575d67f6842af1df5f06ac2972868
MD5 0edae0e902eada0d1f0a6db0c4581f44
BLAKE2b-256 2ac8abe2a2b17c8d90133b34ded8b31865dd85f413018c3a27d8aac24a3a7f04

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