Modular Python client and CLI for the complete Holded API, with rich, JSON and TOON output.
Project description
pyholded
Modular Python client and CLI for the complete Holded API v2
Overview
pyholded is a Python toolkit to talk to the Holded business-management API (v2). Every endpoint across all modules — sales/purchase documents, contacts, products, CRM, projects and team — is described in a single declarative registry from which both the typed client and the CLI are generated. Results print as rich tables, JSON, or TOON.
Key Features
| Feature | Description |
|---|---|
| Registry-driven | Every endpoint is data; client and CLI share one source of truth |
| Full v2 surface | Documents, contacts, products, CRM, projects, team — plus a raw escape hatch |
| Bearer auth | Token from --token, environment variable, or TOML config file |
| Cursor pagination | --all (CLI) / paginate=True (library) merges every page |
| Three outputs | Rich tables, JSON, and TOON (token-efficient for LLMs) |
| CLI + Library | Use as a command-line tool or a typed Python package (py.typed) |
| Strict quality gate | ruff, black, mypy (strict), bandit, vulture, pip-audit — zero suppressions |
Supported Outputs
Records rich tables, JSON, TOON
Pagination cursor-based ({items, cursor, has_more}); --all merges pages
Auth Authorization: Bearer <PAT> via env var or config file
Binary PDF download for any document type (invoices, credit-notes, ...)
Installation
From PyPI (Recommended)
pip install pyholded
From Source
git clone https://github.com/seifreed/pyholded.git
cd pyholded
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -e .
Optional Extras
pip install "pyholded[dev]" # ruff, black, mypy, bandit, vulture, pip-audit, pytest
Authentication
Holded API v2 uses a Personal Access Token (pat_…) sent as Authorization: Bearer.
Generate one in Holded: Settings → Developers → Credentials → Add API Token.
The token is resolved in order of precedence:
- An explicit value (
--token, orHoldedClient(token=...)). - The
HOLDED_TOKENenvironment variable (HOLDED_API_KEYalso accepted). - A TOML config file (
--config,HOLDED_CONFIG, or~/.config/pyholded/config.toml).
# ~/.config/pyholded/config.toml
[holded]
token = "pat_xxx_yyy"
# base_url = "https://api.holded.com/api/v2/" # optional override
Multiple accounts
Configure several Holded accounts and query one or all of them.
Environment variables — HOLDED_TOKEN is the default account; HOLDED_TOKEN_<NAME>
adds a named account:
export HOLDED_TOKEN="pat_default"
export HOLDED_TOKEN_ACME="pat_acme"
export HOLDED_TOKEN_PERSONAL="pat_personal"
Config file — per-account tables (env overrides the file for the same name):
# ~/.config/pyholded/config.toml
default_account = "acme" # optional; picks the account when none is given
[accounts.acme]
token = "pat_acme"
# base_url = "..." # optional, per account
[accounts.personal]
token = "pat_personal"
Select with --account <name> (CLI) / HoldedClient(account="acme") (library), or fan
out to every account with --all-accounts / MultiClient. When several accounts are
configured and none is selected, set default_account or pass one explicitly.
Quick Start
# List every resource and its operations
holded resources
# List records (pretty table by default)
holded contacts list --limit 5
# TOON output, ideal for LLM contexts
holded taxes list -o toon
Usage
Command Line Interface
# List a page, or follow the cursor and fetch all pages
holded invoices list --limit 50
holded expenses_accounts list --all -o json
# Get one record, in JSON
holded contacts get --id 0123456789abcdef01234567 -o json
# Download a document PDF (binary)
holded invoices get-pdf --id 89abcdef0123456789abcdef > invoice.pdf
# Create from inline JSON, a file, or key=value fields
holded contacts create --data '{"name": "ACME SL"}'
holded contacts create --data @contact.json
holded contacts create --field name=ACME --field code=B12345678
# Multiple accounts
holded accounts # list configured accounts
holded --account acme contacts list # one named account
holded --all-accounts contacts list -o json # fan out -> {account: result}
# Call any endpoint directly
holded raw GET taxes -o toon
Main Commands
| Command | Description |
|---|---|
holded resources |
List all resources and their operations |
holded accounts |
List configured accounts (names + base URLs; tokens never shown) |
holded <resource> list |
List records (cursor-paginated; --all fetches every page) |
holded <resource> get --id <id> |
Get a single record |
holded <resource> create --data <json> |
Create a record |
holded <resource> update --id <id> --data <json> |
Update a record |
holded <resource> delete --id <id> |
Delete a record |
holded invoices get-pdf --id <id> |
Download a document PDF (also send) |
holded raw <METHOD> <PATH> |
Call an arbitrary endpoint |
Options
| Option | Description |
|---|---|
-o, --output {rich,json,toon} |
Output format (global default or per-command override) |
-a, --account <name> |
Use a named account (env/config) |
--all-accounts |
Run the command on every configured account |
--all |
Follow the cursor and fetch every page (GET) |
--limit, --cursor |
Manual pagination controls |
--data <json|@file>, --field k=v |
Request body for create/update |
--token, --config, --base-url, --timeout |
Connection options |
Resources
| Group | Resources |
|---|---|
Documents (CRUD + get-pdf, send) |
invoices, credit_notes, sales_orders, estimates, proformas, waybills, sales_receipts, purchases, purchase_orders |
| Masters (CRUD) | contacts, contact_groups, products, services, warehouses, payments, sales_channels, expenses_accounts, taxes, payment_methods |
| CRM | funnels, leads (+ create-note, create-task), events, bookings, booking_locations |
| Projects / Team | projects, tasks, employees |
Python Library
Basic Usage
from pyholded import HoldedClient
with HoldedClient() as client: # token from env or config file
page = client.invoices.list(params={"limit": 50})
everyone = client.contacts.list(paginate=True) # all pages, merged items list
contact = client.contacts.get(id="0123456789abcdef01234567")
pdf = client.invoices.getPdf(id="89abcdef0123456789abcdef") # raw bytes
new = client.contacts.create(data={"name": "ACME SL", "code": "B12345678"})
# Any endpoint, even one not modelled, is reachable directly:
raw = client.request("GET", "taxes", params={"limit": 5})
Resources are attributes; operations are methods. Path parameters (id) are keyword
arguments, query parameters go in params=, and the request body in data=.
Multiple accounts
from pyholded import HoldedClient, MultiClient
# one named account
with HoldedClient(account="acme") as client:
invoices = client.invoices.list()
# every configured account at once -> {account: result}
with MultiClient.from_accounts() as multi: # or from_accounts(["acme", "personal"])
per_account = multi.contacts.list(params={"limit": 5})
# {"acme": {"items": [...]}, "personal": {"items": [...]}}
A failure on one account is captured as {"error": "..."} for that account, so the
others still return their data.
Output Helpers
from pyholded import OutputFormat, render, to_json, to_toon
render(page, OutputFormat.TOON) # print in TOON
print(to_json(page)) # canonical JSON string
print(to_toon(page)) # TOON string
Supply Chain / SBOM
A CycloneDX 1.6 SBOM (sbom.cdx.json) is generated from real data —
package SHA-256 hashes come from a uv-compiled hashed lockfile
(requirements.lock), licenses and suppliers from installed
package metadata, a full dependency graph, and an ECDSA P-256 (ES256) signature
embedded as a CycloneDX JSF block (pure-Python, offline, no external tools). CI
regenerates, scores and verifies it on every push.
make sbom # generate + sign sbom.cdx.json
make sbom-score # generate + score with sbomqs (fails below 9.0)
make sbom-verify # verify the embedded signature
make lock # refresh the hashed lockfile (uv)
Current sbomqs score: 9.3 / 10 (Grade A) —
Identification, Provenance, Integrity, Licensing, Vulnerability and Structural all at A.
Completeness (D) is capped by sbomqs's CycloneDX dependency-graph detection, not by
missing data (the dependencies and compositions are present and valid). A perfect
10/A is not attainable for a PyPI project (it also requires per-component CPEs, which
Python packages do not have).
The signing private key is never committed; the public key travels inside the SBOM
(signature.publicKey JWK), so scripts/verify_sbom.py verifies it with no extra files.
Requirements
- Python 3.14+
- See pyproject.toml for dependencies and extras
Contributing
Contributions are welcome.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
All changes must keep the full quality gate green (ruff, black, mypy --strict,
bandit, vulture, pip-audit, pytest) with zero in-line suppressions.
Support the Project
If this project is useful in your workflows, you can support development:
License
This project is licensed under the MIT license. See LICENSE.
Attribution
- Author: Marc Rivero López | @seifreed
- Repository: github.com/seifreed/pyholded
Built for practical Holded automation and business-data workflows
Project details
Release history Release notifications | RSS feed
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 pyholded-0.1.0.tar.gz.
File metadata
- Download URL: pyholded-0.1.0.tar.gz
- Upload date:
- Size: 39.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aa798e32fc542e3c0a88f88b1fd8dced85c47d4d02ca277c0801ab84c9d3de29
|
|
| MD5 |
dc0b655c358610d84f46e3da29af262e
|
|
| BLAKE2b-256 |
8c970e004ed4af3a529d102cb476b838189a289188975179bcd3708d3ced246f
|
File details
Details for the file pyholded-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pyholded-0.1.0-py3-none-any.whl
- Upload date:
- Size: 25.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3f97726ba91933b9cd7fe877c7bbda2f3efe2a766634e5687d924aa25282f196
|
|
| MD5 |
96047f1dabeec0f305a184b2b88b9285
|
|
| BLAKE2b-256 |
676f2b8ef52ddb0b511e697ed72c396068c20dd63066366b732246122263f336
|