Skip to main content

Modular Python client and CLI for the complete Holded API, with rich, JSON and TOON output.

Project description

pyholded

pyholded

Modular Python client and CLI for the complete Holded API v2

PyPI Version Python Versions License CI Status Typed SBOM

GitHub Stars GitHub Issues Buy Me a Coffee


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:

  1. An explicit value (--token, or HoldedClient(token=...)).
  2. The HOLDED_TOKEN environment variable (HOLDED_API_KEY also accepted).
  3. 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


Contributing

Contributions are welcome.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. 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:

Buy Me A Coffee

License

This project is licensed under the MIT license. See LICENSE.

Attribution


Built for practical Holded automation and business-data workflows

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

pyholded-0.1.0.tar.gz (39.8 kB view details)

Uploaded Source

Built Distribution

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

pyholded-0.1.0-py3-none-any.whl (25.4 kB view details)

Uploaded Python 3

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

Hashes for pyholded-0.1.0.tar.gz
Algorithm Hash digest
SHA256 aa798e32fc542e3c0a88f88b1fd8dced85c47d4d02ca277c0801ab84c9d3de29
MD5 dc0b655c358610d84f46e3da29af262e
BLAKE2b-256 8c970e004ed4af3a529d102cb476b838189a289188975179bcd3708d3ced246f

See more details on using hashes here.

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

Hashes for pyholded-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3f97726ba91933b9cd7fe877c7bbda2f3efe2a766634e5687d924aa25282f196
MD5 96047f1dabeec0f305a184b2b88b9285
BLAKE2b-256 676f2b8ef52ddb0b511e697ed72c396068c20dd63066366b732246122263f336

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