GitHub

Quickstart

Generate, send, confirm, and download a TEST invoice with ksef2.

On this page

This quickstart follows one TEST invoice through the SDK: build an FA(3) invoice, authenticate with a TEST certificate, send it in an online session, download the UPO, and fetch the processed invoice XML back from KSeF.

Install

Choose your preferred installation method:

Install with pip

pip install ksef2
bash

Install with uv

uv add ksef2
bash

ksef2 requires Python 3.12 or newer.

Code example

Copy the following into a quickstart.py file.

from datetime import date, datetime, timezone
from decimal import Decimal
from pathlib import Path

from ksef2 import Client, Environment, FormSchema
from ksef2.fa3 import FA3InvoiceBuilder, VatRate

SELLER_NIP = "5261040828"
DOWNLOADS = Path("downloads")

invoice_number = f"QS/{datetime.now(timezone.utc):%Y%m%d%H%M%S}"
invoice_xml = (
    FA3InvoiceBuilder()
    .header(system_info="ksef2 quickstart")
    .seller(
        name="Demo Seller Sp. z o.o.",
        tax_id=SELLER_NIP,
        country_code="PL",
        address_line_1="Prosta 1",
        address_line_2="00-001 Warszawa",
    )
    .buyer(
        name="Demo Buyer Sp. z o.o.",
        country_code="PL",
        address_line_1="Kwiatowa 2",
        address_line_2="00-002 Warszawa",
    )
    .standard()
        .issue_date(date.today())
        .issue_place("Warszawa")
        .invoice_number(invoice_number)
        .rows()
            .add_line(
                name="Consulting service",
                quantity=Decimal("1"),
                unit_price_net=Decimal("100.00"),
                vat_rate=VatRate.VAT_23,
            )
        .done()
    .done()
    .to_xml()
    .encode("utf-8")
)

DOWNLOADS.mkdir(exist_ok=True)
(DOWNLOADS / "generated-invoice.xml").write_bytes(invoice_xml)

with Client(Environment.TEST) as client:
    auth = client.authentication.with_test_certificate(nip=SELLER_NIP)

    with auth.online_session(form_code=FormSchema.FA3) as session:
        submission = session.send_invoice(invoice_xml)
        print(f"Sent invoice: {submission.reference_number}")

        status = submission.wait(timeout=120.0)
        print("Invoice status:")
        print(status.model_dump_json(indent=2))

        upo_xml = submission.download_upo()
        (DOWNLOADS / "upo.xml").write_bytes(upo_xml)
        print("Saved downloads/upo.xml")

    if status.ksef_number is None:
        raise RuntimeError("KSeF did not assign an invoice number.")

    downloaded_xml = auth.invoices.download(status.ksef_number, timeout=120.0)
    (DOWNLOADS / "processed-invoice.xml").write_bytes(downloaded_xml)
    print("Saved downloads/processed-invoice.xml")
python

Run the Python script:

python quickstart.py
# or
uv run quickstart.py
bash

The script writes three local files:

  • downloads/generated-invoice.xml
  • downloads/upo.xml
  • downloads/processed-invoice.xml

After the script completes, inspect the generated XML, the UPO, and the XML returned by KSeF. The generated and processed invoice XML should represent the same invoice; keep the processed XML as the KSeF-returned copy.

What happened

  1. FA3InvoiceBuilder created a minimal FA(3) invoice XML using public SDK field names such as invoice_number, tax_id, and unit_price_net.

  2. Client(Environment.TEST) selected the TEST KSeF base URLs and owned the SDK HTTP resources.

  3. with_test_certificate() authenticated the TEST context represented by SELLER_NIP.

  4. online_session(FormSchema.FA3) opened an online session for FA(3) invoice submission.

  5. send_invoice() submitted the invoice and returned an InvoiceSubmission handle that carries the session invoice reference number.

  6. submission.wait() polled until KSeF assigned a KSeF invoice number.

  7. submission.download_upo() downloaded the UPO XML, and auth.invoices.download() fetched the processed invoice XML.

Adapt the script

Change the invoice fields or add more lines when you adapt the script. To move the same shape into DEMO or PRODUCTION, change the root Environment and use a credential that belongs to that environment.

For repeated local work, create a CLI-compatible profile and replace the authentication line with client.authentication.with_profile("test-company"). Keep the profile environment aligned with the root client environment.