KSeF Kit

How filing works

A Stripe invoice can reach KSeF with no action from you. Here's the whole flow.

Who's the buyer? That's the first thing we resolve. A tax ID alone is not enough. We combine the country from the buyer's address, the tax ID type, and the net and VAT amounts. Only that set of data lets us classify the document into one of the scenarios below. If a rule cannot produce an unambiguous result, we do not guess and do not send the document to KSeF.

Pipeline

  1. invoice.finalized — Stripe sends a webhook when an invoice is finalized. We resolve the account from the connected Stripe account and store the invoice as an immutable source document. The payload is never mutated afterwards.
  2. FA(3) — we map the Stripe payload to a structured FA(3) invoice (the KSeF schema). Stripe doesn't carry everything: the seller address comes from your company profile, the buyer NIP from customer_tax_ids, the buyer address from the customer's billing address, and amounts are split into VAT buckets. We carry per-line detail, not just rate totals: each Stripe line becomes an FA(3) item with its name (from the line description), quantity, unit net price, net amount and VAT rate. When an invoice has more lines than Stripe fits into one event we ask Stripe for the full list, and a shipping cost gets a position of its own ("Koszt dostawy").
  3. KSeF online session — we open a session, encrypt the invoice (RSA-OAEP + AES-256-CBC, exactly as KSeF requires), send it, and poll for status.
  4. UPO — on acceptance KSeF returns a KSeF number and a signed UPO (official receipt). That is your legal proof of filing.
  5. Write-back to Stripe — we write the KSeF number back onto the invoice's metadata (machine-readable: ksef_number, plus ksef_verification_url for exports). Stripe locks every buyer-visible field (footer, description, custom_fields) once an invoice is finalized — and the KSeF number only exists after filing the finalized invoice — so the number cannot appear on the Stripe PDF or hosted page. The buyer-visible number and the UPO live in the panel; a domestic B2B buyer receives the invoice through KSeF itself.

How the automatic rules work

The rules are predetermined. Classification uses the data of the individual document, your account settings, and, for a correction, its linked original invoice. Currency conversion uses the public NBP rate, and the status after sending follows KSeF's response. We do not use an AI model, probability scoring, other customers' data, or the buyer's behavior history. The app does not infer missing data. Each rule results in filing, holding the document before filing, or leaving it outside KSeF.

What we check Rule What the app does
Stripe event A finalized invoice triggers invoice.finalized. A credit note triggers credit_note.created and can be processed only after its original invoice has been stored. We store the immutable source document and add it to the queue.
Account readiness We check the plan limit, company profile completeness, and active credentials for the selected KSeF environment. If any condition is not met, the document is held before any contact with KSeF.
Polish business The buyer country is PL, the identifier is a Polish NIP or an EU VAT number beginning with PL followed by 10 digits, and the invoice has one supported VAT rate: 23%, 8%, or 5%. We build a domestic B2B invoice with the buyer's NIP and net and VAT amounts. A 0% rate, exemption, multiple rates, or a buyer NIP equal to the seller NIP causes a hold.
Business in another EU country The buyer country is in the EU but is not Poland, and the identifier type is EU VAT. We classify the sale as B2B reverse charge and map the net amount to the corresponding FA(3) fields.
Business outside the EU The buyer country is outside the EU, it has a foreign tax ID, and Polish VAT is zero. We classify the sale as an export of services and map the net amount as outside the scope of Polish VAT. If Stripe reports VAT, we hold the document.
No matching classification The required business identifier for the buyer's country is missing, or the data indicates a consumer. We do not send the document to KSeF. We mark it as outside the scope of this integration, including EU consumer sales handled outside KSeF.
Document completeness An identified buyer must have an address. The line list must be complete, and the lines plus shipping must reconcile to the invoice net amount. A missing address, incomplete line list, or unexplained amount difference causes a hold. The document does not reach KSeF.
Foreign currency For an invoice carrying Polish VAT, we determine the applicable date and fetch the average NBP rate from the preceding business day. We record the VAT amount in PLN in FA(3), together with the rate and NBP table date used.
Correction We take the buyer and VAT treatment from the original invoice and the amounts from the credit note. The original invoice must already be stored and accepted by KSeF. We build a KOR correction referring to the original invoice's KSeF number. We currently hold a foreign-currency correction that carries Polish VAT.
Previous attempts We check whether the document was already accepted in the same KSeF environment or another attempt is in progress. We do not resend a document that was already accepted or is being processed concurrently.
KSeF response After sending, we poll KSeF using the stored session and document references. Acceptance stores the KSeF number and UPO. Rejection stores KSeF's message. A transient failure before sending triggers a retry; an uncertain result after sending causes us to keep checking the same attempt without resending.

The table below describes the exact tax mapping for the supported scenarios. Reasons for a hold and the next step are listed in Rejections & troubleshooting. Our Privacy Policy also describes the rules for automated processing of personal data.

What you see on screen

Every invoice has one plain status. That's all you need to know what's happening:

Status What it means
Queued Waiting to be sent. We'll file it shortly.
Filing Being sent to KSeF — seconds, not hours.
Filed Accepted. You have a KSeF number and a UPO — that's your proof.
Rejected KSeF didn't accept it. We show the reason and a concrete next step. The invoice waits — you can resubmit.
Held We deliberately did not send it, for example because the quota is used up, the company profile or credentials are incomplete, or the invoice data cannot be mapped unambiguously. Nothing is lost.

Rejected and Held are two different things. A rejection is KSeF's answer — something in the data or connection needs fixing. A hold is our deliberate decision not to send yet (e.g. over the free quota, or before you've filled in the NIP and address). In both cases the invoice waits safely.

Important — an identified buyer (one with a NIP, EU VAT, or foreign tax id) requires an address. If Stripe has no address for that customer, we hold the invoice rather than file an incomplete one. Add the customer's address in Stripe and refile. To prevent it entirely, require the billing address in Stripe up front — see Connecting KSeF. The fix is detailed in Rejections & troubleshooting.

Attempts are separate records

Every send attempt is its own record (Submission), not an overwrite. A retry creates a new row — so history is auditable and double-submission is impossible. Under the hood the attempt statuses are submitting, accepted, rejected, blocked — these add up to the visible invoice status above.

Filing is blocking

The library's #submit sleeps between status polls — it runs in a background job, never in a web request. If the process crashes after opening a session, #await resumes it from the stored references (idempotently — it never files twice).

Invoice scenarios we cover

KSeF is a B2B/B2G system: the buyer is a business or a public body. Who the buyer is decides the VAT treatment, and the treatment decides which FA(3) fields we emit. Here's what each scenario maps to and where it stands today.

Scenario Buyer VAT treatment FA(3) mapping Status
Domestic PL B2B Polish business (has a NIP) 23%, 8%, or 5% VAT Buyer NIP; net + VAT in a rated bucket (P_13_1/P_14_1 for 23%) Fully supported
EU B2B Business in another EU state (EU VAT no.) Reverse charge — the buyer accounts for VAT Buyer KodUE + NrVatUE; net-only bucket P_13_9; reverse-charge flag P_18 Supported
Non-EU business Business outside the EU (e.g. a PL sole-proprietor / JDG invoicing a US SaaS company) Export of services — not subject to PL VAT (NP), settled by the buyer Buyer KodKraju + NrID; net-only bucket P_13_8 Supported
B2C consumer A private individual (no business tax ID) Out of KSeF scope Not in KSeF

Every B2B scenario above is built end-to-end — the FA(3) mapping, the markings and the NBP FX. Before we enable the EU and non-EU VAT treatments in production, a Polish tax advisor signs off the exact VAT flags (P_18, the NP buckets) in one review — a correctness gate, not a gradual rollout. You can file all of them in test mode today.

B2C consumers

KSeF is for B2B and B2G. Sales to private consumers are voluntary in KSeF and effectively out of scope for this tool — issue them the way you do now (e.g. a receipt or a regular non-KSeF invoice). If a consumer later asks for an invoice with their NIP, that's a B2B invoice and goes through KSeF normally.

Foreign currency

EU and export invoices are usually in EUR or USD. The złoty amount KSeF needs is computed at the NBP average rate of the last business day before the tax point (for a continuous service — the billing-period end; for an invoice issued in advance — the issue date) — the legally required rate (art. 31a), never Stripe's settlement FX. See Currency & NBP.

This is not tax advice. Confirm the VAT treatment of your specific sales with your accountant.

Notifications

We email the owner about only two things worth interrupting them for: a rejection (you must act) and a filing that just exhausted the free quota (once, at the boundary). See Plans & billing.

When an invoice is rejected: Rejections & troubleshooting.