KSeF FA(3) Validation Errors: Developer Troubleshooting
Diagnose KSeF FA(3) validation errors by separating XML, semantic, duplicate, authentication, and transport failures, then retrying safely in production.
Ernest Bursa
A KSeF FA(3) error is not one kind of error. It can be an HTTP failure, a synchronous API exception, an asynchronous invoice status, a session status, or an authentication status. To diagnose it, read three values together: the operation, the status namespace, and the code. Repair deterministic failures, reconcile duplicates and ambiguous submissions, and retry only the failures that are truly transient.
This guide covers the production KSeF API 2.6.1 contract available on 28 August 2026. TEST already exposes 2.7.1, so check the live production OpenAPI and your runtime limits before relying on any table as permanent. This is an engineering guide, not tax or legal advice.
What counts as a KSeF FA(3) validation error?
KSeF acceptance is a pipeline, not a single XSD gate. An online invoice can pass through six distinct stages:
- Your client serializes the business invoice as FA(3) XML.
- It hashes and encrypts the exact bytes that will be sent.
- KSeF accepts or rejects the API request synchronously.
- An accepted request enters asynchronous invoice processing.
- KSeF verifies the file, encryption, permissions, duplicate identity, and selected semantic rules.
- A successful invoice receives a KSeF number and becomes eligible for a UPO.
The first useful distinction is between transport success and invoice success. POST /sessions/online/{referenceNumber}/invoices returns HTTP 202 Accepted with an invoice reference. That only means processing started. It does not mean the XML passed validation, the invoice received a KSeF number, or a UPO exists.
Persist the returned invoice reference before doing anything else. Poll the invoice result, and treat the session result as a separate signal. The production OpenAPI even shows a session with status 200 containing ten invoices: eight succeeded and two failed. A successful session does not prove that every invoice inside it succeeded.
The reverse mistake is just as common: HTTP 200 from a status endpoint means the status request worked, not that the invoice succeeded. The response body can still contain processing status 440, 450, or another failure.
Why is a KSeF code meaningless without its context?
Suppose a log line says only KSeF error 440. You still do not know what happened.
- Invoice status
440means duplicate invoice. - Session status
440means the session was canceled, for example after a timeout or because it contained no invoices. - Invoice status
450means semantic validation failure. - Authentication status
450means an invalid token.
Those values are not a global error-number registry. They belong to response models. A useful diagnostic record needs the context around the number.
| Field to retain | Why it matters |
|---|---|
| Environment and API version | TEST and production can run different contracts |
| Operation or endpoint | Identifies which status namespace applies |
| HTTP status | Separates request/transport handling from processing outcome |
| Exception or processing code | Classifies the failure inside that namespace |
Description, details, and extensions
|
Carries the server’s actionable diagnostics and original references |
| Session and invoice references | Lets you resume polling and reconcile uncertain outcomes |
| Exact XML hash and size | Connects a response to the bytes you intended to submit |
Assign that record one internal correlation ID, then attach it to every retry and poll. Your dashboard can group failures without discarding the evidence needed to reproduce a specific one.
Keep synchronous request exceptions separate from asynchronous invoice statuses, too. For online send, current HTTP 400 exceptions include invalid session state (21180), size mismatch (21402), hash mismatch (21403), and request validation (21405). If you request X-Error-Format: problem-details, supported request errors can use structured Problem Details. None of these codes belongs in an invoice-status chart.
Which invoice statuses mean wait, repair, reconcile, or retry?
Each invoice status should lead to one of four responses: wait, repair, reconcile, or retry. Use the status from the per-invoice endpoint, not the HTTP status of that GET request.
| Invoice status | Meaning in production 2.6.1 | Default action |
|---|---|---|
100, 150
|
Accepted for further processing / processing | Wait and poll again with backoff |
200 |
Successfully processed | Record the KSeF number; retrieve and store the UPO |
405 |
Canceled because of a session error | Inspect the session failure first |
410 |
Invalid permission scope | Repair authorization; do not change XML blindly |
415 |
Attachment invoice cannot be sent | Repair attachment entitlement or invoice form |
430 |
Invoice-file verification failed | Check bytes, XML, schema, limits, hash, and related file rules |
435 |
Decryption failed | Repair key and encryption handling |
440 |
Duplicate invoice | Reconcile with the original session and KSeF number |
450 |
Semantic validation failed | Repair invoice data using returned details |
500 |
Unknown internal status | Preserve diagnostics and reconcile before bounded recovery |
550 |
Processing canceled internally | Reconcile, then retry with a bounded policy |
Treat this table as a routing layer, not a replacement for the response. Preserve the raw description, all details, and any extensions. The Ministry does not publish a stable, exhaustive catalog mapping every possible 430 or 450 detail to an XPath. Handle new and unknown states defensively instead of building a brittle parser around today’s descriptions.
Session status still matters, but for a different reason. An archive, decryption, timeout, or package error at session level can cancel its invoices. Once the session is processed, inspect its success and failure counts, then check each invoice state. For a mixed batch, the failed-invoices endpoint is the fastest route to diagnostics.
How should you preflight FA(3) XML before upload?
Since 1 February 2026, FA(3) is the only structured-invoice schema accepted for new submissions, including corrections to invoices originally issued under FA(1) or FA(2). Open the session with systemCode: "FA (3)", schemaVersion: "1-0E", and value: "FA", and validate against the authoritative FA(3) XSD.
Validate the exact bytes you will hash and encrypt. The Ministry’s invoice verification guide requires XML 1.0, UTF-8 without a byte-order mark, the schema declared when the session was opened, no conflicting encoding declaration, no processing instructions, and no specified discouraged Unicode ranges. Once you include an optional structure, its required child fields still become mandatory.
For a Rails integration using Nokogiri, a local preflight can begin like this:
schema = Nokogiri::XML::Schema(File.read("schemat_FA(3)_v1-0E.xsd"))
bytes = File.binread("invoice.xml")
raise "UTF-8 BOM is not allowed" if bytes.start_with?("\xEF\xBB\xBF".b)
document = Nokogiri::XML(bytes) { |config| config.strict.nonet }
errors = schema.validate(document)
raise errors.map(&:message).join("\n") if errors.any?
This catches malformed XML and XSD violations. It does not reproduce every server-side check. Add application checks for at least:
- the seller identity and the invoice number assigned by your numbering system;
- dates, especially that
P_1is not later than KSeF acceptance; - conditional FA(3) structures and business arithmetic;
- file-size and session-count limits;
- attachment entitlement when applicable;
- the plaintext and encrypted byte sizes and SHA-256 hashes sent in metadata;
- encryption with the current KSeF public key and the documented algorithms.
Keep your business-rule validation even when KSeF returns 200. The Ministry’s KSeF questions and answers says the system can accept an invoice with arithmetic errors or an incorrect but checksum-valid counterparty NIP. Server acceptance proves that KSeF accepted the structured invoice, not that the accounting data was correct.
TEST, DEMO, and production also prove different things. TEST is anonymized and has no legal effect. DEMO uses real authentication but still has no legal effect. Production does. Some checks, including selected NIP checksum checks, apply only in production. Passing in TEST shows that your integration works in TEST; it does not guarantee a production pass.
How do you diagnose status 450 without guessing?
Treat 450 as a finding about invoice semantics, then preserve enough input to reproduce it exactly. Do not edit fields at random until the error disappears.
- Save the full status object, including every detail returned by KSeF.
- Locate the immutable source snapshot used to build the invoice.
- Match its stored XML hash to the bytes submitted under the invoice reference.
- Re-run local XSD and business validators against that snapshot.
- Map the returned detail to the FA(3) field and the source-system value that produced it.
- Fix the source or mapper, generate new XML, and validate the new bytes from the beginning.
A rejected XML was not issued. The Ministry says to repair it and submit a valid XML; this is not a correction of an accepted invoice. That distinction matters when you design retries. The repair creates a new submission attempt, but its business identity and audit trail must still link back to the failed one.
If the same payload passes TEST but fails production, investigate environment-specific authorization, entitlement, identity, and validation differences before weakening a local validator. Never send a throwaway invoice to production to see what happens: a successful result has legal effect.
Why is duplicate status 440 a reconciliation event?
KSeF identifies a duplicate by three business fields: seller NIP (Podmiot1:NIP), invoice type (RodzajFaktury), and invoice number (P_2). The documented uniqueness period lasts for ten full years after the end of the year in which the invoice was issued.
Status 440 does not prove that the XML bytes are identical. It means KSeF has already accepted an invoice with that business identity. The status may include originalSessionReferenceNumber and originalKsefNumber; use them.
The recovery path is:
- Look up the original KSeF number and session reference from the status extensions.
- Compare the original invoice with the intended source transaction.
- Retrieve and verify the original UPO.
- Mark the local attempt as reconciled to that accepted invoice.
- Escalate if the accepted invoice does not represent the intended business transaction.
Do not increment P_2 merely to make the error disappear. If the original request succeeded but its response was lost, changing the number can create a second legally effective invoice. Separate business numbering from transport attempts: one invoice can have several attempt records, but a retry should not quietly invent a new business document.
This exposes an important race condition. If separate teams or issuing units share one seller NIP, they must coordinate invoice numbering. Uniqueness within each application is not enough for KSeF’s global duplicate key.
Which KSeF failures are safe to retry?
A safe retry policy starts by identifying the failure layer.
Do not retry deterministic input failures unchanged. XML/XSD failures, size or hash mismatches, permission problems, attachment entitlement failures, decryption failures, and semantic status 450 need repair. Sending the same bytes through the same conditions will create noise, consume limits, and preserve no new information.
Do not blind-retry duplicate status 440. Reconcile it to the original accepted invoice.
For HTTP 429, wait for the full Retry-After. KSeF rate limits use overlapping one-second, one-minute, and one-hour windows. Repeated calls during a block may lengthen it. Coordinate workers that share the same authentication context and IP, add jitter before releasing queued work, and inspect GET /rate-limits at runtime instead of assuming that published defaults apply to your account.
For timeouts and HTTP 5xx, the outcome may be ambiguous. A response can disappear after the server commits the request. Production OpenAPI documents no client-supplied idempotency key for online send or batch close. The following is therefore an engineering recommendation, not a KSeF guarantee:
- persist session reference, invoice hash, size, and attempt timestamp before sending;
- after an ambiguous online send, inspect the same session and reconcile its invoice list before replaying;
- after an ambiguous batch close, query that session and close it again only if it remains open;
- use bounded exponential backoff with jitter after reconciliation finds no committed result;
- stop at a retry budget and send the attempt to human review with its evidence intact.
Invoice status 550 explicitly says to try again, but “retryable” does not mean “retry forever.” Preserve the diagnostics, reconcile the attempt, and apply the same bounded recovery policy. For status 500, do not confuse a business processing code with HTTP 500; retain the namespace and investigate before deciding.
Polling requires the same restraint. Continue for 100 and 150, back off between polls, and stop at a terminal state. A fixed one-second loop in a sample client is not an official processing SLA.
What evidence should a production integration retain?
When an invoice succeeds, the evidence amounts to more than a green status in a dashboard. Keep a durable record that connects the business transaction to what KSeF accepted:
- immutable source snapshot and mapper/schema version;
- exact plaintext XML hash and byte size;
- encryption metadata and encrypted hash/size;
- environment and API version observed;
- session and invoice reference numbers;
- timestamped status history with descriptions, details, and extensions;
- KSeF invoice number;
- UPO XML and its SHA-256/Base64 integrity value;
- links among the business invoice, every submission attempt, and the accepted result.
An invoice UPO becomes available only after that invoice succeeds, and you can retrieve it while the session remains open. Aggregate session UPOs appear after closure and cover only the accepted subset. A mixed-success session can therefore have both a UPO and failed invoices. Do not use “session has UPO” as shorthand for “all invoices succeeded.”
KSeF may expose a temporary UPO download URL in a status response. It expires and is not the durable artifact. Download the signed XML, verify the x-ms-meta-hash value returned by the authenticated endpoint, and retain the artifact according to your evidence policy.
How KSeF Kit handles the submission lifecycle
KSeF Kit is a separate product for teams filing Stripe invoices to Poland’s KSeF. Its documented filing workflow follows the same boundaries this guide recommends: it snapshots source data, maps it to FA(3), submits through an encrypted online session, polls the result, records separate submission attempts, resumes waiting from saved references, stores the UPO, and writes the KSeF number back to Stripe.
That does not remove the need to understand a rejection. It gives each rejection a durable place in a stateful workflow instead of burying it inside a failed HTTP request. Teams building their own integration can follow the same design: immutable inputs, explicit attempts, resumable references, operation-aware status handling, and reconciliation before replay.
If Stripe is your invoice source and you would rather operate this workflow than build it, review how KSeF Kit connects environments and its troubleshooting reference. Either way, the production rule is the same: a code without its operation is not a diagnosis, and a retry without reconciliation is not a recovery plan.
Related articles
Ready to hire smarter?
Start free for 30 days. Cancel before it ends and you pay nothing. Set up your first hiring pipeline in minutes.
Start hiring free