Poland Fiscalization Guide
What is Polish fiscalization?
Poland operates KSeF (Krajowy System e-Faktur — National e-Invoicing System), run by the Ministry of Finance. KSeF is mandatory for large taxpayers from 2026-02-01 and all VAT-registered taxpayers from 2026-04-01. Every B2B invoice (and B2C in opt-in mode) must be transmitted as FA(2) XML; KSeF returns a numerKSeF which becomes the canonical legal identifier.
In plain English: Poland is moving everyone onto a single national invoicing platform in 2026. KSeF accepts invoices, validates them, and assigns its own number. Zyntem keeps your issuer-side numbering intact (P_2) while also surfacing the KSeF number for legal references.
Zyntem builds the FA(2) XML, handles all four KSeF authentication modes (token, cloud QSCD, hardware key, qualified seal), submits to KSeF, polls the UPO (Urzędowe Poświadczenie Odbioru — official confirmation of receipt), and surfaces both the P_2 and numerKSeF on the transaction.
How it works
Poland's flow is asynchronous and dual-numbered:
- Your POS creates a transaction via
POST /v1/transactions. - Zyntem assembles the FA(2) XML (
http://crd.gov.pl/wzor/2023/06/29/12648/) including the issuer-sideP_2invoice number. - Zyntem authenticates per the location's
auth_method(see below) and submits to/api/online/Invoice/Send. - KSeF validates synchronously and queues the invoice; the transaction returns with
fiscal_id=P_2andfiscal_id_upstream=null. - The polling worker hits
/api/online/Invoice/Status/{id}until KSeF issues the UPO and assigns thenumerKSeF. Status flips tosuccessandfiscal_id_upstream=numerKSeF.
Submission window: KSeF mandates within-day submission for B2B (Article 106na of the Polish VAT Act). Zyntem queues immediately.
FA(2) → FA(3) cutover: FA(3) becomes effective 2026-10-01. Configurations using schema_version: FA3 reject prepare() until the FA(3) builder lands. Default to FA2 until the cutover date.
Authentication modes
KSeF supports four auth modes. Pick the one that matches your merchant's setup. The auth_method.kind discriminator selects the variant.
| Mode | Cloud-OK | Use when |
|---|---|---|
token | ✅ Recommended | KSeF-portal-issued bearer token. Simplest — generate the token from the merchant's KSeF portal session and Zyntem stores the KMS reference. |
xades_cloud_qscd | ✅ | Merchant has a cloud QSCD (SimplySign / mSzafir) and prefers XAdES-BES signing. |
xades_hardware_key | ❌ Embedded only | Hardware-bound qualified key (forces Embedded-Local mode — Cloud config is rejected). |
qualified_seal | ✅ | Legal-entity-bound qualified seal (eIDAS). |
Country config fields
The country_config object for Polish locations:
| Field | Type | Required | Description |
|---|---|---|---|
nip | string | Yes | Polish NIP (10 digits). |
auth_method | object | Yes | One of the four KSeF auth modes (see above). |
environment | string | Yes | test, demo, or production. |
schema_version | string | Yes | FA2 (current) or FA3 (post-2026-10-01). |
tax_office_code | string | Yes | 4-digit KodUrzedu from the Naglowek. |
place_of_issue | string | Yes | Place of issue printed on the invoice (P_1M). |
supplier_gln | string | No | Optional supplier GLN (used for SCM integrations). |
legal_name | string | No | Optional supplier legal name. |
vat_ue | string | No | Optional VAT-UE number (PL + 10 digits). |
Configuration example
curl -X POST https://api.zyntem.dev/v1/locations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-d '{
"name": "Warsaw Office",
"country": "PL",
"address": "ul. Marszałkowska 1, 00-001 Warszawa",
"country_config": {
"nip": "1234567890",
"auth_method": {
"kind": "token",
"token_kms_ref": "kms://pl/ksef-token"
},
"environment": "production",
"schema_version": "FA2",
"tax_office_code": "1010",
"place_of_issue": "Warszawa",
"legal_name": "Zyntem Przykład Sp. z o.o.",
"vat_ue": "PL1234567890"
}
}'
Creating a transaction
Example: A B2B invoice for PLN 1 230 to a Polish customer.
curl -X POST https://api.zyntem.dev/v1/transactions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-d '{
"location_id": "loc_abc123",
"type": "sale",
"amount": 123000,
"currency": "PLN",
"buyer": {
"name": "Kontrahent Sp. z o.o.",
"tax_id": "PL9876543210",
"country": "PL"
},
"line_items": [
{
"description": "Usługa konsultingowa",
"quantity": 1,
"unit_price": 100000,
"vat_rate": 2300
}
]
}'
Response
{
"id": "txn_abc123",
"status": "pending_submission",
"fiscal_id": "FV/2026/03/00042",
"fiscal_id_upstream": null,
"country": "PL",
"created_at": "2026-03-15T14:30:22Z"
}
The fiscal_id is the issuer-side P_2 invoice number that you (or your numbering policy) assigned. Once KSeF issues the UPO, fiscal_id_upstream populates with the numerKSeF:
fiscal_id_upstream: 1234567890-20260315-A1B2C3D4E5F6-7G
(35-character format: <NIP>-<YYYYMMDD>-<12 hex>-<2 hex>)
Sandbox access
KSeF has two pre-production rings:
- Test (
ksef-test.mf.gov.pl) — open onboarding, lower fidelity. - Demo (
ksef-demo.mf.gov.pl) — closer to production in throttling and UPO behaviour.
Sandbox routing is automatic with a test API key. Tokens for test/demo are obtained from the corresponding KSeF portal — typical onboarding is 1 day for test, 2–3 days for demo.
Error handling
When KSeF rejects an invoice, error.category distinguishes:
transient— network blip, KSeF 5xx, throttling. Zyntem retries automatically.regulatory_reject— KSeF business validation failed.error.authority_codecarries theprocessingCode(40xxxseries) or schema-validation code. Correct the invoice and re-issue under a newP_2.permanent— auth-method mismatch (e.g.xades_hardware_keyconfigured for Cloud), sealed-config error, or FA(3) attempted before the cutover. Surface to operator.duplicate_as_success— KSeF rejected as a repeated submission of an already-accepted invoice. Treated as success.
See the Error handling guide for the full taxonomy.
Receipt presentation
render_hints on the transaction response tells your POS what to print.
key | When present | Placement | Mandatory | Kind |
|---|---|---|---|---|
numer_ksef | Online (after /Invoice/Status returns numerKSeF) | footer_bottom | Optional | text |
KSeF FA(2) does not mandate printing numerKSeF on the receipt; the
issuer's invoice number (P_2) carries the legal identity. The
runtime appends numer_ksef to render_hints once the polling loop
receives it, but printing it is at the merchant's discretion. No QR
is mandated.