GitHub

Download Invoices

Download processed invoice XML directly or through encrypted export packages with ksef2.

On this page

Use auth.invoices when you need invoice content after KSeF has processed it. The examples below assume you already have an authenticated client named auth.

Download one invoice

If you already have a KSeF number, direct download is the shortest path.

from pathlib import Path

ksef_number = "1234567890-20260625-..."

xml_bytes = auth.invoices.download(ksef_number)
Path("invoice.xml").write_bytes(xml_bytes)
python

If the invoice was just sent, KSeF may need time before the processed XML is downloadable. Pass timeout and download() polls until the document is available.

xml_bytes = auth.invoices.download(ksef_number, timeout=120.0, poll_interval=2.0)
Path("invoice.xml").write_bytes(xml_bytes)
python

Build an export filter

Use exports for larger downloads. Exports use InvoicesFilter, the same filter shape used by metadata queries.

from datetime import datetime, timedelta, timezone

from ksef2.models import InvoicesFilter

now = datetime.now(tz=timezone.utc)

filters = InvoicesFilter.for_buyer(
    date_type="permanent_storage",
    date_from=now - timedelta(days=1),
    date_to=now,
    restrict_to_permanent_storage_hwm_date=True,
)
python

Export many invoices

export() schedules the export and returns an ExportJob. Its wait() polls until the package is ready, downloads and decrypts the parts, joins them and returns an ExportedInvoices object.

job = auth.invoices.export(filters)
package = job.wait(timeout=300.0)

for ksef_number, xml in package.invoices():
    print(ksef_number, len(xml))

# Parsed _metadata.json (list of InvoiceMetadata), or None if absent.
metadata = package.metadata

# Package details for incremental sync.
print(package.package.is_truncated, package.package.permanent_storage_hwm_date)
python

Save files

from pathlib import Path

written = package.save(Path("downloads"))
python

save() extracts the archive and refuses any entry whose path would escape the target directory. The raw ZIP bytes are available as package.archive.

Resume after a restart

job.resume_state() returns an ExportResumeState with the reference number and the key that decrypts the package. Save its JSON, and export(state=...) gives the ExportJob back, so wait() still works.

job = auth.invoices.export(filters)
saved = job.resume_state().to_json()   # store as a credential, never log it

# after a restart
job = auth.invoices.export(state=saved)
package = job.wait(timeout=300.0)
python

Pass exactly one of filters or state. to_dict(), from_json() and from_dict() work like the session states; model_dump() and repr() redact the key.

After sending invoices

Keep sending, processing, and retrieval as separate phases.

  1. Send invoice XML through an online or batch session.

  2. Poll session or invoice status until KSeF reports a final accepted result.

  3. Persist the returned ksef_number values.

  4. Download one processed XML document by ksef_number, or build an export filter for a larger time window.

Download a batch under rate limits

You do not need a fixed sleep between download() calls, and you do not need to log in again after a 401. The SDK honors KSeF's Retry-After up to RetryConfig.max_retry_after (default 120 seconds), retries transient failures, and refreshes the access token once after a 401.

The one case the SDK hands back to you: KSeF asked to wait longer than the ceiling. Then download() raises KSeFRateLimitError immediately, without sleeping, and error.retry_after carries the true header value.

A single download() still blocks when the header is short enough to honor. If KSeF repeats the same Retry-After, that one call waits up to (max_attempts - 1) * max_retry_after before it raises, which is 240 seconds with the defaults. Budget for that in a long batch.

from pathlib import Path

from ksef2 import KSeFRateLimitError

downloads = Path("downloads")
try:
    for ksef_number in ksef_numbers:
        xml_bytes = auth.invoices.download(ksef_number)
        downloads.joinpath(f"{ksef_number}.xml").write_bytes(xml_bytes)
except KSeFRateLimitError as error:
    # KSeF asked to wait more than RetryConfig.max_retry_after seconds.
    # The SDK did not sleep. Schedule the remaining numbers for
    # error.retry_after seconds from now instead of retrying in a loop.
    print(f"slow down, resume in {error.retry_after} seconds")
python

See Operations for the full 429 and 401 behavior and what stays the app's job.

Next workflows

Export a PDF with a verification QR

Install the optional dependencies with pip install 'ksef2[pdf]' and the native libraries required by WeasyPrint.

from ksef2 import Environment
from ksef2.renderers import InvoicePDFExporter, PDFPageSize

exporter = InvoicePDFExporter(
    page_size=PDFPageSize.A4,
    verification_environment=Environment.TEST,
)
output = exporter.export_to_file(
    "invoice.xml",
    "invoice.pdf",
    ksef_number="9999999999-20260215-ABCDEF123456-AB",
)
python

Use the actual assigned KSeF number for the invoice. Supplying ksef_number adds a locally generated SVG QR and displays that number below it. Omitting it keeps the existing PDF output without a QR. The same keyword works with export_from_path() and export_from_string(). The environment defaults to Environment.PRODUCTION; select TEST or DEMO for invoices from those systems. The QR identifies the seller NIP, issue date and SHA-256 of the original XML bytes, encoded as unpadded Base64URL, according to the Ministry's verification QR specification. Use bytes when calling export_from_string(); a string is encoded as UTF-8. Reserializing an invoice changes its hash. Rendering does not check its acceptance or verify that the supplied KSeF number belongs to it.

This option supports invoices that already have an assigned KSeF number. It does not generate the offline certificate QR, sign URLs, or replace custom PDF layouts.

PDFPageSize supports A4, A4_LANDSCAPE, A3, A3_LANDSCAPE, LETTER and LETTER_LANDSCAPE. With no page_size, the existing stylesheet setting remains, normally A4 landscape. An explicit page size takes precedence over html_overrides CSS; the other CSS rules still apply. Large invoice tables may need landscape orientation or custom CSS to fit on a portrait page.