Krok po kroku — od certyfikatu do pierwszej faktury w Krajowym Systemie e-Faktur
Cześć! Na samym wstępie muszę się przyznać — jestem wieloletnim i oddanym fanem systemu LMS :D Poniższy projekt powstawał momentami w bólach, prawdopodobnie bywa pełen chaosu i z pewnością nie wygra konkursu na najlepiej udokumentowany kod w historii. Początkowo tworzyłem to wyłącznie na swoje własne, absolutnie prywatne potrzeby, żeby jakoś okiełznać nadchodzącego potwora zwanego KSeF. Nie ukrywam! W dużej mierze pomogła tutaj sztuczna inteligencja...
Jednak, ulegając presji tłumu i na prośbę wielu z Was, postanowiłem podzielić się tym dziełem zupełnie nieodpłatnie. Musicie jednak wiedzieć o jednym: nie działam w porozumieniu z autorami ani programistami oficjalnego LMS-a. Oznacza to, że wszelkie ich poprawki i aktualizacje mogą znacząco wpłynąć na (nie)działanie niniejszej integracji. Mój projekt nie jest oficjalną częścią systemu. Oczywiście bardzo się starałem, by całość była jak najbardziej spójna i niczego nie zepsuła, ale korzystacie z tego na własną odpowiedzialność! Oczywiście udostępniony kod może być także inspiracją dla innych, śmiało korzystacjie! Zachęcam również do dzielenia się :P
| Składnik | Minimalna wersja | Sprawdź |
|---|---|---|
| LMS | 28-git, migracje ≥ 2026040800 | SELECT keyvalue FROM dbinfo WHERE keytype='dbversion' |
| PHP | 8.1+ | php -v |
| Composer | dowolna | composer --version |
| Biblioteka KSeF | n1ebieski/ksef-php-client | ls vendor/n1ebieski/ |
| Certyfikat KSeF | Plik .p12 tworzony z plików .crt, .key oraz hasła wygenerowanych w portalu KSeF | — |
| ext-zip | wymagane przez PHP | php -m | grep zip |
Jeśli brak biblioteki ksef-php-client:
cd /twoja/sciezka/lms
composer require n1ebieski/ksef-php-client
Aby uwierzytelnić się w systemie KSeF, potrzebujesz certyfikatu. W portalu KSeF Ministerstwa Finansów (Aplikacja Podatnika → Certyfikaty) generujesz parę plików o rozszerzeniach .crt oraz .key, a także podajesz dla nich hasło, które musisz zapamiętać i wpisać do pliku lms.ini. Z tych danych należy wygenerować docelowy plik .p12 (PKCS#12) przy użyciu poniższego polecenia:
# Połącz .crt i .key w plik .p12 openssl pkcs12 -export \ -in certyfikat.crt \ -inkey certyfikat.key \ -out /etc/lms/ksef_cert.p12 # lub inna wybrana ścieżka \ -passout pass:TwojeHaslo123! # Zabezpiecz plik chmod 640 /etc/lms/ksef_cert.p12 chown root:www-data /etc/lms/ksef_cert.p12
/etc/lms/ksef_cert.p12 z uprawnieniami 640.
Ministerstwo Finansów może ogłosić awaryjny tryb offline KSeF — wtedy faktury wystawia się bez połączenia z systemem MF. W takim przypadku na każdej fakturze PDF pojawia się specjalny kod QR podpisany kluczem ECDSA, który można zweryfikować bez dostępu do internetu.
Do obsługi trybu offline potrzebny jest oddzielny certyfikat offline — generujesz go tak samo jak certyfikat do logowania, ale w portalu KSeF wybierasz przeznaczenie „Offline". Wygenerowane pliki .crt i .key łączysz w jeden plik .pem:
# Certyfikat offline musi być ECDSA (EC P-256) — nie RSA cat ksef_offline.key ksef_offline.crt > /etc/lms/ksef_offline.pem chmod 640 /etc/lms/ksef_offline.pem # Weryfikacja że to EC (a nie RSA): openssl pkey -in /etc/lms/ksef_offline.pem -text -noout | grep "EC\|prime256" # Oczekiwany wynik: id-ecPublicKey, prime256v1
Po wygenerowaniu pliku dodaj do lms.ini w sekcji [ksef]:
offline_certificate = /etc/lms/ksef_offline.pem offline_password = # hasło do pliku .pem (zazwyczaj puste) offline_support = 1 # zezwól na wysyłkę e-mail faktur bez numeru KSeF
Edytuj swój plik lms.ini (zazwyczaj /etc/lms/lms.ini lub inna ścieżka podana przy instalacji LMS) i dodaj / uzupełnij poniższe sekcje. Pamiętaj, by we wszystkich komendach zamienić /sciezka/do/lms.ini na własną ścieżkę:
[phpui] plugins = KSeFSubmit # dodaj do istniejącej listy pluginów [ksef] environment = prod # prod / test / demo auth_method = certificate # certificate (zalecane) lub token certificate = /etc/lms/ksef_cert.p12 password = "TwojeHaslo123!" boundary_date = 2026/04/01 # data od której wysyłamy faktury encryption_key = Wpisz32ZnakowyLosowyKluczAES256! encryption_iv = 16ZnakowIV1234 debug = 0 # 1 = loguj plugin do PHP error_log, 0 = cisza [invoices] issuer = 'Dział obsługi rozliczeń' ; podpis wystawcy na fakturze PDF show_balance_summary = 0 ; 0 = nie pokazuj salda klienta na fakturze PDF (zalecane dla ISP) show_pricing_method = 0 info_box_text = '<center><b>Informacja</b>\nFaktura wystawiona w KSeF.\nDokument w KSeF stanowi fakturę właściwą.\nPDF ma charakter informacyjny.</center>' ; ↑ ramka na fakturach B2B — obsługuje HTML, \n = nowa linia info_box_text_b2c = '<center>Dziękujemy za korzystanie z naszych usług</center>' ; ↑ ramka na fakturach B2C — jeśli brak: używana info_box_text [sendinvoices] ; faktury B2B — firmy, klienci z NIPem smtp_host = poczta.twojadomena.pl ; serwer SMTP smtp_port = 587 ; 587=STARTTLS, 465=SSL, 25=plain smtp_auth = PLAIN ; PLAIN lub LOGIN smtp_user = nadawca@twojadomena.pl ; login SMTP smtp_pass = "HasloDoSMTP!" ; haslo SMTP smtp_ssl_verify_peer = 0 ; 0 = nie weryfikuj certyfikatu SSL (webstorage itp.) smtp_ssl_verify_peer_name = 0 smtp_ssl_allow_self_signed = 0 lms_url = https://adres.twojego.lms/ ; URL do LMS (do linkow w mailach) lms_user = uzytkownik_lms ; uzytkownik LMS do generowania PDF lms_password = "HasloUzytkownikaLMS!" customergroups = nazwa-grupy ; grupy klientow do wysylki (spacja = separator) sender_name = Nazwa Twojej Firmy sender_email = biuro@twojadomena.pl invoice_filename = Faktura_%number ; nazwa pliku PDF w zalacznik mail_subject = Faktura VAT %invoice mail_body = /etc/lms/mail_body_b2b.txt mail_format = html ; debug_email = twoj@email.pl ; jesli ustawione — mail idzie TYLKO na ten adres (do testow) [sendinvoices-b2c] ; faktury B2C — osoby fizyczne smtp_host = poczta.twojadomena.pl smtp_port = 587 smtp_auth = PLAIN smtp_user = nadawca@twojadomena.pl smtp_pass = "HasloDoSMTP!" smtp_ssl_verify_peer = 0 smtp_ssl_verify_peer_name = 0 smtp_ssl_allow_self_signed = 0 lms_url = https://adres.twojego.lms/ lms_user = uzytkownik_lms lms_password = "HasloUzytkownikaLMS!" customergroups = nazwa-grupy sender_name = Nazwa Twojej Firmy sender_email = biuro@twojadomena.pl invoice_filename = Faktura_%number mail_subject = Faktura %invoice mail_body = /etc/lms/mail_body_b2c.txt mail_format = html
Pliki wskazane w mail_body to szablony HTML. Zmienne dostępne w treści:
| Zmienna | Znaczenie |
|---|---|
%invoice | Numer faktury (np. FV/001/04/2026) |
%ksef-number | Numer KSeF (np. 8441240447-20260401-...) |
%gross | Kwota brutto faktury |
%bankaccount | Numer konta bankowego |
%paytime | Termin płatności |
%customerid | ID klienta w LMS |
/etc/lms/mail_body_b2b.txt — dla firm (B2B):
Szanowni Państwo,<br><br> informujemy, że została wystawiona faktura VAT nr <b>%invoice</b>.<br><br> <b>Numer KSeF: </b> %ksef-number<br> Faktura została wystawiona w KSeF.<br> Dokument dostępny w KSeF stanowi fakturę właściwą.<br><br> Załączony plik PDF ma charakter poglądowy.<br><br> W razie pytań prosimy o kontakt.<br><br> Z poważaniem,<br> <b>Twoja Firma</b>
/etc/lms/mail_body_b2c.txt — dla osób fizycznych (B2C):
Szanowny Kliencie,<br><br> w załączniku przesyłamy fakturę nr <b>%invoice</b>.<br><br> Dziękujemy za korzystanie z naszych usług.<br><br> Pozdrawiamy,<br> <b>Twoja Firma</b>
info_box_text i info_box_text_b2c w sekcji [invoices] to nasza modyfikacja LMSTcpdfInvoice.php — nie ma ich w standardowym LMS.
Ramka pojawia się na fakturach PDF wystawionych po dacie granicznej KSeF (boundary_date). Dla faktur starszych — nie wyświetla się.
Obsługuje HTML: <b>, <center>, <i> oraz \n jako nową linię.
Jeśli ustawiona tylko info_box_text — pojawia się dla wszystkich klientów.
Jeśli ustawione oba — info_box_text dla B2B (firmy), info_box_text_b2c dla B2C (osoby fizyczne).
# encryption_key — 32 znaki losowe openssl rand -base64 24 | tr -d '=+/' | head -c 32 # encryption_iv — 16 znaków losowych openssl rand -base64 12 | tr -d '=+/' | head -c 16
Paczka zawiera skrypt lms-npi.sh (NETLink Patch Installer) który zrobi wszystko automatycznie: skopiuje pliki, naniesie poprawki i zaktualizuje bazę danych. To jest zalecany sposób instalacji.
# 1. Wgraj paczkę na serwer i rozpakuj unzip lms-ksef-netlink-*.zip -d /tmp/ksef-pack # 2. Dry-run — podgląd co zrobi instalator (nic nie zmienia) bash /tmp/ksef-pack/lms-npi.sh -c /etc/lms/lms.ini --dry-run # 3. Właściwa instalacja bash /tmp/ksef-pack/lms-npi.sh -c /etc/lms/lms.ini
lms.ini/tmp/lms-backup-TIMESTAMP/lms-dir.tar.gz/tmp/lms-backup-TIMESTAMP/lms-db.sql.gz (MySQL i PostgreSQL)plik.baklib/KSeF/KSeF.php (separator dziesiętny)bash lms-npi.sh -c /etc/lms/lms.ini # instalacja (z backupem) bash lms-npi.sh -c /etc/lms/lms.ini --dry-run # tylko podgląd, nic nie zmienia bash lms-npi.sh -c /etc/lms/lms.ini --no-backup # bez backupu katalogu i bazy bash lms-npi.sh -c /etc/lms/lms.ini --no-backup-dir # bez backupu katalogu LMS bash lms-npi.sh -c /etc/lms/lms.ini --no-backup-db # bez backupu bazy danych
Jeśli wolisz kopiować pliki samodzielnie, oto pełna lista:
PACK=/tmp/ksef-pack # katalog z rozpakowaną paczką
LMS=/twoja/sciezka/lms # katalog instalacji LMS
cp $PACK/lib/KSeF/KSeF.php $LMS/lib/KSeF/KSeF.php
cp $PACK/lib/LMSDocuments/LMSTcpdfInvoice.php $LMS/lib/LMSDocuments/LMSTcpdfInvoice.php
mkdir -p $LMS/plugins/KSeFSubmit/handlers $LMS/plugins/KSeFSubmit/lib
cp $PACK/plugins/KSeFSubmit/KSeFSubmit.php $LMS/plugins/KSeFSubmit/
cp $PACK/plugins/KSeFSubmit/handlers/KSeFSubmitHandler.php $LMS/plugins/KSeFSubmit/handlers/
cp $PACK/plugins/KSeFSubmit/lib/KSeFApiService.php $LMS/plugins/KSeFSubmit/lib/
cp $PACK/bin/lms-ksef.php $LMS/bin/
cp $PACK/bin/lms-ksef-download.php $LMS/bin/
cp $PACK/bin/lms-ksef-sync.php $LMS/bin/
cp $PACK/bin/lms-ksef-bulk-correct.php $LMS/bin/
chmod +x $LMS/bin/lms-ksef*.php
lms-ksef.php?Zasada działania: Skrypt szuka w bazie wszystkich faktur bez numeru KSeF wystawionych od boundary_date (ustawiane w lms.ini). Nie wie nic o miesiącach ani datach uruchomienia — patrzy tylko na tabelę ksefdocuments i pyta: "czy ta faktura ma już numer KSeF?". Jeśli nie — wysyła.
Zawsze najpierw sprawdza czy są otwarte sesje batch (status 0 lub 100) i pobiera dla nich numery KSeF od MF. To jest ważne — jeśli poprzednie uruchomienie wysłało faktury ale nie zdążyło odebrać numerów (np. skrypt się wysypał), ta faza je nadrobi.
Zapytanie SQL szuka faktur gdzie:
d.cdate >= boundary_date (np. 2026-04-01)d.type IN (DOC_INVOICE, DOC_CNOTE, DOC_DNOTE)kd.id IS NULL — brak rekordu w ksefdocuments = jeszcze nie wysłanaPotem filtruje B2B/B2C — firmy zawsze, osoby fizyczne tylko gdy mają zgodę.
Wystawiasz faktury 1. dnia, uruchamiasz skrypt 5. dnia tego miesiąca:
Działa dokładnie tak samo jak gdybyś uruchomił 1. dnia. Skrypt znajdzie wszystkie faktury z kd.id IS NULL — czyli wszystkie które w międzyczasie nie zostały wysłane — i wyśle je w jednej sesji batch. Nic się nie traci.
Nie uruchamiasz 2 miesiące:
Skrypt znajdzie faktury z obu miesięcy naraz i wyśle je wszystkie w jednym przebiegu. Przykład: masz 200 faktur za marzec i 200 za kwiecień → skrypt wyśle 400 faktur w jednej lub kilku paczkach ZIP (max 50MB per paczka). Jedyne ograniczenie: MF ma limit sesji batch — ale przy 400 fakturach ISP to żaden problem.
Co jeśli skrypt uruchomisz dwa razy z rzędu:
Drugie uruchomienie nic nie zrobi. Faktury już mają rekordy w ksefdocuments → warunek kd.id IS NULL zwróci pustą listę → skrypt zakończy się komunikatem "Brak nowych faktur".
Co jeśli MF odrzuci fakturę (status 400):
Faktura trafia do ksefdocuments ze statusem 400. Kolejne normalne uruchomienie jej nie ruszy — bo kd.id IS NULL już nie jest prawdą. Żeby ponownie wysłać — trzeba użyć flagi: --retry-errors:
./lms-ksef.php -C /sciezka/do/lms.ini --retry-errors
Co jeśli skrypt padnie w połowie wysyłki:
FAZA 1 przy następnym uruchomieniu sprawdzi otwarte sesje i odbierze numery KSeF dla tego co zdążyło dotrzeć do MF. Faktury które nie zostały w ogóle wysłane — mają nadal kd.id IS NULL i wejdą w kolejnym uruchomieniu.
--status-only (w cronie co godzinę):
Wykonuje tylko FAZĘ 1 — sprawdza otwarte sesje i odbiera numery KSeF. Nie wysyła nic nowego. Używaj tego w cronie między 1. a ostatnim dniem miesiąca, żeby faktury dostały numery KSeF szybciej niż czekając na kolejne miesięczne uruchomienie.
Podsumowanie jednym zdaniem:
Skrypt jest idempotentny i bezpieczny do wielokrotnego uruchamiania — zawsze wysyła tylko to czego jeszcze nie ma w KSeF, nigdy dwa razy to samo.
lms-ksef-download.phpCo robi: pobiera faktury które dostawcy wystawili NA Twój NIP i wrzucili do KSeF. To są Twoje faktury zakupowe — Shell, Orange, Allegro itp.
Opcje uruchomienia:
--from=2026/04/01 → od konkretnej daty--days=7 → ostatnie N dni--full-sync → od boundary_date do dziś (pierwsze uruchomienie)Jak działa krok po kroku:
POST /invoices/query/metadata) z paginacją po 100 sztuk — powtarza dopóki hasMore = true.ksef_number już jest w bazie — jeśli tak, pomija.ksefinvoices i plik XML na dysk.fillMissingXml() — dla faktur które dostały 429 przy pierwszym pobraniu i nie mają XML, próbuje jeszcze raz.Idempotentny? Tak — sprawdza ksef_number w bazie, nigdy nie zapisze dwa razy tej samej faktury.
Przy 429: czyta Retry-After z odpowiedzi MF, czeka tyle ile MF każe + bufor, zwiększa globalny odstęp do minimum 5s na kolejne zapytania.
lms-ksef-sync.phpCo robi: to samo co download.php ale innym mechanizmem — przez Export API. Zamiast pobierać faktury jedna po drugiej (64 zapytania/h), prosi MF o spakowanie wszystkiego do jednego zaszyfrowanego ZIPa.
Kiedy używać: jednorazowo — do nadrobienia zaległości historycznych. Do crona codziennego używaj lms-ksef-download.php.
Jak działa krok po kroku:
POST /invoices/exports → MF zaczyna pakować faktury asynchronicznie.GET /invoices/exports/{ref}) aż MF powie że gotowe (status 200)..zip.aes.lms.ini (encryption_key, encryption_iv)._metadata.json (pomijany).lms-ksef-download.php.Zaleta: brak throttlingu — zamiast setek pojedynczych zapytań MF pakuje wszystko do jednego zaszyfrowanego ZIPa (AES-256-CBC). Skrypt odszyfrowuje go kluczami z lms.ini (encryption_key + encryption_iv) i przetwarza wszystkie faktury lokalnie.
Aktywacja w lms.ini: plugin wymaga wpisu w sekcji [phpui]:
[phpui] plugins = KSeFSubmit ; dodaj do istniejącej listy pluginów
Aktywacja w GUI LMS: po wpisaniu do lms.ini przejdź do
Administracja → Konfiguracja → KSeF — pojawi się zakładka konfiguracji pluginu.
Poniżej widok przycisku wysyłki na fakturze:

Co robi: wysyła fakturę do KSeF natychmiast po zapisaniu jej w GUI — bez czekania na cron 1. dnia miesiąca. Klikasz "Zapisz fakturę" → w ciągu kilku sekund masz numer KSeF.
Jak jest zbudowany:
KSeFSubmit.php — rejestruje plugin i hookiKSeFSubmitHandler.php — łapie zdarzenia z GUIKSeFApiService.php — komunikacja z APIKtóre zdarzenia łapie: invoicenew_save_after_submit (nowa faktura), invoiceedit_save_after_submit (edycja faktury), invoicenote_save_after_submit (faktura korygująca).
Jak działa krok po kroku:
boundary_date? Czy to nie proforma? Czy klient B2B albo ma zgodę B2C? Czy KSeF jest dostępny (Latarnia)?DB->UnLockTables() (faktura już zapisana, unlock bezpieczny).ksefdocuments).GetInvoiceContent().KSeF::getInvoiceXml().sessions()->batch()->openAndSend().sessions()->batch()->close() — wymagane żeby MF zaczął przetwarzać fakturę.ksefbatchsessions i fakturę do ksefdocuments (status 0 = oczekuje).sessions()->status().Zarówno plugin jak i lms-ksef.php używają Batch API — plugin wysyła jedną fakturę na sesję, CLI może wysłać wiele faktur naraz.
Co jeśli plugin nie zdąży w 45s (MF wolno przetwarza): faktura zostaje w ksefdocuments ze statusem 0. FAZA 1 w lms-ksef.php --status-only odbierze numer i UPO przy kolejnym uruchomieniu crona.
Plik lib/KSeF/KSeF.php jest dostarczany w paczce z nałożoną poprawką P1. Przy każdej aktualizacji LMS przez git pull plik jest nadpisywany — dlatego w paczce jest skrypt bin/patch-netlink.py, który automatycznie nanosi poprawkę ponownie. Instalator uruchamia go automatycznie jako KROK 4.
LMS przestawia separator dziesiętny na przecinek (locale pl_PL). KSeF wymaga kropki w XML — bez tej poprawki pola P_13_*, P_14_*, P_15 (podstawy VAT, kwoty VAT, suma) generowane przez sprintf('%.2f', ...) zawierają przecinki i MF odrzuca fakturę.
smartFormatNumber() z NumberFormatter('en_US'), ale używa go tylko dla ilości, cen jednostkowych i kursu waluty. Pola sum i podatków (P_13_*, P_14_*, P_15) nadal używają sprintf('%.2f', ...) — czyli są nadal wrażliwe na locale. Poprawka P1 (setlocale) jest nadal wymagana.
# Co robi poprawka P1: public function getInvoiceXml(array $invoice) { setlocale(LC_NUMERIC, 'C'); // NETLink P1 ...
<Rozliczenie> przez tabelę ksefconfig i GUI LMS. Saldo klienta pojawia się tylko wtedy, gdy w konfiguracji oddziału w GUI LMS jest zaznaczony odpowiedni checkbox (showbalancesummary). Domyślnie jest wyłączone — co jest poprawnym zachowaniem. Zgodnie ze schematem FA(3) sekcja <Rozliczenie> ma minOccurs="0" — jest opcjonalna i MF jej nie wymaga.
Jeśli chcesz żeby faktury PDF i XML zawierały saldo klienta (sekcja <Rozliczenie>) — skonfiguruj to w GUI LMS:
Administracja → Konfiguracja → KSeF → Ustawienia oddziałów → Pokazuj rozliczenie salda
Sprawdź czy patche zostały poprawnie nałożone:
# Uruchom z katalogu LMS cd /twoja/sciezka/lms && bash bin/check-netlink-patches.sh
[ksef] do lms.ini ze środowiskiem (jeśli jeszcze nie ma): environment = prodksefconfig — jeśli pusta, wstaw rekord (patrz sekcja Historia zmian → 18.04.2026)cd /twoja/sciezka/lms && composer dump-autoloadUPO to dokument XML wystawiony przez MF potwierdzający że faktura przeszła walidację i została przyjęta przez KSeF. MF przechowuje UPO przez 10 lat — możesz je pobrać w każdej chwili, nie ma pośpiechu.
lms-ksef.php automatycznie zapisuje UPO na dysk podczas
pobierania statusów sesji wsadowych. Pliki lądują w:{sys_dir}/storage/ksef/upo/{NIP}/{YYYYMMDD}/{numerKSeF}.xml
upgradedb.php Chilka. Jeśli jest pusty lub go
brakuje — sprawdź i utwórz ręcznie:
ls -la /twoja/sciezka/lms/storage/ksef/
# Powinno byc: drwxr-xr-x apache apache ... upo
# Jesli brak:
mkdir -p /twoja/sciezka/lms/storage/ksef/upo
chown apache:apache /twoja/sciezka/lms/storage/ksef/upo
chmod 750 /twoja/sciezka/lms/storage/ksef/upo
Plugin GUI (KSeFSubmit) od wersji 17.04.2026 fix3 pobiera i zapisuje UPO automatycznie — razem z numerem KSeF, w tym samym żądaniu HTTP co wysyłka faktury. UPO trafia do tego samego katalogu co przy wysyłce przez CLI:
{sys_dir}/storage/ksef/upo/{NIP}/{YYYYMMDD}/{numerKSeF}.xml
Jeśli plugin nie zdążył pobrać UPO (timeout 45s) — lms-ksef.php --status-only pobierze je automatycznie przy kolejnym uruchomieniu crona.
Pierwsze uruchomienie — pobierz wszystkie faktury które wystawcy przesłali do KSeF na Twój NIP. Skrypt używa Export API (jeden zaszyfrowany ZIP zamiast setek osobnych zapytań):
cd /twoja/twoja/sciezka/lms/bin
# Najpierw dry-run — sprawdź ile faktur zostanie pobranych
./lms-ksef-sync.php -C /sciezka/do/lms.ini --from=2026/01/01 --dry-run
# Właściwy sync (zakres maksymalnie 3 miesiące, podziel jeśli większy)
./lms-ksef-sync.php -C /sciezka/do/lms.ini --from=2026/01/01 --to=2026/03/31
./lms-ksef-sync.php -C /sciezka/do/lms.ini --from=2026/04/01
# Dry-run — sprawdź ile faktur do wysłania ./lms-ksef.php -C /sciezka/do/lms.ini --dry-run # Właściwa wysyłka ./lms-ksef.php -C /sciezka/do/lms.ini
Zamień /twoja/sciezka/lms i /sciezka/do/lms.ini na własne ścieżki.
lms-ksef.php kiedy chcesz — 1. dnia, 5. dnia, codziennie. To wynika z Twojej konfiguracji taryf w LMS, nie ze skryptów.
Klasyczny model: LMS generuje faktury 1. dnia, skrypty wysyłają je do KSeF i e-mailem tego samego dnia. Ważne: lms-ksef.php startuje 20 minut po generowaniu — żeby faktury były już gotowe w bazie.
# crontab -e # Generowanie faktur i płatności — 1. dnia o 02:00 0 2 1 * * /twoja/sciezka/lms/bin/lms-payments.php -C /sciezka/do/lms.ini # Wysyłka do KSeF — 20 minut po generowaniu 20 2 1 * * php /twoja/sciezka/lms/bin/lms-ksef.php -C /sciezka/do/lms.ini >> /var/log/lms-ksef.log 2>&1 # Wysyłka e-mail B2B (tylko faktury z numerem KSeF) 40 2 1 * * /twoja/sciezka/lms/bin/lms-sendinvoices.php -C /sciezka/do/lms.ini --ksef # Wysyłka e-mail B2C (osoby fizyczne — nie wymagają KSeF) 45 2 1 * * /twoja/sciezka/lms/bin/lms-sendinvoices.php -C /sciezka/do/lms.ini --without-ksef --section=sendinvoices-b2c # Sprawdzanie statusów KSeF co godzinę (przez cały miesiąc) 0 * * * * php /twoja/sciezka/lms/bin/lms-ksef.php -C /sciezka/do/lms.ini --status-only >> /var/log/lms-ksef.log 2>&1 # Pobieranie faktur zakupowych od dostawców — codziennie o 06:15 15 6 * * * php /twoja/sciezka/lms/bin/lms-ksef-download.php -C /sciezka/do/lms.ini -q >> /var/log/lms-ksef-download.log 2>&1
Jeśli Twoi klienci mają zobowiązania rozłożone na różne dni miesiąca, lms-payments.php generuje faktury codziennie tylko dla tych którzy mają zobowiązanie na dany dzień. Wtedy warto też codziennie wysyłać do KSeF:
# Generowanie faktur — codziennie o 02:00 0 2 * * * /twoja/sciezka/lms/bin/lms-payments.php -C /sciezka/do/lms.ini # Wysyłka do KSeF — 20 min po generowaniu, codziennie 20 2 * * * php /twoja/sciezka/lms/bin/lms-ksef.php -C /sciezka/do/lms.ini >> /var/log/lms-ksef.log 2>&1 # Wysyłka e-mail B2B 40 2 * * * /twoja/sciezka/lms/bin/lms-sendinvoices.php -C /sciezka/do/lms.ini --ksef # Wysyłka e-mail B2C 45 2 * * * /twoja/sciezka/lms/bin/lms-sendinvoices.php -C /sciezka/do/lms.ini --without-ksef --section=sendinvoices-b2c # Sprawdzanie statusów KSeF co godzinę 0 * * * * php /twoja/sciezka/lms/bin/lms-ksef.php -C /sciezka/do/lms.ini --status-only >> /var/log/lms-ksef.log 2>&1 # Pobieranie faktur zakupowych — codziennie 15 6 * * * php /twoja/sciezka/lms/bin/lms-ksef-download.php -C /sciezka/do/lms.ini -q >> /var/log/lms-ksef-download.log 2>&1
lms-payments.php generuje faktury w LMS.lms-ksef.php wysyła je do KSeF (uruchom minimum 20 minut po generowaniu).lms-sendinvoices.php --ksef wysyła e-maile tylko fakturom z numerem KSeF — musi startować po lms-ksef.php.--status-only (co godzinę) odbierze numery automatycznie. E-mail pójdzie przy następnym uruchomieniu lms-sendinvoices.php.
Możesz uruchamiać wszystkie skrypty ręcznie kiedy chcesz — cron tylko to automatyzuje:
php /twoja/sciezka/lms/bin/lms-ksef.php -C /sciezka/do/lms.ini --dry-run # sprawdź ile faktur czeka php /twoja/sciezka/lms/bin/lms-ksef.php -C /sciezka/do/lms.ini # wyślij do KSeF php /twoja/sciezka/lms/bin/lms-ksef.php -C /sciezka/do/lms.ini --status-only # odbierz numery /twoja/sciezka/lms/bin/lms-sendinvoices.php -C /sciezka/do/lms.ini --ksef # wyślij e-maile
| lms-payments.php | Generuje faktury w LMS na dany dzień (wg taryf) | zależy od taryf |
| lms-ksef.php | Wysyła faktury do KSeF, odbiera numery KSeF | po generowaniu |
| lms-ksef.php --status-only | Sprawdza statusy i odbiera numery KSeF dla oczekujących | co godzinę |
| lms-sendinvoices.php --ksef | Wysyła e-mail z PDF do B2B (tylko z numerem KSeF) | po wysyłce do KSeF |
| lms-sendinvoices.php --without-ksef | Wysyła e-mail z PDF do B2C (osoby fizyczne) | po generowaniu |
| lms-ksef-download.php | Pobiera faktury zakupowe od dostawców | codziennie |
Szanowni Państwo,<br> <br> w załączniku faktura VAT <b>%invoice</b>.<br> Numer KSeF: <b>%ksef-number</b><br> Kwota do zapłaty: <b>%value PLN</b><br> Numer konta: %bankaccount<br> <br> Z poważaniem
Szanowny Kliencie,<br> <br> w załączniku faktura <b>%invoice</b>.<br> Kwota do zapłaty: <b>%value PLN</b><br> Numer konta: %bankaccount<br> <br> Dziękujemy!
| Zmienna | Znaczenie |
|---|---|
%invoice | Numer faktury (np. LMS_001/04/2026) |
%ksef-number | Numer KSeF (po potwierdzeniu przez MF) |
%value | Kwota brutto |
%bankaccount | Numer konta bankowego |
%customerid | ID klienta w LMS |
%pin | PIN klienta |
Przed wysyłką do KSeF warto sprawdzić czy XML faktury wygląda poprawnie. Skrypt gen-xml.php generuje XML bez wysyłki:
cd /twoja/sciezka/lms php bin/gen-xml.php -C /sciezka/do/lms.ini ID_FAKTURY # XML zapisywany jest do /tmp/ID_FAKTURY.xml cat /tmp/ID_FAKTURY.xml
Sprawdź w wygenerowanym XML:
2595.30<Rozliczenie> zawiera kwotę faktury i <DoZaplaty><Podmiot2> z NIPemKSeF bywa niedostępny — czy to przez kilka godzin, czy przez kilka dni. Skrypty są zaprojektowane tak, żeby nic nie ginęło i żebyś nie musiał robić niczego ręcznie w większości przypadków.
Co się dzieje automatycznie: skrypty przed wysyłką sprawdzają dostępność KSeF przez oficjalny endpoint MF (tzw. Latarnia). Jeśli KSeF nie działa — skrypt kończy pracę z komunikatem i faktury czekają w kolejce.
Co Ty robisz: nic. Po powrocie MF cron automatycznie wyśle wszystkie zaległe faktury i odbierze numery KSeF. Możesz też uruchomić ręcznie:
# Sprawdź ile faktur czeka php /twoja/sciezka/lms/bin/lms-ksef.php -C /sciezka/do/lms.ini --dry-run # Wyślij zaległe i odbierz numery KSeF php /twoja/sciezka/lms/bin/lms-ksef.php -C /sciezka/do/lms.ini
Czasem Ministerstwo Finansów oficjalnie ogłasza tryb offline — oznacza to, że faktury można wystawiać bez wysyłania ich do KSeF, a po powrocie systemu masz kilka dni na uzupełnienie. W tym czasie:
# Wyślij e-mailem faktury które czekają na numer KSeF (tryb offline MF) /twoja/sciezka/lms/bin/lms-sendinvoices.php -C /sciezka/do/lms.ini --ksef-offline
Jeśli masz skonfigurowany certyfikat offline — na fakturach PDF pojawi się specjalny kod QR podpisany Twoim kluczem ECDSA, który można zweryfikować bez internetu. Po powrocie KSeF wystarczy uruchomić lms-ksef.php — skrypt sam wyśle zaległe faktury.
Latarnia (ang. lighthouse) to oficjalny publiczny endpoint Ministerstwa Finansów, który informuje o aktualnym stanie systemu KSeF. Nie wymaga logowania ani certyfikatu — każdy może go odpytać.
Nasze skrypty pytają Latarnię przed każdą próbą wysyłki faktur. Jeśli KSeF jest niedostępny — skrypt nie próbuje wysyłać, tylko loguje informację i kończy pracę. Faktury czekają bezpiecznie w bazie. Przy kolejnym uruchomieniu crona (lub ręcznym) — procedura powtarza się.
Możliwe stany Latarni:
| Status | Znaczenie | Co robi skrypt |
|---|---|---|
Available |
KSeF działa normalnie | Wysyła faktury |
Planned_Maintenance |
Planowana przerwa techniczna | Przerywa — cron nadrobi później |
Unavailable |
Awaria nieplanowana | Przerywa — cron nadrobi później |
Error |
Błąd systemu MF | Przerywa — cron nadrobi później |
| brak odpowiedzi | Latarnia sama niedostępna (problem sieciowy) | Próbuje wysyłać — jeśli KSeF też nie odpowie, obsługuje błąd normalnie |
Jeśli MF odrzuci fakturę (np. brakujący NIP, błędna stawka VAT) — faktura trafia do bazy ze statusem błędu. Nie zniknie i nie zostanie wysłana ponownie automatycznie — musisz to naprawić ręcznie:
# Sprawdź które faktury mają błąd (w bazie danych) SELECT d.fullnumber, kd.status, kd.statusdescription FROM ksefdocuments kd JOIN documents d ON d.id = kd.docid WHERE kd.status >= 400 ORDER BY d.cdate DESC; # Po poprawieniu danych klienta/faktury — ponów wysyłkę php /twoja/sciezka/lms/bin/lms-ksef.php -C /sciezka/do/lms.ini --retry-errors
| Sytuacja | Rozwiązanie |
|---|---|
| Faktury wysłane, brak numerów KSeF (status 100) | Poczekaj — cron --status-only odpytuje co godzinę. Lub ręcznie: lms-ksef.php --status-only |
| Faktury z błędem (status 400) | Popraw dane klienta/faktury, potem: lms-ksef.php --retry-errors |
| Błąd 429 Too Many Requests | MF limit zapytań na godzinę. Odczekaj. Do historycznego sync używaj lms-ksef-sync.php |
| Kwoty w XML z przecinkami (KSeF odrzuca) | Brak poprawki P1. Uruchom: python3 bin/patch-netlink.py /twoja/sciezka/lmsUwaga: smartFormatNumber w KSeF.php od 22.04.2026 nie zastępuje P1 — pola P_13_*/P_14_*/P_15 nadal używają sprintf i są wrażliwe na locale. |
| Błędna kwota w KSeF (niedopłata) | Sprawdź ustawienia oddziału w GUI LMS (ksefconfig.showbalancesummary). Korekty: lms-ksef-bulk-correct.php --dry-run |
| Błąd certyfikatu | openssl pkcs12 -in /etc/lms/ksef_cert.p12 -info — sprawdź ważność i hasło |
| Faktury sprzed boundary_date mają "BRAK NUMERU KSEF" na PDF | Tabela ksefconfig ma boundarydate=0. Napraw SQL:MySQL: UPDATE ksefconfig SET boundarydate=UNIX_TIMESTAMP('2026-04-01') WHERE boundarydate=0 OR boundarydate IS NULL;PostgreSQL: UPDATE ksefconfig SET boundarydate=1775001600 WHERE boundarydate=0 OR boundarydate IS NULL;Lub uruchom instalator ponownie — uzupełni automatycznie. |
Po git pull przestały działać faktury lub KSeF |
REPO MASTER mógł zmienić lib/KSeF/KSeF.php — nasze poprawki P1/P2 znikają po każdej aktualizacji. Zawsze po git pull uruchom ponownie instalator:bash /tmp/ksef-pack/lms-npi.sh -c /etc/lms/lms.inirm -rf /sciezka/lms/templates_c/* |
Plugin wysyła fakturę ale timeout 45s — brak numeru KSeF w GUI, pojawia się dopiero po --status-only |
Najprawdopodobniej update biblioteki n1ebieski/ksef-php-client ze starszej wersji na v1.x — w v1.x kod statusu sesji jest w statusData->status->code, a starsze wersje używały statusData->processingCode. Plugin czyta zawsze null, polling nigdy nie widzi kodu 200, odpada timeoutem. Faktura i tak trafia do KSeF — tylko numer KSeF i UPO nie wróciły do GUI od razu.Fix: zainstaluj paczkę 17.04.2026 fix3 lub nowszą — poprawka jest już w KSeFApiService.php. |
| Instalator wychodzi bez żadnego komunikatu | Uruchamiasz sam plik .sh bez reszty paczki — brak version.txt obok skryptu. Zawsze rozpakowuj całą paczkę:unzip lms-ksef-netlink-*.zip -d /tmp/ksef-packbash /tmp/ksef-pack/lms-npi.sh -c /etc/lms/lms.ini --dry-run |
| Instalator: configparser.ParsingError lub Brak nazwy bazy | Stara wersja instalatora (przed v2026-04-09). Pobierz aktualną paczkę — nowy instalator czyta lms.ini przez grep, odporny na wieloliniowe wartości takie jak mail_body w sekcji [sendinvoices]. |
| Instalator: Nie znaleziono katalogu LMS | Skrypt nie może znaleźć katalogu LMS. Sprawdź czy w lms.ini jest:[directories]sys_dir = /sciezka/do/lms |
check-netlink-patches.sh: BRAK P2 |
Stary checker z wersji przed 13.04.2026. Wgraj aktualną paczkę — nowy checker sprawdza showbalancesummary |
lib/KSeF/KSeF.php lokalnie zmodyfikowany — instalator pomija, P1 nie jest naniesiona |
Instalator celowo nie nadpisuje plików zmienionych lokalnie. Po instalacji uruchom patcher który bezpiecznie naniesie tylko brakujące poprawki:python3 /twoja/sciezka/lms/bin/patch-netlink.py /twoja/sciezka/lmsbash /twoja/sciezka/lms/bin/check-netlink-patches.sh |
lib/LMSDocuments/LMSTcpdfInvoice.php lokalnie zmodyfikowany — instalator pomija, stary plik bez naszych zmian |
Sprawdź czy plik ma nasze modyfikacje:grep -c "info_box_text" /twoja/sciezka/lms/lib/LMSDocuments/LMSTcpdfInvoice.phpWynik 0 = stary plik, skopiuj ręcznie z paczki:cp /tmp/ksef-pack/lib/LMSDocuments/LMSTcpdfInvoice.php /twoja/sciezka/lms/lib/LMSDocuments/ |
30.04.2026
<TerminOpis> z opisem kwoty do zapłaty w PLN, kwoty w walucie i kursu przeliczenia28.04.2026
xmladdallvalues w konfiguracji oddziału (ksefconfig) — generuje w XML wartości <P_11> / <P_11A> dla wszystkich pozycjiksefdelays, ksefallconsumers, ksefboundarydates, ksefshowbalancesummaries) zastąpione jedną tabelą ksefconfig24.04.2026
<P_18A> (mechanizm podzielonej płatności) bazuje teraz na fladze DOC_FLAG_SPLIT_PAYMENT zamiast automatycznego progu kwotowego ≥ 15 000 złdemo i test (Latarnia działa tylko na PROD; wcześniej blokować wysyłkę na środowiskach testowych)<Podmiot3> nie jest generowany (wcześniej <BrakID>1</BrakID> powodował odrzucenie faktury przez walidator XSD)lms-npi.sh — zastępuje lms-netlink-install.sh; automatycznie wykrywa katalog LMS z lms.ini, wersję PHP i użytkownika Apache; jedno polecenie instaluje wszystko22.04.2026
smartFormatNumber() dla ilości i cen jednostkowych (merge z master 22.04); poprawka P1 (setlocale) nadal wymagana dla pól sum/VATKodKraju) pobierany z bazy danych (countries), wcześniej na stałe PLpart=auto (merge z master)PIN/ID) wyświetlany po lewej stronie (obok danych nabywcy), nie po prawejinvoices.logo_path jako zamiennik invoices.header_imageinvoices.issuer w lms.ini nadpisuje pole wystawcy z bazy danychinvoices.info_box_text (B2B) i invoices.info_box_text_b2c (B2C) — pojawia się tylko na fakturach po ksef.boundary_date21.04.2026
smartFormatNumber() z NumberFormatter (merge z master 21.04)18.04.2026
n1ebieski/ksef-php-client v1.x — po aktualizacji biblioteki plugin nie pobierał numeru KSeF ani UPOlms-ksef.php gotowy na PostgreSQL (wcześniej działał tylko z MySQL)lms-ksef.php) — funkcja pobierająca dane faktury nie zwracała typu dokumentu, przez co XML był generowany błędnie. Teraz typ jest dociągany osobno z bazy.lms-ksef.php — domyślne okno skanowania 60 dni wstecz zamiast od 1.04 na zawsze; opcja --from-date=YYYY-MM-DDlms-ksef.php — stan Latarni TOTAL_FAILURE blokuje wysyłkę (wcześniej był ignorowany)lms-ksef.php — czytelniejszy log po filtracji B2B/B2CWymagane po instalacji — tabela ksefconfig
Jeśli tabela ksefconfig jest pusta, filtry KSeF w GUI LMS nie działają poprawnie. Wstaw rekord dla każdej dywizji (sprawdź ID w Ustawienia → Oddziały).
MySQL:
INSERT INTO ksefconfig (divisionid, delay, allconsumers, boundarydate, showbalancesummary, xmladdallvalues)
VALUES (1, 0, 0, UNIX_TIMESTAMP('2026-04-01'), 0, 0);
PostgreSQL:
INSERT INTO ksefconfig (divisionid, delay, allconsumers, boundarydate, showbalancesummary, xmladdallvalues) VALUES (1, 0, 0, EXTRACT(EPOCH FROM TIMESTAMP '2026-04-01')::bigint, 0, 0);
Zmień divisionid = 1 jeśli masz inny ID dywizji. Jeśli masz więcej dywizji — wstaw wiersz dla każdej.
17.04.2026
StopkaFaktury, format_ten, bugfixy KSeF.phpksefallconsumers → ksefconfigPobierz wymagane paczki do prawidłowej instalacji i integracji:
Aktualizacja LMS z repo Chilka + ponowne nałożenie NETLink
Jeśli LMS jest zainstalowany z repo git, zwykły git pull często się burzy gdy masz lokalne zmiany (a masz — pliki NETLink).
Poniższe komendy wymuszają czystą synchronizację z repo, a potem przywracają NETLink jednym poleceniem.
1. Wejdź do katalogu LMS i wymuś aktualizację do najnowszego repo:
cd /home/lms git fetch --all git reset --hard origin/master
2. Pobierz aktualną paczkę NETLink i uruchom instalator:
cd /tmp wget -O lms-ksef-netlink-30042026.zip "https://ksef2lms.idzik.pl/?track=lms-ksef-netlink-30042026.zip" unzip -o lms-ksef-netlink-30042026.zip -d ksef-pack bash ksef-pack/lms-npi.sh -c /etc/lms/lms.ini
Co zostaje nieruszone przez git reset: Twoje własne pluginy i pliki których Chilek nie ma w repo (np. własny plugin SMS, moduły, szablony) — git ich nie widzi i nie tknie. Pliki NETLink zostaną nadpisane przez git, ale lms-npi.sh je przywróci w kroku 2.
⚠ Uwaga: Jeśli masz własne modyfikacje w plikach które są częścią repo Chilka (np. zmiany w szablonach, modułach LMS-a),
git reset --hard usunie je bezpowrotnie.
W takim przypadku nie używaj tych poleceń dosłownie — dostosuj je do swoich potrzeb
(np. zrób wcześniej git stash albo ręcznie skopiuj zmienione pliki przed resetem).
Instalacja krok po kroku — instalacja manualna z paczki ZIP (bez git)
Dla tych którzy nie mają LMS-a z repo git — pobierają ZIP z oryginalnego repozytorium Chilka i wgrywają ręcznie. Wejdź SSH na serwer i wykonaj kolejno:
1. Pobierz obie paczki na serwer (podmień datę jeśli inna):
cd /tmp wget -O lms-master-30042026.zip "https://ksef2lms.idzik.pl/?track=lms-master-30042026.zip" wget -O lms-ksef-netlink-30042026.zip "https://ksef2lms.idzik.pl/?track=lms-ksef-netlink-30042026.zip"
2. Rozpakuj LMS-master i zaktualizuj pliki LMS (zamień /ścieżka/do/katalogu/lms na właściwą):
unzip -q lms-master-30042026.zip -d /tmp/lms-src rsync -a /tmp/lms-src/lms-master/ /ścieżka/do/katalogu/lms/
3. Rozpakuj paczkę z poprawkami NETLink i uruchom instalator:
unzip -q lms-ksef-netlink-30042026.zip -d /tmp/ksef-pack bash /tmp/ksef-pack/lms-npi.sh -c /etc/lms/lms.ini
Jeśli chcesz najpierw zobaczyć co zostanie zrobione (bez żadnych zmian):
bash /tmp/ksef-pack/lms-npi.sh -c /etc/lms/lms.ini --dry-run
Instalator sam odczyta katalog LMS z lms.ini, wykryje użytkownika Apache,
skopiuje pliki (z backupem .bak) i nałoży patche na KSeF.php.
| Data | LMS-MASTER | NETLink pack | Zmiany |
|---|---|---|---|
| 28.04.2026 | lms-master-28042026.zip | lms-ksef-netlink-28042026.zip | Merge z master 28.04: |
| 24.04.2026 | lms-master-24042026.zip | lms-ksef-netlink-24042026.zip | Merge z master 24.04: KSeF.php — P_18A bazuje na DOC_FLAG_SPLIT_PAYMENT (bugfix Chilka). Latarnia pomijana na demo/test. Fix XSD JST: Podmiot3 nie generuje BrakID gdy brak NIP odbiorcy. |
| 22.04.2026 | lms-master-22042026.zip | lms-ksef-netlink-22042026.zip | Merge z master 22.04: KSeF.php — GROUPING_USED=false (fix separatora tysięcy w XML), KodKraju z bazy countries, lms-sendinvoices.php part=auto, bugfixy LMSEventManager/numberplanedit. |
| 21.04.2026 | lms-master-21042026.zip | lms-ksef-netlink-21042026.zip | Merge z master 21.04: smartFormatNumber (KSeF.php) + zachowany setlocale patch P1. lms-master-update-21042026.zip zawiera tylko 12 zmienionych plików (delta, nie pełny master). |
| 18.04.2026 | lms-master-18042026.zip | lms-ksef-netlink-18042026.zip | Faktura korygująca (plugin + bulk). PostgreSQL. lms-ksef.php: --from-date, TOTAL_FAILURE, czystszy log. Instrukcja INSERT ksefconfig. |
| 17.04.2026 | lms-master-17042026.zip | lms-ksef-netlink-17042026.zip | REPO MASTER 17.04. Plugin KSeFSubmit przepisany na Batch API. Migracja ksefallconsumers→ksefconfig. |
| 15.04.2026 | lms-master-15042026.zip | lms-ksef-netlink-15042026.zip | Chilek: bugfix pustego adresu w XML (AdresL1/AdresL2), bugfix INSERT ksefconfig. |
| 14.04.2026 | lms-master-14042026.zip | lms-ksef-netlink-14042026.zip | Chilek: xmladdallvalues (P_11/P_11A), bugfix ksefshowbalancesummary, lms-notify. |
| 13.04.2026 | lms-master-13042026.zip | lms-ksef-netlink-13042026.zip | KSeF.php: nowe Rozliczenie, NazwaBanku. Poprawka fill-xml po 429. P2 wycofana. |
| 11.04.2026 | lms-master-11042026.zip | lms-ksef-netlink-11042026.zip | Pierwsza publiczna wersja. UPO, plugin GUI, saveUpoFile w cron. |
cd /twoja/sciezka/lms && bash check-netlink-patches.sh
Uwagi lub zapytania dotyczące działania integracji można zgłaszać na adres: cocoban78@gmail.com.