ksef2 CLI
Scriptable terminal workflows for the ksef2 Python SDK.
Use local profiles, environment-backed authentication, JSON output, and resumable workflow files to automate KSeF invoicing from shells, CI jobs, and operational scripts.
What is ksef2 CLI?
ksef2-cli exposes the ksef2
Python SDK as a command-line tool. It is designed for developers and operators
who want to authenticate with KSeF, query invoice metadata, send invoices,
download invoice XML, export packages, inspect sessions, and manage
administrative resources without writing Python code.
The CLI avoids hidden state by default. Commands can read credentials from global options or environment variables, while local profiles store reusable non-secret defaults such as environment, NIP, auth method, certificate paths, and secret environment variable names.
This project is not published, endorsed, or supported by Poland's Ministry of Finance. Official KSeF documentation remains the source of truth for API behavior.
Install
KSeF2 CLI 1.0 requires Python 3.12 or newer and the published ksef2 SDK 1.1 or
newer, below 2.0. Install the CLI from PyPI:
uv tool install ksef2-cli
or:
pipx install ksef2-cli
The package exposes two equivalent executables:
ksef2 --help
ksef2-cli --help
Package metadata requires the ksef2 SDK >=1.1,<2, including fixes for certificate
retrieval and offline PDF code lookups. tests/unit/test_release_metadata.py fails if the project version,
those bounds, ksef2_cli.__version__, or the locked SDK drift apart.
Check what you are running, CLI and SDK together:
ksef2 --version
ksef2-cli 1.0.0
ksef2 1.1.0
--version is read before the local config file, so it answers even with a broken
or missing config. It prints plain text in every output mode.
Choose an environment
The default environment is TEST. An explicit --env wins over a profile, and a
profile's environment wins over the default, so --env production and a profile
that says environment = "production" both keep working unchanged:
ksef2 --env production --nip "$KSEF2_NIP" --token "$KSEF2_TOKEN" --json auth login
Examples below name TEST explicitly. A copy-pasted command that omits --env and a
profile now talks to TEST rather than to production.
Authenticate
For repeated local work, create a profile once and let commands inherit its environment, NIP, and authentication settings:
ksef2 profile create test-company \
--env test \
--nip 5261040828 \
--test-cert
ksef2 --profile test-company --json auth login
For CI and one-off scripts, pass global options before the command group:
ksef2 --env test --nip "$KSEF2_NIP" --token "$KSEF2_TOKEN" --json \
invoices metadata \
--role seller \
--date-from 2026-01-01T00:00:00Z \
--all
Supported direct authentication methods:
--tokenorKSEF2_TOKEN--test-certfor the TEST environment--certand--keyfor PEM XAdES credentials--p12for PKCS#12/PFX XAdES credentials--auth-stateto reuse state saved byauth login --save-state
auth login, auth resume, and auth refresh mask both token values in text and in
JSON output. Each line shows a short prefix and suffix of the token next to its expiry
time, so a log, a shell transcript, or a pasted issue comment keeps nothing reusable.
--show-tokens prints the raw bearer tokens instead, and a command that also uses
--save-state stays masked even under --show-tokens, because the file is where the
credential belongs. When a later command needs the credential, save it rather than
capturing the output.
Saving auth state lets later commands skip a fresh KSeF login:
ksef2 --profile test-company --json auth login --save-state ksef2-auth.json
ksef2 --env test --auth-state ksef2-auth.json --json sessions auth-list
--save-state holds the access and refresh bearer tokens in plain text. The CLI
writes the file with mode 0600, which stops other users reading it and does nothing
else: it is not encrypted, and it survives chmod-preserving copies, backups, and
git add. Keep it outside the repository, or ignore it in .gitignore, and put it
somewhere private such as $XDG_RUNTIME_DIR on a shared machine.
Each command that reuses the state writes any refreshed token back to that file, and
keeps that mode. auth refresh accepts the same --auth-state, so a refresh needs
neither a token in the arguments nor a fresh login:
ksef2 --env test --auth-state ksef2-auth.json --json auth refresh
End the session and remove the file with ksef2 --auth-state ksef2-auth.json --json auth logout.
Send and download invoices
Send one XML invoice, wait for processing, save a UPO, and keep a receipt for later status checks:
ksef2 --profile test-company \
invoices send invoice.xml \
--wait \
--upo-dir upos \
--receipt invoice-receipt.json
Download the UPO later from the saved receipt:
ksef2 --profile test-company \
invoices upo \
--receipt invoice-receipt.json \
--out invoice-upo.xml
Submit many XML files as one batch:
ksef2 --env test --nip "$KSEF2_NIP" --token "$KSEF2_TOKEN" \
invoices send invoice-1.xml invoice-2.xml \
--mode batch \
--wait \
--upo-dir upos
Prepare a batch separately when its parts should survive a failed upload:
ksef2 --env test --nip "$KSEF2_NIP" --token "$KSEF2_TOKEN" \
batch prepare invoice-1.xml invoice-2.xml --out batch-parts
ksef2 --env test --nip "$KSEF2_NIP" --token "$KSEF2_TOKEN" \
batch submit --prepared batch-parts --state-file batch-state.json
List and record-query commands print aligned plain-text tables with uppercase
headers and one row per record. Empty lists print No results.. Identifiers stay
complete; long names and descriptions may end in …, and optional columns may
disappear on narrow terminals. Datetime columns use UTC, dates use YYYY-MM-DD,
and amounts use two decimal places. Redirected output uses a 120-column layout.
Pagination metadata, when returned by the command, appears below the table.
An online receipt carries the session AES key and IV in plain text, exactly as a session
state file does; a batch receipt holds the session reference and the file names. Every
one of them is written with mode 0600, and every one of them is a credential for as
long as it can continue the workflow.
online send decides what to do with the session it used by how it got it: a session
the command opened closes when the command ends, and a session resumed with
--state-file stays open for the next command. --close and --keep-open say which
you want regardless.
Stopping a send after KSeF accepted the invoice does not unsubmit it. Ctrl-C, a
timed-out --wait, and a dropped connection all end the command with the invoice
still inside the session, so read the saved receipt or state file, or ask with
invoices status, online list, or batch status, before sending the file a second
time.
Use --json before the command group for all fields or when scripts consume the
output. JSON retains the SDK fields, values, and nulls.
Output and exit codes
A successful command writes to stdout: plain text by default, one JSON document with
--json. Command errors go to stderr as plain text or one JSON object in
--json mode, leaving stdout empty. A completed invoices send can instead
return a result on stdout with rejected invoice items and exit 1.
In --json mode that object carries one error:
{"error": {"type": "RemoteServiceError", "title": "KSeF rejected the request", "message": "...", "details": [], "hints": [], "exit_code": 1, "ksef": {"status_code": 400, "ksef_code": 21405, "trace_id": "..."}}}
type names the CLI error class and title gives the short summary. details,
hints, and exit_code are always present. The ksef object appears when KSeF
answered, and holds that service's status_code, ksef_code, and trace_id; quote
the trace ID when you report an incident. report_url appears on an unexpected CLI
error, and traceback appears with --verbose. A rejected command line reports
"type": "UsageError" with "exit_code": 2.
| Exit code | Meaning |
|---|---|
0 |
Success. |
1 |
Runtime failure: a rejected request, a KSeF timeout, missing or contradictory credentials, a bad file. |
2 |
Usage failure: unknown option, missing argument, or an invalid value such as a bad NIP checksum or an unreadable --auth-state path. |
130 |
Interrupted (Ctrl-C). The command may still have changed something server-side. |
invoices send exits 1 when any file failed, after printing every per-file result.
Documentation
- Online docs: https://docs.stackinghq.com/ksef2/cli/intro/
- Installation: https://docs.stackinghq.com/ksef2/cli/getting-started/installation/
- Quickstart: https://docs.stackinghq.com/ksef2/cli/getting-started/quickstart/
- Command reference: https://docs.stackinghq.com/ksef2/cli/reference/commands/
- Source docs:
docs - SDK repository:
stacking-hq/ksef2
Development
uv sync --all-groups
uv run ksef2 --help
uv run ksef2 --version
uv run python -m coverage run -m pytest -q
uv run python scripts/validate_docs_frontmatter.py docs/
uv sync installs the published ksef2 SDK from PyPI, and uv.lock pins the exact
version. See CONTRIBUTING.md for the checks that keep the two in
step.
The CLI command domains live under src/ksef2_cli/commands/. Source
documentation lives under docs/.
Contributing
Issues and pull requests are welcome. Before opening a PR, run the focused test or docs check that covers your change, and update source docs when command behavior changes.
Metadata
Release files for ksef2-cli 1.0.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 | Provenance | Actions |
|---|---|---|---|---|
| ksef2_cli-1.0.0.tar.gz | 1.8 MB | With provenance | Details for ksef2_cli-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ksef2_cli-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for ksef2_cli-1.0.0-py3-none-any.whl |
Total release size: 1.9 MB
Release files / ksef2_cli-1.0.0.tar.gz
| Download URL | ksef2_cli-1.0.0.tar.gz |
|---|---|
| Size | 1.8 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
df589d05f5fec82ea797676eb5d304de8851b6f8865682ef79dd3c7d2232b3ff
|
|
BLAKE2b-256 checksum How to use checksums |
58115060a847e291903f293573f8da4c5962f70515abbe840b631f025f8e5823
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logRelease files / ksef2_cli-1.0.0-py3-none-any.whl
| Download URL | ksef2_cli-1.0.0-py3-none-any.whl |
|---|---|
| Size | 106.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
20e367a259c3984583310f343b77444d03e94693d5c25baff1db5037744dad3a
|
|
BLAKE2b-256 checksum How to use checksums |
37320887667da5257a1e8c15669a37733ddfb61b0cd6f74d75e0078c588225b9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logSecurity for ksef2-cli 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for ksef2_cli-1.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 26, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.