Metadata-Version: 2.4
Name: timbro-invoices
Version: 0.9.0
Summary: Python client for Timbro's canonical Invoice API
Author: Timbro contributors
Project-URL: Homepage, https://timbro.do
Project-URL: Source, https://github.com/indexa-labs/Timbro
Project-URL: Issues, https://github.com/indexa-labs/Timbro/issues
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: jsonschema[format]<5,>=4.22
Requires-Dist: regex>=2024.11.6

# Timbro Invoices for Python

`timbro-invoices` is the Python client for the canonical `/v1/invoices` API.
Invoice request and response wire shapes are defined by the Timbro API; the installed package carries a generated projection of that contract.

Requires Python 3.10 or later.

```sh
pip install timbro-invoices
```

## Compatibility

`timbro-invoices` 0.9 belongs to the same compatible set as `@timbro/payments`, `@timbro/invoices`, and `@timbro/protocol` 0.9. Packages that share a minor version work together.

The client validates requests strictly and accepts additional properties in responses. Timbro may add a property to a response, at any depth and in any union variant, without a breaking release; the client ignores what it does not know and returns it unchanged. Required fields, types, formats, and enum values stay enforced, so removing or renaming a field, or adding a value to an existing enum, is a breaking change. An unknown webhook event type is returned to you, after the envelope and scope checks, instead of raising.

```python
from timbro_invoices import TimbroInvoices

client = TimbroInvoices(api_key="your-invoice-api-key")
preview = client.preview_invoice(invoice_request)
invoice = client.create_invoice(invoice_request)
current = client.retrieve_invoice(invoice["id"])
page = client.list_invoices(invoice_reference="odoo:2026:1042")
xml = client.download_xml(invoice["id"])
pdf = client.download_pdf(invoice["id"])

# Replace a retained, replaceable Invoice with a new canonical request.
replacement = client.replace_invoice(
    rejected_invoice["id"],
    {**invoice_request, "invoiceReference": "odoo:2026:1042-r1"},
)

# Share an accepted receipt with a payment service.
receipt_access = client.share_receipt(invoice["id"])
receipt = client.retrieve_receipt(receipt_access)
receipt_pdf = client.download_receipt_pdf(receipt_access)
```

Keep the receipt access grant on your server and pass it to the receiving service over an authenticated channel. Restricted reads use its `accessToken`; the token is never sent in the URL and grants access only to receipt data and its PDF. Receipt PDF downloads verify the Invoice identity, media type, and SHA-256 headers. Other file downloads also verify Timbro's security metadata. HTTP problems preserve Timbro's status and machine-readable `code`. The distribution uses `jsonschema` and `regex` to validate data against the generated OpenAPI contract.

A `422` admission refusal has no retained Invoice ID. Correct the request and call `create_invoice`; `replace_invoice` applies to a retained replaceable Invoice. Authority-rejected numbering gaps still require the existing administrator authorization. A replacement retains the predecessor's Source ID, uses a new Source version and `invoiceReference`, and exposes its predecessor in `replacesInvoiceId`.

## Deliveries

Email an Invoice to its buyer and read the delivery back. Retrying with the same idempotency key (16 to 255 characters from `A-Z`, `a-z`, `0-9`, `.`, `_`, `:`, and `-`) returns the same delivery. `retrieve_delivery` returns `None` when Timbro has no such delivery for the Invoice.

```python
delivery = client.create_delivery(
    invoice["id"],
    recipient_email="buyer@example.do",
    idempotency_key="delivery-order-1042-0001",
    account_claim_url="https://example.do/account/claim",  # optional, https only
)
current = client.retrieve_delivery(invoice["id"], delivery["id"])
```

## Webhooks

`verify_webhook_signature` checks the Standard Webhooks signature over the original request bytes. `parse_invoice_webhook_event` does not check the signature, so call the verifier first. The parser then checks the event envelope against the generated contract and confirms that the event belongs to your organization, environment, and Source connection. An event type that this package version does not know is returned as an `UnknownInvoiceWebhookEvent` instead of raising; `is_known_invoice_webhook_event` narrows the result for type checkers.

```python
verify_webhook_signature(body, headers, signing_secret)
event = parse_invoice_webhook_event(
    body, message_id=headers["webhook-id"], organization_id=organization_id,
    environment="production", source_connection_id=source_connection_id,
)
if is_known_invoice_webhook_event(event) and event["type"] == "invoice.accepted":
    ...
```

## Errors and validation

- `InvalidInvoiceRequest`, a `ValueError`, is raised before any request is sent when a request does not match the contract. Its `path` attribute names the failing field as a JSON path, for example `$.items[0].quantity`; a failure on a method argument, such as `idempotency_key`, names the argument instead. The message never contains the rejected value.
- `TimbroHttpError` carries Timbro's `status`, machine-readable `code`, `request_id`, and `problems`. A response that breaks the contract raises it with the code `invalid_invoice_response`.
- `validate_contract_schema(name, value)` validates a value strictly against a named schema of the generated contract and raises `SchemaValidationError`, a `ValueError` with a `path`. Use it to check a payload before you store or queue it.
- `InvalidWebhook` is raised for a bad signature or signing secret, a malformed envelope, or an event outside your organization, environment, or Source connection.

## Development

Regenerate the contract projection with `python python-sdk/scripts/generate_contract.py`. Run the contract check and the package tests in a temporary virtual environment with `python python-sdk/scripts/verify.py`, or run only the tests with `PYTHONPATH=python-sdk python -m unittest discover -s python-sdk/tests`. Add `--wheel path/to/timbro_invoices-0.9.0-py3-none-any.whl` to `verify.py` to test a built wheel instead of the source tree.
