← Back to blog

KSeF API in .NET: the most common errors and how to handle them

KSeF is an async, rate-limited, stateful government system. These are the error categories we hit in .NET integrations and the concrete patterns to handle each one.

KSeF is asynchronous, rate-limited, and has its own session and state. Those are four properties that a normal REST API does not have all at once. The result: an integration that passed testing fails in production in a way that is invisible — until an audit arrives.

Here are the error categories we hit most often in .NET integrations, and patterns for handling each one. This is not a list from the documentation. It is a list of problems we actually saw and had to fix.

1. Session expiration — silent death

A KSeF session has limited validity. The problem occurs when the application creates a session, stores the token in memory, and assumes it is valid forever. On the first call after expiration the request fails with an authorization error, the retry repeats it with the same token, and the cycle continues.

Handling pattern: never store the session token only in memory. Verify token validity before every call, not once at startup. Refresh proactively — not after the first error. Session state (token + expiry time) lives in the DB or a shared cache so all application instances can see it.

2. Rate limiting — HTTP 429 always arrives at the worst moment

KSeF restricts the number of requests per time unit. When sending invoices in bulk at month-end you will hit this limit reliably. Without handling it, sending stops mid-batch and part of the invoices go nowhere.

Handling pattern in .NET: Polly RateLimiter policy or a custom token-bucket. Two things matter:

  1. The rate limiter must be shared state — in-memory in a single process is not enough if you run multiple instances. A shared token-bucket via Redis ensures instances do not override each other's quota.
  2. An HTTP 429 response must put the request back in the queue, not end in an error. The KSeF error response usually contains how long to wait — respect that interval.

3. Retry without idempotence = duplicates in the government system

A transient error, a timeout, or an application restart. The application sends the invoice again on retry. KSeF accepts it as new. Result: one invoice in accounting, two in KSeF.

This is a problem that is hard to fix after the fact. The Polish tax authority does not try to reconcile invoices in KSeF with your ERP — you will only find out during an audit.

Handling pattern: every invoice gets a deterministic idempotency key before the first submission. The key is derived from invoice data (number, date, tax ID) — not from a random UUID. When submission is retried, the system recognises the key and does not send the invoice a second time. KSeF has no native idempotency mechanism at the API level — you must implement this logic yourself in the integration layer.

4. Invoice validation errors — discovered too late

KSeF rejects an invoice that does not match the FA(2) schema or violates its business rules. Rejection arrives asynchronously — not immediately on submission but only when polling for the result. This means a validation error is deferred: submission goes through, we write the invoice as "sent", and only on the next polling do we find out it was rejected.

Categories we encounter most often: incorrect NIP format, missing or invalid timestamps, inconsistent VAT rates, or incorrect invoice line structure.

Handling pattern: validate on the .NET side before submission, rather than relying solely on the KSeF response. The FA(2) schema is available as XSD — validate the XML document before sending it to the API. Rejection from KSeF is caught during polling, written to the audit log, and triggers an alert, not just a LogWarning.

5. Waiting for UPO — the polling nobody wrote

Submitting an invoice is step 1. KSeF processing is asynchronous — the result (UPO, Urzędowe Poświadczenie Odbioru) arrives after some time. Without active polling the invoice status never updates. The invoice sits in "sent" state and nobody knows whether the government accepted it.

Handling pattern: after each submission the invoice status is written to the DB as Pending. A background job periodically polls KSeF for the result. Once UPO arrives (or a rejection), the status is updated and stored. UPO is the proof. An invoice without UPO is not a done invoice.

6. Timeout and government system outage — without a queue it won't work

KSeF has outages. That is not a hypothesis — it is a fact that applies to every government system. With a synchronous call inside an HTTP request, a timeout or outage means a lost invoice. The app throws an exception, the user sees an error, the invoice went nowhere.

Handling pattern: every KSeF call happens asynchronously outside the HTTP request. The invoice is first written to the DB as a task to be sent. A background worker picks it up and sends it. When KSeF is down the task stays in the queue and is retried after the backoff interval expires. Polly RetryPolicy with exponential backoff + jitter ensures instances do not flood the API when availability is restored.

// Example Polly policy with exponential backoff
var retryPolicy = Policy
    .Handle<KSeFApiException>(ex => ex.IsTransient)
    .WaitAndRetryAsync(
        retryCount: 5,
        sleepDurationProvider: attempt =>
            TimeSpan.FromSeconds(Math.Pow(2, attempt))
            + TimeSpan.FromMilliseconds(Random.Shared.Next(0, 500)));

7. Missing reconciliation — you don't know what you don't know

This is the piece every first integration skips. And it is the most dangerous failure — not because it fails loudly, but because it fails silently.

A reconciliation job asks once a day (or once an hour): "Are all invoices in a consistent state?" Does every submitted invoice have either a UPO or a reasoned rejection? Is anything stuck in Pending longer than is reasonable to wait? Did an orphan appear — a document that was sent to KSeF but got lost in our DB?

Without reconciliation we are relying on everything having gone correctly. In a distributed system that is not enough.

8. Without an anti-corruption layer the SDK affects your entire codebase

The KSeF .NET SDK has its own quirks — proprietary error strings, numeric codes, a session model that changes between versions. If you scatter SDK calls across the whole application, every SDK change becomes a project-wide refactor.

Handling pattern: wrap the SDK in your own layer (anti-corruption layer). This layer translates KSeF concepts (session, UPO, rejection) into your domain types. The rest of the application does not know the SDK — it only knows your interface. SDK change = change in one file.

How it all fits together

The resulting architecture is not complex, but it must have all the parts:

  1. Invoice is written to the DB with an idempotency key.
  2. Background worker picks it up and sends it via the anti-corruption layer.
  3. Retry + rate limiting are in the Polly policy.
  4. UPO polling runs separately.
  5. Reconciliation job checks overall state.
  6. Every state transition generates a structured event in the audit log.
  7. Terminal failures and stuck states trigger an alert.

This is not over-engineering. It is the minimum architecture for a system that is asynchronous, rate-limited, and occasionally unavailable.

Proven in production

In production we process over 40,000 documents. During forensic recovery we retrieved 15,141 invoices that a previous fire-and-forget pipeline had silently lost. Each error category described above corresponds to a real incident from that recovery.

A KSeF integration that survives month-end without losing documents looks exactly like this.


We build KSeF integrations in .NET — from a simple API connection to a production-grade pipeline with durable state, reconciliation, and alerting. Get in touch — we will tell you exactly where your integration is vulnerable.


Frequently asked questions

Do we really need a reconciliation job, or is retry enough?

Retry handles transient errors during submission. Reconciliation addresses the state of the entire batch from a whole-system perspective — stuck records, orphans, errors that passed through retry but ended in a terminal state without an alert. One does not replace the other. Without reconciliation you will discover a lost invoice at an audit.

How big a problem is a shared rate limiter if we only have one instance?

With one instance, in-memory is sufficient. The problem comes the moment you add a second instance for high availability or load balancing — both processes then consume the quota independently and their combined total easily exceeds the KSeF limit. A shared limiter (Redis) is a safer starting point than migrating back after hitting 429 in the middle of the night.

What happens when KSeF rejects an invoice with a validation error and we resubmit it with the same idempotency key?

The idempotency key protects against duplicates on a successful submission. A rejected invoice must be corrected and resubmitted — with a new key, because the invoice content changed. The key should be derived from invoice data, so a corrected version automatically gets a different key.

Facing a similar problem? Get in touch.

Book a consultation