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. timezonecontrols how naivedateanddatetimevalues 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 local00:00:00.endtmp=date(...)uses local23:59:59.- Naive
datetimeis interpreted in the client timezone. - Aware
datetimepreserves 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:
400and422:HoldedValidationError401and403:HoldedAuthError404:HoldedNotFoundError409:HoldedConflictError5xx:HoldedServerErrorwithretryable=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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
35537cb8fcb67188c87f7f9dd14c57076cfbaf3af24e1437c2524748cdbff043
|
|
| MD5 |
fd90a79ee5ae8c409224618f9ae4e5ad
|
|
| BLAKE2b-256 |
aabb950b72ad3937e92c7985db3c4d457dadfb4fbc036ccf379423da830bbb46
|
File details
Details for the file misterpato_holded_client-0.2.2-py3-none-any.whl.
File metadata
- Download URL: misterpato_holded_client-0.2.2-py3-none-any.whl
- Upload date:
- Size: 20.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1bda79f355a676ab9f6b0b6e503d29776c0575d67f6842af1df5f06ac2972868
|
|
| MD5 |
0edae0e902eada0d1f0a6db0c4581f44
|
|
| BLAKE2b-256 |
2ac8abe2a2b17c8d90133b34ded8b31865dd85f413018c3a27d8aac24a3a7f04
|