Skip to content

Build1 publisher3 min readPublished

Poland's KSeF: a batch session reference is not proof you delivered an invoice

Only the per-invoice UPO receipt evidences that an invoice reached the system. Integrations that store a session reference and call it delivered are filing invoices they cannot later prove.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Illustration accompanying Poland's KSeF: a batch session reference is not proof you delivered an invoice
Generated illustration

What happened

  • Poland's mandatory structured e-invoicing applies since 1 February 2026 for the largest companies, since 1 April 2026 for all active VAT payers, and from 1 January 2027 for everyone else, including the smallest and VAT-exempt.
  • The system is called KSeF (Krajowy System e-Faktur); an invoice is sent as XML in a schema called FA(3), which KSeF validates, assigns a number, and for which it returns a signed receipt. There is an OpenAPI spec and official SDKs in C# and Java.
  • The author spent the year building a KSeF integration in TypeScript on Deno, and it now files invoices in production.
  • Invoices can be sent in a batch session as one ZIP of XML files, hundreds at a time, and the session returns a reference number.
  • The document that legally proves an invoice reached the system is the UPO (Urzedowe Poswiadczenie Odbioru), and it is issued per invoice.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

Poland's structured e-invoicing mandate is already in force: since 1 February 2026 for the largest companies, since 1 April 2026 for all active VAT payers, and from 1 January 2027 for everyone else, including the smallest and the VAT-exempt [1]. That means a large number of integrations built this year are now the system of record for tax evidence, and at least one common shortcut in them does not hold up.

The shortcut is the batch session. You can send one ZIP of XML files, hundreds at a time, and the session returns a reference number [4]. According to a developer who documented a production TypeScript integration on dev.to, that reference is not proof of anything about a specific document: the artefact that legally proves an invoice reached KSeF is the UPO, the Urzedowe Poswiadczenie Odbioru, and it is issued per invoice [5][6]. A session reference proves a session happened [6]. If a client is asked to produce evidence for one named invoice, a session reference is not it [6].

The engineering consequence is a fan-out you have to budget for. A batch of 200 invoices means one upload followed by 200 UPO fetches [7], so 201 requests, of which the receipt fetches are 200 [8]. The author's report is that this fetch phase, not the upload, dominates wall-clock time [7]. Any queue design that models submission as a single job and treats completion as the upload's HTTP response will look fast and produce unprovable invoices.

The mirror-image error is treating the batch as atomic. A document that fails semantic validation is rejected individually while the other 199 in the same session go through normally [9]. Roll the whole batch back on one error and you re-send invoices that were already accepted, which produces duplicate invoice numbers [10]. Per-invoice status tracking is the same requirement viewed from the other side: you need per-invoice state because the system gives you per-invoice outcomes.

What makes this a documentation problem rather than a reading problem is that the surrounding failures are also silent. The same write-up describes validation that demanded a buyer tax ID even though roughly three in four invoices in one real customer's book have none [11], a validator and a serialiser twelve lines apart in the same file encoding the same rule differently and disagreeing for months without anyone noticing [12], and a session key call that became async, returned undefined for all three fields when destructured synchronously, and surfaced several calls later as a server-side complaint about an empty encryption field [13]. On Deno, the node:crypto compatibility path reportedly downgraded the RSA-OAEP mask generation function to SHA-1, producing ciphertext the server rejects while nothing fails locally [14]. None of these throw where the bug is.

Two things to check. First, whether your integration can produce a UPO for an arbitrary invoice number on demand, and how far back; that is the query an auditor asks, and it is cheap to test now and expensive to discover you cannot answer. Second, the 1 January 2027 cohort [1], which is where the smallest filers arrive, mostly on packaged software written by someone else. The receipt-fetch fan-out is the part that gets quietly dropped when a vendor optimises for a fast-looking send.

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories