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 Administer ‣ Configuration ‣ Job subsystems ‣ KSeF — 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

nip

yes

The company’s tax identifier.

certificate_thumbprint

yes

Thumbprint of its KSeF certificate in the Windows store.

ksef_environment

yes

DEMO, TEST or PROD.

from_date

no

Where the first run starts, as a TOML date-time. Used only until the folder has a checkpoint.

unzip

no

Also extract the invoice XML beside each package ZIP.

polling_interval_seconds

no

How often to poll KSeF for a requested export (default 5).

polling_timeout_minutes

no

How long to keep polling before giving up (default 15).

max_run_minutes

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 Administer ‣ Configuration ‣ Job subsystems ‣ KSeF (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 Administer ‣ Job management, 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:

reason

What happened

(absent)

The run finished on its own terms; the exit code says how.

stopped_by_request

Something asked the run to stop — a job timeout, an operator, or Ctrl+C when run by hand. Invoices already downloaded are kept.

deadline_reached

The run hit its own max_run_minutes limit. Invoices already downloaded are kept. If this keeps happening the runs are not keeping up: raise the limit, or schedule them more often.

error

The run was abandoned. The exit code is 1 and the company folder’s log says what failed.

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#