timbro-invoices
The Python SDK for Timbro Invoices: issue, retrieve, deliver, and download Invoices, share accepted receipts, respond to received supplier documents, and verify signed webhooks. It needs Python 3.11 or newer and depends only on httpx and timbro-protocol.
pip install timbro-invoices
Issue an Invoice
from timbro_invoices import TimbroInvoices
with TimbroInvoices(api_key=secret_key, application="my-erp/1.0") as timbro:
preview = timbro.invoices.preview(invoice_request)
invoice = timbro.invoices.create(invoice_request)
current = timbro.invoices.retrieve(invoice["id"])
if current["files"]["pdf"]["status"] == "ready":
pdf = timbro.invoices.download_pdf(current["id"])
api_key is a secret key with the invoices capability. base_url defaults to https://api.timbro.do; an independent installation passes its own origin and key. Its invoice and received-document operations match; its webhook registration differs from webhook_endpoints.create, so reach it with request. The constructor, timeouts, retries, and http_client work as in timbro-payments.
Requests and responses are plain dictionaries with the API's camelCase keys, typed as TypedDicts in timbro_invoices.types. invoice_request is the same JSON body HTTP accepts. Responses are not validated, so fields and enum values added by a later API version pass through unchanged.
invoiceReference identifies one issuance intent. Repeating a create with the same request returns the same Invoice, so a create whose answer was lost is safe to repeat, and invoices.list(invoice_reference=...) finds it. AsyncTimbroInvoices has the same namespaces: await each call and use async for with list_all.
| Namespace | Methods |
|---|---|
invoices |
create, preview, retrieve, list, list_all, replace, deliver, retrieve_delivery, download_xml, download_pdf, share_receipt, retrieve_receipt, download_receipt_pdf |
received_documents |
list, list_all, retrieve, respond, download_xml |
webhook_endpoints |
create, retrieve, list, list_all, delete, suspend, reactivate, rotate_secret, list_deliveries, list_all_deliveries, replay |
business |
retrieve, update, configure_invoicing, import_signing_certificate, issue_test_invoice, issue_invoice_key |
api_keys |
rotate |
webhook_endpoints, business, and api_keys are the account controls timbro-payments exposes too, generated identically, so an invoices-only integration needs no other package. The business writes need an administrator key. The checkout policy, Merchant Accounts, and onboarding stay in timbro-payments.
Path identifiers are positional, the request body is the positional request, and filters and options are keyword-only. The names follow @timbro/invoices. Where Python differs:
retrieve,retrieve_delivery,retrieve_receipt, andreceived_documents.retrieveraiseNotFoundErrorfor a missing resource; the JS methods returnundefined.deliver(invoice_id, {"recipientEmail": ...}, idempotency_key=...)andretrieve_delivery(invoice_id, delivery_id)take arguments instead of one command object.list_allandlist_all_deliveriesiterate every page; JS has no counterpart.api_keys.rotatehas no counterpart in@timbro/invoices; JS exposes it in@timbro/paymentsonly.- File downloads return
contentrather thanbytes, and the metadata headers underheaders; a received document's digest isheaders.artifact_sha256, notcontentSha256.
Deliveries and receipts
invoices.deliver emails the Invoice to its buyer. Its idempotency_key is required: derive it from your durable record of the delivery and send the same key on every retry, so a retry returns the same delivery.
delivery = timbro.invoices.deliver(
invoice["id"],
{"recipientEmail": "buyer@example.do"},
idempotency_key=f"{invoice['invoiceReference']}:delivery:1",
)
delivery = timbro.invoices.retrieve_delivery(invoice["id"], delivery["id"])
invoices.share_receipt(invoice_id) returns an InvoiceReceiptAccess grant, {id, invoiceId, accessToken}, for an accepted Invoice. retrieve_receipt(access) and download_receipt_pdf(access) authenticate with that grant instead of your API key, and the token never appears in a URL. Keep the grant on your server and hand it to the receiving service over an authenticated channel; it reaches only the receipt and its PDF.
Answers are checked against what you asked for: an Invoice, delivery, receipt grant, receipt, received document, or webhook endpoint that names another identifier than the one requested raises UnexpectedResponseError with the code response_identity_mismatch.
Files
download_xml, download_pdf, download_receipt_pdf, and received_documents.download_xml return the file only after its bytes and metadata agree with the headers Timbro sends. Each result has the bytes as content, its media_type, and the checked metadata headers as headers:
| Result | Checked | headers |
|---|---|---|
InvoiceXmlDownload |
media type, SHA-256, Invoice identity, creation time, certificate digest, security code | InvoiceXmlHeaders: file_sha256, invoice_id, file_created_at, certificate_sha256, security_code |
InvoicePdfDownload |
the same file checks, the PDF digest, document type, eNCF, layout, and page count | InvoicePdfHeaders: file_sha256, invoice_id, file_created_at, pdf_sha256, encf, invoice_type, pdf_layout, page_count, signed_source_sha256 |
ReceivedDocumentDownload |
media type and SHA-256 of the original bytes | ReceivedDocumentHeaders: artifact_sha256 |
pdf = timbro.invoices.download_pdf(invoice["id"])
print(pdf.headers.encf, pdf.headers.page_count, len(pdf.content))
A mismatch raises FileVerificationError, an UnexpectedResponseError, with the code file_integrity_mismatch when the bytes, media type, or identity differ, or file_metadata_invalid when a header is missing or malformed. Any answer larger than 10 MiB is refused with the code response_too_large.
Received documents
for summary in timbro.received_documents.list_all(state="received", response="pending"):
xml = timbro.received_documents.download_xml(summary["id"])
archive(xml.content)
timbro.received_documents.respond(summary["id"], {"decision": "accepted"})
A commercial response accepts a supplier document, or rejects it with {"decision": "rejected", "motive": ...}. It does not post a bill or issue an Invoice.
Errors
Errors share one hierarchy with timbro-payments, from timbro-protocol: TimbroError carries code, message, request_id, operation and its SDK method (such as invoices.create), retryable, outcome_unknown, and recovery and its recovery_method; branch on method to treat one package's calls differently, and APIError adds status, title, problems, retry_after_seconds, and the whole Problem as body. InvalidRequestError (400 or 422) lists each field at fault in problems, as JSON Pointers into your request. Problem members specific to Invoices are in body:
from timbro_invoices import APIError
try:
invoice = timbro.invoices.create(invoice_request)
except APIError as error:
if error.code == "fiscal_issuance_blocked":
blockers = error.body.get("issuanceBlockers", [])
invoice_id = error.body.get("invoiceId") # present when the refusal retained an Invoice
When outcome_unknown is true, a write may have taken effect although the call failed. The error text says what to do next.
Routes outside the published contract
Prefer the typed methods. For a route the published contract does not include yet, such as a Stamp installation's own readiness read, request sends one call through the same client, with the same API key, user agent, redirect refusal, 10 MiB limit, and errors:
readiness = timbro.request("GET", "/v1/stamp/readiness")
path must be a /v1/ path; an absolute URL, a query or fragment in the path, and an empty or dot segment raise ValueError, and query= carries the filters. The answer is the decoded JSON object, or None for an empty one, and nothing checks its shape. A GET is retried when safe; any other method is retried only when you pass idempotency_key=. An error it raises has method "request", operation set to the method and path, such as "GET /v1/stamp/readiness", and no recovery. AsyncTimbroInvoices clients have the same method to await.
Webhooks
Register an endpoint once, and store its signing secret; Timbro shows it only in this answer:
endpoint = timbro.webhook_endpoints.create(
{"url": "https://erp.example.do/timbro/webhooks", "enabledEvents": ["invoice.accepted"]},
idempotency_key="erp-webhook-endpoint-1",
)
store_secret(endpoint["secret"])
Verification needs no API key and accepts exactly the deliveries timbro_payments.webhooks.verify accepts. The endpoint's signing secret authenticates the delivery:
from timbro_invoices import webhooks
event = webhooks.verify(body=raw_body, headers=request_headers, secret=webhook_secret)
inbox.store(event) # Deduplicate by event["id"], then acknowledge.
if webhooks.is_known_event(event) and event["type"] == "invoice.accepted":
invoice = timbro.invoices.retrieve(event["data"]["id"])
Events of a type this version does not know verify and return as UnknownWebhookEvent. A failure raises WebhookVerificationError, whose reason names the check.
Receivers bound to several Source connections
A receiver that serves several Source connections from one endpoint, such as an ERP with one connection per company, can also confirm which one an event belongs to. verify_invoice_scope raises WebhookVerificationError with the reason scope_mismatch unless the event names the expected organization, fiscal environment, and Source connection:
webhooks.verify_invoice_scope(
event,
organization_id=company.timbro_organization_id,
environment=company.timbro_environment,
source_connection_id=company.timbro_source_connection_id,
)
The organization and Source connection IDs come from the Source connection your integration recorded when it connected to Timbro; the Gateway API does not return them. A receiver of a single endpoint secret does not need this check.
Offline request validation
Timbro validates every request and answers an invalid one with InvalidRequestError. To check a request without sending it, such as requests retained before an upgrade, install the validation extra:
pip install "timbro-invoices[validation]"
from timbro_invoices.validation import InvalidInvoiceRequest, validate_invoice_request
try:
validate_invoice_request(retained_request)
except InvalidInvoiceRequest as error:
for problem in error.problems:
print(problem["path"], problem["code"]) # "/items/0/quantity", "type"
The check is strict, against the InvoiceRequest schema this version bundles. Messages never contain the rejected value. The extra adds jsonschema and regex; the client never imports them.
Testing
timbro_invoices.testing builds schema-valid Invoices, received documents, and Invoice events, and signs webhook fixtures. invoice_xml_download, invoice_pdf_download, and received_document_download build the verified results the download methods return, with the digests of the content you pass, and api_error(status, code, method="invoices.create") builds the APIError subclass that method raises for that status. Fake the API by giving the client an httpx.MockTransport:
import json
import httpx
from timbro_invoices import TimbroInvoices, testing
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json=testing.invoice(status="accepted"))
timbro = TimbroInvoices(
api_key="sk_test_123",
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
)
event = testing.invoice_event("invoice.accepted")
body = json.dumps(event).encode()
headers = testing.sign_webhook(body, "whsec_...", webhook_id=event["id"])
Migrating from 0.10
0.11 moves with timbro-payments and timbro-protocol; upgrade them together. Invoices carry fiscalIdentity.signature (null until signed, then signedAt, securityCode, and verificationUrl) for printing the e-CF, deliveries accept attachments and report them, and two events announce delivery outcomes: invoice.deliverySent and invoice.deliveryFailed, whose data adds deliveryId. The buyer email now leaves only after DGII accepts the Invoice.
Migrating from 0.9
0.10 moves timbro-invoices onto the shared timbro-protocol runtime, with namespaces generated from the Gateway contract. Packages that share a minor version form one compatible set, so upgrade timbro-payments and timbro-protocol to 0.10 with it.
| 0.9 | 0.10 |
|---|---|
Python 3.10, jsonschema, regex |
Python 3.11, httpx, timbro-protocol; jsonschema and regex only in the validation extra |
TimbroInvoices(api_key, base_url=, transport=) |
TimbroInvoices(api_key, base_url=, timeout=, max_retries=, http_client=, application=); fake the API with httpx.MockTransport |
create_invoice(request, idempotency_key=) |
invoices.create(request, idempotency_key=) |
preview_invoice(request) |
invoices.preview(request) |
retrieve_invoice(invoice_id) |
invoices.retrieve(invoice_id) |
list_invoices(invoice_reference=, type=, cursor=, limit=) |
invoices.list(...) with the same keywords, or invoices.list_all(...) |
replace_invoice(invoice_id, request) |
invoices.replace(invoice_id, request) |
create_delivery(invoice_id, recipient_email=, idempotency_key=, account_claim_url=) |
invoices.deliver(invoice_id, {"recipientEmail": ..., "accountClaimUrl": ...}, idempotency_key=) |
retrieve_delivery(...) returning None for fiscal_delivery_not_found |
invoices.retrieve_delivery(...) raising NotFoundError with that code |
download_xml, download_pdf, download_receipt_pdf returning InvoiceFile with a metadata dict |
the same names on invoices, returning InvoiceXmlDownload or InvoicePdfDownload with typed headers |
share_receipt, retrieve_receipt, download_receipt_pdf |
the same names on invoices, taking the same grant |
| Received documents through raw HTTP | received_documents.list, list_all, retrieve, respond, download_xml |
TimbroHttpError with status, code, request_id, path, problems, retryable |
APIError and its subclasses with the same members except path, which is problems[0]["path"]; status=0 is now APIConnectionError |
artifact_integrity_mismatch, artifact_metadata_invalid |
FileVerificationError with file_integrity_mismatch, file_metadata_invalid |
invalid_problem_response |
UnexpectedResponseError with unexpected_http_error; a failure body needs only a string code |
invalid_invoice_response for a response outside the schema |
none: responses pass through; a response for another resource raises response_identity_mismatch |
Methods raising InvalidInvoiceRequest(path="$.items[0].quantity") before sending |
Timbro answers InvalidRequestError with problems[].path such as /items/0/quantity |
validate_contract_schema("InvoiceRequest", value), SchemaValidationError |
validation.validate_invoice_request(value), validation.InvalidInvoiceRequest (a ValueError) with problems |
verify_webhook_signature(body, headers, secret, now=) |
webhooks.verify(body=, headers=, secret=, now=), which also returns the event and accepts rotated secrets |
parse_invoice_webhook_event(body, message_id=, organization_id=, environment=, source_connection_id=) |
webhooks.verify(...), then webhooks.verify_invoice_scope(event, organization_id=, environment=, source_connection_id=) |
is_known_invoice_webhook_event(event) |
webhooks.is_known_event(event) |
InvoiceWebhookEvent, UnknownInvoiceWebhookEvent |
types.InvoiceWebhookEventEnvelope (InvoiceAcceptedEvent and its siblings), webhooks.UnknownWebhookEvent |
InvalidWebhook |
WebhookVerificationError with a reason |
decode_signing_secret(secret) |
none: webhooks.verify raises WebhookVerificationError with the reason invalid_secret, and accepts keys of any length |
Behavior that changed besides the names:
- Responses are not validated against the contract, so a missing or unknown member no longer raises.
- Webhook envelopes are checked for the members each known type requires, not for UUID, date-time, or environment formats;
verify_invoice_scopestill compares the organization and Source connection as UUIDs. - The client no longer checks request bodies or the delivery key's format before sending; Timbro refuses an invalid request with
InvalidRequestError.
License
No license has been granted for this package yet. The license will be declared before the first public release.
Metadata
Release files for timbro-invoices 0.11.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| timbro_invoices-0.11.0.tar.gz | 74.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| timbro_invoices-0.11.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 136.0 kB
Release files / timbro_invoices-0.11.0.tar.gz
| Download URL | timbro_invoices-0.11.0.tar.gz |
|---|---|
| Size | 74.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bbdc1989793c67bbb28fc7ac793d03b6c8ba17ff4884fd48c83dbb5fdaeab906
|
|
BLAKE2b-256 checksum How to use checksums |
c1ab2f2dc5bfc86c85eb43d4c4fc048a7f5ea86afc1ddd783e257066cc208b71
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|
Release files / timbro_invoices-0.11.0-py3-none-any.whl
| Download URL | timbro_invoices-0.11.0-py3-none-any.whl |
|---|---|
| Size | 61.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c7bbaa03018586cd7e8b665ed986ad1c261aa76d743dd9439adeec8bd921ed3a
|
|
BLAKE2b-256 checksum How to use checksums |
51c55bd8413d950025f5e0da436db7848aa74f01a1a38750ab06a1a3baa066ca
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|