Search by

beziworld / laravel-ksef

kamcw

KSeF 2.0 (Krajowy System e-Faktur) client for Laravel: token authentication, encrypted online sessions, FA(3) invoice builder, UPO, invoice download and QR verification links.

Package info

github.com/Beziworld/laravel-ksef

Homepage

Issues

pkg:composer/beziworld/laravel-ksef

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.0 2026-11-04 15:08 UTC

This package is auto-updated.

Last update: 2026-11-04 16:36:33 UTC


README

tests Latest Version License

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

Polska wersja dokumentacji

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 .env or 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 OnlineSession object.

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.

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.