laravel-ksef maintained by beziworld
Laravel KSeF
KSeF 2.0 (Krajowy System e-Faktur, the Polish National e-Invoicing System) client for Laravel:
- authentication with a KSeF token (RSA-OAEP SHA-256), with cached and refreshed access tokens
- encrypted interactive sessions (AES-256-CBC)
- a fluent FA(3) invoice builder that validates against the official XSD shipped with the package
- invoice status, KSeF number, UPO, invoice download and metadata queries
- the KOD I verification link for the QR code printed on invoice visualisations
Requirements
PHP 8.3+, Laravel 12 or 13, ext-openssl, ext-dom. RSA-OAEP with SHA-256 is handled by
phpseclib, because PHP's openssl_public_encrypt() only supports OAEP with SHA-1, which KSeF rejects.
Installation
composer require beziworld/laravel-ksef
php artisan vendor:publish --tag=ksef-config # optional
KSEF_ENVIRONMENT=test # test | demo | production
KSEF_NIP=5261040828 # context: the taxpayer the application acts for
KSEF_TOKEN=20251104-EC-... # KSeF token with the InvoiceWrite / InvoiceRead permissions
KSEF_SYSTEM_INFO="My ERP" # stored in the invoice header (SystemInfo)
KSEF_CACHE_STORE= # cache store for tokens and certificates, default store when empty
Generate the token in the KSeF application of the selected environment (Tokens section). The TEST environment accepts fictitious data, DEMO is the pre-production environment and PRODUCTION issues legally binding invoices.
Build an FA(3) invoice
use Beziworld\Ksef\Fa3\Address;
use Beziworld\Ksef\Fa3\Invoice;
use Beziworld\Ksef\Fa3\Line;
use Beziworld\Ksef\Fa3\Party;
use Beziworld\Ksef\Fa3\PaymentMethod;
use Beziworld\Ksef\Fa3\VatRate;
$invoice = Invoice::make('FV/12/11/2026')
->issuedAt('2026-11-04')
->soldAt('2026-11-03')
->seller(Party::company('5261040828', 'ACME Sp. z o.o.', Address::polish('ul. Prosta 1', '00-001', 'Warszawa'), 'faktury@acme.pl'))
->buyer(Party::company('1234563218', 'Klient S.A.', Address::polish('ul. Polna 5', '30-001', 'Kraków')))
->addLine(new Line('Website development', quantity: 1, unitNetPrice: 5000.00, vatRate: VatRate::Rate23, unit: 'usł.'))
->addLine(new Line('Book', quantity: 2, unitNetPrice: 39.99, vatRate: VatRate::Rate5))
->payment(PaymentMethod::Transfer, dueDate: '2026-11-18', bankAccount: 'PL61 1090 1014 0000 0712 1981 2874');
$xml = $invoice->validate(); // FA(3) XML, validated against the bundled XSD
The builder calculates the P_13_x / P_14_x totals per VAT rate and the gross total P_15; VAT is rounded once per rate.
| Case | How |
|---|---|
| Consumer (no NIP) | Party::consumer('Jan Kowalski', $address) |
| EU company | Party::euCompany('DE', '123456789', 'Kunde GmbH', $address) with VatRate::ZeroIntraCommunity |
| Non-EU company | Party::foreignCompany('US', '12-3456789', 'Client Inc.', $address) with VatRate::ZeroExport |
| Foreign currency | ->currency('EUR', 4.2512) adds the VAT converted to PLN (P_14_xW) |
| Exempt / reverse charge | VatRate::Exempt / VatRate::ReverseCharge (annotations are set automatically) |
| Already paid | ->paid('2026-11-04') |
| GTU code, PKWiU, GTIN | new Line(..., gtu: 'GTU_12', pkwiu: '62.01.11.0', gtin: '5901234123457') |
Plain "0%" does not exist in FA(3): use ZeroDomestic (0 KR), ZeroIntraCommunity (0 WDT) or ZeroExport (0 EX).
Correction invoice (KOR)
use Beziworld\Ksef\Fa3\CorrectedInvoice;
$correction = Invoice::make('FK/1/11/2026')
->seller($seller)->buyer($buyer)
->correcting(new CorrectedInvoice('FV/12/11/2026', '2026-11-04', ksefNumber: '5261040828-20261104-0100A0B0C0D0-AB'), 'Discount')
->addLine(new Line('Website development', 1, 5000.00, VatRate::Rate23, before: true)) // state before
->addLine(new Line('Website development', 1, 4500.00, VatRate::Rate23)); // state after
The totals of a correction are the differences (here -500.00 net, -115.00 VAT).
Send to KSeF
use Beziworld\Ksef\Facades\Ksef;
$status = Ksef::send($invoice); // opens a session, sends, closes the session and waits for the result
if ($status->isAccepted()) {
$invoiceModel->update(['ksef_number' => $status->ksefNumber]);
} else {
logger()->warning('KSeF rejected the invoice', ['code' => $status->code, 'details' => $status->details]);
}
send() accepts an Invoice or ready FA(3) XML (for example generated by another system).
For many invoices, reuse one session:
$session = Ksef::openSession();
try {
$submitted = collect($invoices)->map(fn (Invoice $invoice) => Ksef::sendInvoice($session, $invoice));
} finally {
Ksef::closeSession($session);
}
foreach ($submitted as $item) {
$status = Ksef::waitForInvoice($item); // or Ksef::invoiceStatus($item) from a queued job
$upo = $status->isAccepted() ? Ksef::upo($item) : null;
}
Store SubmittedInvoice::$sessionReferenceNumber and $invoiceReferenceNumber to read the status and UPO later.
Status codes: 100/150 processing, 200 accepted (KSeF number assigned), 440 duplicate, 450 semantic error.
Received invoices and downloads
$page = Ksef::queryInvoices('Subject2', now()->startOfMonth(), now()); // Subject1 = issued, Subject2 = received
foreach ($page->invoices as $meta) {
$xml = Ksef::invoice($meta->ksefNumber);
}
InvoicePage::$hasMore tells whether to ask for the next page (pageOffset); isTruncated means KSeF reached its
result limit, so narrow the date range.
QR code (KOD I)
Every invoice visualisation (PDF, printout) must carry a QR code with the verification link:
$url = Ksef::verificationUrl($xml);
// https://qr.ksef.mf.gov.pl/invoice/5261040828/04-11-2026/<base64url(sha256(xml))>
Pass exactly the XML that was sent to KSeF: the link contains its SHA-256 hash. Render the QR code with any library
(for example simplesoftwareio/simple-qrcode) and print the KSeF number below it.
Errors
| Exception | When |
|---|---|
InvalidInvoice |
the invoice is incomplete or fails the XSD (errors() lists libxml messages) |
AuthenticationException |
the token is invalid, revoked or lacks permissions |
ApiException |
KSeF rejected a request: exceptionCode(), httpStatus(), isRateLimited(), retryAfter() |
InvalidConfiguration |
missing NIP or token, unknown environment |
KSeF enforces rate limits (HTTP 429 with Retry-After). In queued jobs, release the job after retryAfter() seconds.
After HTTP 401 the client forgets the cached tokens and repeats the request once with fresh ones.
Security
- The KSeF token is a secret equivalent to a password: keep it in
.envor a secrets manager, never in the repository. - Access and refresh tokens are cached in the configured cache store; use a store that is not shared with untrusted code.
- The symmetric session key exists only in memory for the lifetime of the
OnlineSessionobject.
Development
composer install
composer ci # pint --test, phpstan (level 8), pest
Tests validate the generated XML against the official FA(3) schema and run the whole authentication and session flow against a faked API that decrypts the payloads with a test RSA key.
Author
Created and maintained by Kamil Ćwiertnia, beziworld.eu.
- E-mail: biuro@beziworld.eu
- Issues: GitHub issues
Need a KSeF integration for your ERP, shop or invoicing system? Get in touch through beziworld.eu.
Other packages by the author
laravel-ifirma · laravel-fakturownia · laravel-infakt · laravel-wfirma · laravel-company-data
License
MIT. See LICENSE.md. The FA(3) XSD files in resources/schema are published by the Polish Ministry of
Finance. This package is not affiliated with the Ministry of Finance.