KSeF#
KSeF (Krajowy System e-Faktur) is Poland’s national e-invoice system. Querona reads its invoices in two stages: a scheduled job downloads them into a company folder, and you query the downloaded files from that folder like any other data.
Why not a live connection#
KSeF does not answer queries. Invoices are retrieved by an asynchronous export that has to be requested, polled for, downloaded in parts and decrypted, under a limit of twenty exports per rolling hour, with a per-subject checkpoint recording how far the last retrieval got. A single query could not be served without spending that budget, so acquisition is a scheduled synchronization that lands files, and querying happens afterwards over those files.
Synchronizing invoices#
The synchronization tool ships with Querona and runs as a job step of the KSeF
subsystem.
Note
Before the first step, an administrator has to add at least one allowed folder root under — there is no built-in default, and without one no KSeF step can be created. See Job subsystems settings.
Create one job per company:
EXEC sp_add_job @job_name = N'KSeF Apple';
EXEC sp_add_jobstep
@job_name = N'KSeF Apple',
@step_name = N'Sync invoices',
@subsystem = N'KSeF',
@command = N'--target = ''D:\KSeF\Apple''
nip = "5260001246"
certificate_thumbprint = "A1B2C3D4E5F60718293A4B5C6D7E8F9012345678"
ksef_environment = "PROD"
unzip = true';
The command carries two kinds of thing, and the leading dash tells them apart. A key written as a command-line
argument — --target — is passed to the tool as one. Everything else is a setting, and becomes the
tool’s config.toml: Querona writes it into the company folder exactly as you typed it, comments, ordering,
nested tables, arrays and all, so every setting that file accepts can be written in the step, including any the
tool gains in a later version. Querona reads the command only to find the target and to check what it can
check; it never decides what the tool may be told. The step needs no database, so none is asked for.
--target is the only argument a step may pass. What else the tool can be told to do is fixed when Querona
is built, not by whoever writes a job step.
The settings the tool understands today:
Setting |
Required |
Meaning |
|---|---|---|
|
yes |
The company’s tax identifier. |
|
yes |
Thumbprint of its KSeF certificate in the Windows store. |
|
yes |
|
|
no |
Where the first run starts, as a TOML date-time. Used only until the folder has a checkpoint. |
|
no |
Also extract the invoice XML beside each package ZIP. |
|
no |
How often to poll KSeF for a requested export (default 5). |
|
no |
How long to keep polling before giving up (default 15). |
|
no |
Stop the run after this many minutes, keeping everything downloaded so far. No default: runs until caught up. |
Write the required settings in lower case exactly as above: the file reaches the tool untouched and the tool
reads its settings case-sensitively, so NIP is refused when the step is saved rather than silently
corrected.
Three things cannot be set from the command. The target directory is passed as an argument, as above.
ksef_base_url is accepted but ignored — the tool resolves the endpoint from ksef_environment, so it
cannot be pointed elsewhere. Logging is configured by an optional appsettings.json in the company folder,
which Querona neither writes nor overwrites; place it there by hand if you need to change Serilog levels.
Everything else the tool accepts belongs in the command, including options it gains in a later version. The
tool also has command-line options that only make sense when a person runs it — --init to lay out a folder,
for instance — which is deliberate: manual runs during diagnostics or recovery keep working, and none of it is
needed for a scheduled step.
Note
The whole file is replaced on every run, so anything added to config.toml by hand is lost. Put it in the
step command instead — that is what makes the step, and not the folder, the record of how a company
synchronizes.
--target names the company folder — both where the invoices are written and where the tool reads its
configuration from. Because it is the folder the settings themselves live in, it cannot be one of them, which is
why it is written as the command-line argument it is. Writing target without the dashes is refused: the tool
would read it as a setting it does not have, ignore it, and leave the step pointing nowhere.
Give it a full path, which must resolve inside one of the folder roots configured under
(Job subsystems settings).
Single quotes make it a TOML literal string, so Windows backslashes need no escaping:
--target = 'D:\KSeF\Apple'.
Those roots are a whitelist and nothing more. Querona never picks one for you: which root a company’s invoices belong under is a decision only you can make, and a step that names no target is refused rather than defaulted.
Create the company folder before the first run. Querona never creates it: the folder holds an external system’s invoices under the retention, backup and access rules you arrange for it, and creating one automatically would silently accept a mistyped company name and start filling a directory nobody chose. A step whose folder is missing fails with a message naming the path. The Querona service account needs write access to it.
What Querona does do is write the tool’s configuration into that folder from the step before every run, so nothing has to be edited on the server and the step stays the single source of truth — editing the step takes effect on the next run. The rest of the folder — the synchronization checkpoint, the downloaded invoices and the tool’s logs — is left untouched.
The command is checked as the step is created: a command that is not TOML, omits a required setting, names an unknown environment or points outside the allowed roots is refused there, not hours later on the first scheduled run.
Different companies are independent and their jobs can run at the same time; two jobs must not target the same company folder.
Note
The account the Querona service runs as needs read access to each company’s KSeF certificate and its private key in the Windows certificate store. Without it, the step fails at authentication.
Scheduling#
Schedule the job hourly — each run keeps a safety margin under the KSeF export limit, so running it more often risks being rate-limited. One schedule can drive several companies’ jobs:
EXEC sp_add_schedule
@schedule_name = N'Hourly',
@freq_type = 4, -- daily
@freq_interval = 1, -- every day
@freq_subday_type = 8, -- sub-day interval in hours
@freq_subday_interval = 1; -- every hour
EXEC sp_attach_schedule
@job_name = N'KSeF Apple',
@schedule_name = N'Hourly';
Warning
Companies sharing one certificate should not run on the same schedule. Each run stays under the export limit on its own, which is enough when the limit is counted per company. If it is counted per certificate, companies that share one draw on a single budget, and running them together can exhaust it however careful each run is — with the last companies in the hour getting nothing.
Which of the two applies is not settled here. Until it is, stagger companies that share a certificate: give
them schedules starting at different minutes past the hour. The sign of collective exhaustion is the same
companies reporting exit_code 2 with no reason hour after hour while never catching up — see
Querying what was downloaded for reading that back. Companies with a certificate each are unaffected
either way.
The step fails if the tool reports a configuration, certificate or authentication problem, or if KSeF returns a response that leaves the checkpoint unable to advance — that last case would otherwise repeat the same query indefinitely without making progress. A run that stops early because the hourly export budget was spent is not a failure: the next scheduled run continues from the checkpoint.
Important
Give the job a timeout. Open the job in , set Timeout mode to Fixed duration and a Maximum duration — an hour is a sensible starting point, since the schedule is hourly. A synchronization step runs an external program, and a job timeout is what stops one that has stopped making progress: when it fires, Querona ends the tool and everything it started, records the run as canceled with the timeout as the reason, and does not retry it. The timeout is a job setting and can only be set in the portal — the job procedures have no parameter for it — so a job created purely in SQL has none until somebody adds one.
See Jobs and Schedules for jobs, steps and schedules in general.
What a run leaves behind#
A company folder holds the configuration written from the step, the synchronization checkpoint, the tool’s logs, a summary of the most recent run, and the invoices themselves — one directory per export package, filed by month:
Apple\
config.toml
checkpoint.toml
lastrun.json
logs\
invoices\
<yyyy-MM>\
<reference-number>\
<reference-number>_<subject>.zip
<reference-number>_<subject>_unzipped\ (only when unzip = true)
*.xml
The reference number is the one KSeF assigned the export, and subject is which side of the invoice it was
retrieved for (S1, S2, S3 or SAuth). The package ZIP is always kept, exactly as KSeF delivered
it; unzip = true additionally extracts the invoice XML beside it, which is what makes the invoices
queryable.
Stopping a run early#
A run can end early in two ways, and neither loses work.
Set max_run_minutes in the step’s command to bound how long a run may take — useful when the folder is
needed on a schedule, or to stop one step holding its slot indefinitely. On reaching the limit the run stops
itself and records reason = "deadline_reached".
A step that is cancelled — by a job timeout, or by an administrator — is not simply killed. Querona asks the tool to stop, gives it a moment to finish what it was doing, and terminates it only if it does not respond.
A run that stops this way keeps every invoice it had already downloaded, saves its checkpoint, and records
reason = "stopped_by_request" in the run summary. The next scheduled run continues from where it left off,
so nothing has to be re-downloaded and no export budget is wasted. The step itself is recorded as cancelled and
is not retried.
Nothing is left in an unusable state either way: a package is only published once it is complete, and the checkpoint is replaced in one step rather than rewritten in place, so even an interrupted run leaves the folder readable.
Querying what was downloaded#
Point a File connection at the company-folders root — see Files and data services — and read what the run produced.
The run summary is JSON, so read the file as text and pull values out of it:
SELECT JSON_VALUE(f.Text, '$.exit_code') AS exit_code,
JSON_VALUE(f.Text, '$.reason') AS reason,
JSON_VALUE(f.Text, '$.finished_at') AS finished_at
FROM OPENROWSET(BULK 'Apple/lastrun.json', DATA_SOURCE = 'ksef', FORMAT = 'TEXT') AS f;
An exit_code of 0 means every subject is caught up, and 2 that the run stopped early with work
remaining. subjects lists each subject with its status and the point its checkpoint reached. Reading
lastrun.json needs .json among the connection’s accepted text extensions — see TEXT.
Several situations end a run early, and all of them report 2 because none of them is a failure — the next
scheduled run resumes from the checkpoint. reason tells them apart:
|
What happened |
|---|---|
(absent) |
The run finished on its own terms; the exit code says how. |
|
Something asked the run to stop — a job timeout, an operator, or Ctrl+C when run by hand. Invoices already downloaded are kept. |
|
The run hit its own |
|
The run was abandoned. The exit code is |
The invoices are XML, and a second job step can process them as soon as the first finishes:
SELECT Path, Xml
FROM OPENROWSET(BULK 'Apple/invoices/**/*.xml', DATA_SOURCE = 'ksef', FORMAT = 'XML') AS invoices;
** spans subdirectories, which is what reaches the invoices inside each month’s package directories. Cast
Xml to the xml type to shred fields out of each invoice — see XML.
See also#
Jobs and Schedules — jobs, steps, subsystems and schedules
Job subsystems settings — the folder roots a synchronization may write into
XML — reading the downloaded invoices
TEXT — reading the run summary