n1ebieski / ksef-php-client
PHP API client that allows you to interact with the API Krajowego Systemu e-Faktur
Installs: 97
Dependents: 0
Suggesters: 0
Security: 0
Stars: 7
Watchers: 3
Forks: 1
Open Issues: 0
pkg:composer/n1ebieski/ksef-php-client
Requires
- php: ^8.1.0
- cuyz/valinor: ^1.15
- endroid/qr-code: ^5.1
- friendsofphp/php-cs-fixer: ^3.88
- krowinski/bcmath-extended: ^6.0
- pestphp/pest: ^2.36
- pestphp/pest-plugin-drift: ^2.5
- php-http/discovery: ^1.20.0
- phpseclib/phpseclib: ^3.0
- psr-discovery/log-implementations: ^1.0
- psr/http-client: ^1.0.3
- psr/http-client-implementation: ^1.0.1
- psr/http-factory-implementation: *
- psr/http-message: ^1.1.0|^2.0.0
- symfony/uid: ^6.4
Requires (Dev)
- dg/bypass-finals: ^1.9
- guzzlehttp/guzzle: ^7.9.2
- guzzlehttp/psr7: ^2.7.0
- mockery/mockery: ^1.6
- monolog/monolog: ^3.9
- phpstan/phpstan: ^2.1
- rector/rector: ^2.0
- vlucas/phpdotenv: ^5.6
This package is auto-updated.
Last update: 2025-10-08 06:49:18 UTC
README
KSEF PHP Client
This package is not production ready yet!
PHP API client that allows you to interact with the KSEF API Krajowy System e-Faktur
Main features:
- Support for authorization using qualified certificates, KSeF certificates, KSeF tokens, and trusted ePUAP signatures (manual mode)
- Logical invoice structure mapped to DTOs and Value Objects
- Automatic access token refresh
- CSR (Certificate Signing Request) handling
- KSeF exception handling
- QR codes generation
KSEF Version | Branch | Release Version |
---|---|---|
2.0 | main | ^0.3 |
1.0 | 0.2.x | 0.2.* |
Table of Contents
- Get Started
- Authorization
- Resources
- Examples
- Testing
- Roadmap
- Special thanks
Get Started
Requires PHP 8.1+
First, install ksef-php-client
via the Composer package manager:
composer require n1ebieski/ksef-php-client
Ensure that the php-http/discovery
composer plugin is allowed to run or install a client manually if your project does not already have a PSR-18 client integrated.
composer require guzzlehttp/guzzle
Client configuration
use N1ebieski\KSEFClient\ClientBuilder; use N1ebieski\KSEFClient\ValueObjects\Mode; use N1ebieski\KSEFClient\Factories\EncryptionKeyFactory; $client = (new ClientBuilder()) ->withMode(Mode::Production) // Choice between: Test, Demo, Production ->withApiUrl($_ENV['KSEF_API_URL']) // Optional, default is set by Mode selection ->withHttpClient(new \GuzzleHttp\Client(...)) // Optional PSR-18 implementation, default is set by Psr18ClientDiscovery::find() ->withLogger(new \Monolog\Logger(...)) // Optional PSR-3 implementation, default is set by PsrDiscovery\Discover::log() ->withLogPath($_ENV['PATH_TO_LOG_FILE'], $_ENV['LOG_LEVEL']) // Optional, level: null disables logging ->withAccessToken($_ENV['ACCESS_TOKEN']) // Optional, if present, auto authorization is skipped ->withRefreshToken($_ENV['REFRESH_TOKEN']) // Optional, if present, auto refresh access token is enabled ->withKsefToken($_ENV['KSEF_TOKEN']) // Required for API Token authorization. Optional otherwise ->withCertificatePath($_ENV['PATH_TO_CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE']) // Required .p12 file for Certificate authorization. Optional otherwise ->withVerifyCertificateChain(true) // Optional. Explanation https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Uzyskiwanie-dostepu/paths/~1api~1v2~1auth~1xades-signature/post ->withEncryptionKey(EncryptionKeyFactory::makeRandom()) // Required for invoice resources. Remember to save this value! ->withIdentifier('NIP_NUMBER') // Required for authorization. Optional otherwise ->build();
Auto mapping
Each resource supports mapping through both an array and a DTO, for example:
use N1ebieski\KSEFClient\Requests\Auth\Status\StatusRequest; use N1ebieski\KSEFClient\Requests\ValueObjects\ReferenceNumber; $authorisationStatusResponse = $client->auth()->status(new StatusRequest( referenceNumber: ReferenceNumber::from('20250508-EE-B395BBC9CD-A7DB4E6095-BD') ))->object();
or:
$authorisationStatusResponse = $client->auth()->status([ 'referenceNumber' => '20250508-EE-B395BBC9CD-A7DB4E6095-BD' ])->object();
Authorization
Auto authorization via KSEF Token
use N1ebieski\KSEFClient\ClientBuilder; $client = (new ClientBuilder()) ->withKsefToken($_ENV['KSEF_KEY']) ->withIdentifier('NIP_NUMBER') ->build(); // Do something with the available resources
Auto authorization via certificate .p12
use N1ebieski\KSEFClient\ClientBuilder; $client = (new ClientBuilder()) ->withCertificatePath($_ENV['PATH_TO_CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE']) ->withIdentifier('NIP_NUMBER') ->build(); // Do something with the available resources
Manual authorization
use N1ebieski\KSEFClient\ClientBuilder; use N1ebieski\KSEFClient\Support\Utility; use N1ebieski\KSEFClient\Requests\Auth\DTOs\XadesSignature; use N1ebieski\KSEFClient\Requests\Auth\XadesSignature\XadesSignatureXmlRequest; $client = (new ClientBuilder())->build(); $nip = 'NIP_NUMBER'; $authorisationChallengeResponse = $client->auth()->challenge()->object(); $xml = XadesSignature::from([ 'challenge' => $authorisationChallengeResponse->challenge, 'contextIdentifierGroup' => [ 'identifierGroup' => [ 'nip' => $nip ] ], 'subjectIdentifierType' => 'certificateSubject' ])->toXml(); $signedXml = 'SIGNED_XML_DOCUMENT'; // Sign a xml document via Szafir, ePUAP etc. $authorisationAccessResponse = $client->auth()->xadesSignature( new XadesSignatureXmlRequest($signedXml) )->object(); $client = $client->withAccessToken($authorisationAccessResponse->authenticationToken->token); $authorisationStatusResponse = Utility::retry(function () use ($client, $authorisationAccessResponse) { $authorisationStatusResponse = $client->auth()->status([ 'referenceNumber' => $authorisationAccessResponse->referenceNumber ])->object(); if ($authorisationStatusResponse->status->code === 200) { return $authorisationStatusResponse; } if ($authorisationStatusResponse->status->code >= 400) { throw new RuntimeException( $authorisationStatusResponse->status->description, $authorisationStatusResponse->status->code ); } }); $authorisationTokenResponse = $client->auth()->token()->redeem()->object(); $client = $client ->withAccessToken( token: $authorisationTokenResponse->accessToken->token, validUntil: $authorisationTokenResponse->accessToken->validUntil ) ->withRefreshToken( token: $authorisationTokenResponse->refreshToken->token, validUntil: $authorisationTokenResponse->refreshToken->validUntil ); // Do something with the available resources
Resources
Auth
Xades Signature
use N1ebieski\KSEFClient\Requests\Auth\XadesSignature\XadesSignatureRequest; $response = $client->auth()->xadesSignature( new XadesSignatureRequest(...) )->object();
or:
use N1ebieski\KSEFClient\Requests\Auth\XadesSignature\XadesSignatureXmlRequest; $response = $client->auth()->xadesSignature( new XadesSignatureXmlRequest(...) )->object();
Auth Status
use N1ebieski\KSEFClient\Requests\Auth\Status\StatusRequest; $response = $client->auth()->status( new StatusRequest(...) )->object();
Token
Security
Sessions
Sessions Invoices
Upo
use N1ebieski\KSEFClient\Requests\Sessions\Invoices\Upo\UpoRequest; $response = $client->sessions()->invoices()->upo( new UpoRequest(...) )->body();
Ksef Upo
use N1ebieski\KSEFClient\Requests\Sessions\Invoices\KsefUpo\KsefUpoRequest; $response = $client->sessions()->invoices()->ksefUpo( new KsefUpoRequest(...) )->body();
Invoices Status
use N1ebieski\KSEFClient\Requests\Sessions\Invoices\Status\StatusRequest; $response = $client->sessions()->invoices()->status( new StatusRequest(...) )->object();
Online
Open
use N1ebieski\KSEFClient\Requests\Sessions\Online\Open\OpenRequest; $response = $client->sessions()->online()->open( new OpenRequest(...) )->object();
Close
use N1ebieski\KSEFClient\Requests\Sessions\Online\Close\CloseRequest; $response = $client->sessions()->online()->close( new CloseRequest(...) )->status();
Invoices send
for DTO invoice:
use N1ebieski\KSEFClient\Requests\Sessions\Online\Send\SendRequest; $response = $client->sessions()->online()->send( new SendRequest(...) )->object();
for XML invoice:
use N1ebieski\KSEFClient\Requests\Sessions\Online\Send\SendXmlRequest; $response = $client->sessions()->online()->send( new SendXmlRequest(...) )->object();
Sessions Status
use N1ebieski\KSEFClient\Requests\Sessions\Status\StatusRequest; $response = $client->sessions()->status( new StatusRequest(...) )->object();
Invoices
Invoices Download
use N1ebieski\KSEFClient\Requests\Invoices\Download\DownloadRequest; $response = $client->invoices()->download( new DownloadRequest(...) )->body();
Query
Query Metadata
use N1ebieski\KSEFClient\Requests\Invoices\Query\Metadata\MetadataRequest; $response = $client->invoices()->query()->metadata( new MetadataRequest(...) )->object();
Exports
Exports Init
use N1ebieski\KSEFClient\Requests\Invoices\Exports\Init\InitRequest; $response = $client->invoices()->exports()->init( new InitRequest(...) )->object();
Exports Status
use N1ebieski\KSEFClient\Requests\Invoices\Exports\Status\StatusRequest; $response = $client->invoices()->exports()->status( new StatusRequest(...) )->object();
Certificates
Enrollments
Enrollments Send
use N1ebieski\KSEFClient\Requests\Certificates\Enrollments\Send\SendRequest; $response = $client->certificates()->enrollments()->send( new SendRequest(...) )->object();
Enrollments Status
use N1ebieski\KSEFClient\Requests\Certificates\Enrollments\Status\StatusRequest; $response = $client->certificates()->enrollments()->status( new StatusRequest(...) )->object();
Certificates Retrieve
use N1ebieski\KSEFClient\Requests\Certificates\Retrieve\RetrieveRequest; $response = $client->certificates()->retrieve( new RetrieveRequest(...) )->object();
Certificates Revoke
use N1ebieski\KSEFClient\Requests\Certificates\Revoke\RevokeRequest; $response = $client->certificates()->revoke( new RevokeRequest(...) )->status();
Certificates Query
use N1ebieski\KSEFClient\Requests\Certificates\Query\QueryRequest; $response = $client->certificates()->query( new QueryRequest(...) )->object();
Tokens
Tokens Create
https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Tokeny-KSeF/paths/~1api~1v2~1tokens/post
use N1ebieski\KSEFClient\Requests\Tokens\Create\CreateRequest; $response = $client->tokens()->create( new CreateRequest(...) )->object();
Tokens List
https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Tokeny-KSeF/paths/~1api~1v2~1tokens/get
use N1ebieski\KSEFClient\Requests\Tokens\List\ListRequest; $response = $client->tokens()->list( new ListRequest(...) )->object();
Tokens Status
use N1ebieski\KSEFClient\Requests\Tokens\Status\StatusRequest; $response = $client->tokens()->list( new StatusRequest(...) )->object();
Tokens Revoke
use N1ebieski\KSEFClient\Requests\Tokens\Revoke\RevokeRequest; $response = $client->tokens()->revoke( new RevokeRequest(...) )->status();
Testdata
Person
Person Create
use N1ebieski\KSEFClient\Requests\Testdata\Person\Create\CreateRequest; $response = $client->testdata()->person()->create( new CreateRequest(...) )->status();
Person Remove
use N1ebieski\KSEFClient\Requests\Testdata\Person\Remove\RemoveRequest; $response = $client->testdata()->person()->remove( new RemoveRequest(...) )->status();
Examples
Generate a KSEF certificate and convert to .p12 file
<?php use N1ebieski\KSEFClient\Actions\ConvertCertificateToPkcs12\ConvertCertificateToPkcs12Action; use N1ebieski\KSEFClient\Actions\ConvertCertificateToPkcs12\ConvertCertificateToPkcs12Handler; use N1ebieski\KSEFClient\Actions\ConvertDerToPem\ConvertDerToPemAction; use N1ebieski\KSEFClient\Actions\ConvertDerToPem\ConvertDerToPemHandler; use N1ebieski\KSEFClient\Actions\ConvertPemToDer\ConvertPemToDerAction; use N1ebieski\KSEFClient\Actions\ConvertPemToDer\ConvertPemToDerHandler; use N1ebieski\KSEFClient\ClientBuilder; use N1ebieski\KSEFClient\DTOs\DN; use N1ebieski\KSEFClient\Factories\CSRFactory; use N1ebieski\KSEFClient\Support\Utility; use N1ebieski\KSEFClient\ValueObjects\Certificate; use N1ebieski\KSEFClient\ValueObjects\Mode; use N1ebieski\KSEFClient\ValueObjects\PrivateKeyType; $client = (new ClientBuilder()) ->withMode(Mode::Test) ->withIdentifier('NIP_NUMBER') // To generate the KSEF certificate, you have to authorize the qualified certificate the first time ->withCertificatePath($_ENV['PATH_TO_CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE']) ->build(); $dataResponse = $client->certificates()->enrollments()->data()->json(); $dn = DN::from($dataResponse); // You can choose beetween EC or RSA private key type $csr = CSRFactory::make($dn, PrivateKeyType::EC); $csrToDer = (new ConvertPemToDerHandler())->handle(new ConvertPemToDerAction($csr->raw)); $sendResponse = $client->certificates()->enrollments()->send([ 'certificateName' => 'My first certificate', 'certificateType' => 'Authentication', 'csr' => base64_encode($csrToDer), ])->object(); $statusResponse = Utility::retry(function () use ($client, $sendResponse) { $statusResponse = $client->certificates()->enrollments()->status([ 'referenceNumber' => $sendResponse->referenceNumber ])->object(); if ($statusResponse->status->code === 200) { return $statusResponse; } if ($statusResponse->status->code >= 400) { throw new RuntimeException( $statusResponse->status->description, $statusResponse->status->code ); } }); $retrieveResponse = $client->certificates()->retrieve([ 'certificateSerialNumbers' => [$statusResponse->certificateSerialNumber] ])->object(); $certificate = base64_decode($retrieveResponse->certificates[0]->certificate); $certificateToPem = (new ConvertDerToPemHandler())->handle( new ConvertDerToPemAction($certificate, 'CERTIFICATE') ); $certificateToPkcs12 = (new ConvertCertificateToPkcs12Handler())->handle( new ConvertCertificateToPkcs12Action( certificate: new Certificate($certificateToPem, [], $csr->privateKey), passphrase: 'password' ) ); file_put_contents(Utility::basePath('config/certificates/ksef-certificate.p12'), $certificateToPkcs12);
Send an invoice, check for UPO and generate QR code
<?php use Endroid\QrCode\Builder\Builder as QrCodeBuilder; use Endroid\QrCode\Label\Font\OpenSans; use Endroid\QrCode\RoundBlockSizeMode; use N1ebieski\KSEFClient\Actions\ConvertEcdsaDerToRaw\ConvertEcdsaDerToRawHandler; use N1ebieski\KSEFClient\Actions\GenerateQRCodes\GenerateQRCodesAction; use N1ebieski\KSEFClient\Actions\GenerateQRCodes\GenerateQRCodesHandler; use N1ebieski\KSEFClient\ClientBuilder; use N1ebieski\KSEFClient\DTOs\QRCodes; use N1ebieski\KSEFClient\DTOs\Requests\Sessions\Online\Faktura; use N1ebieski\KSEFClient\Factories\EncryptionKeyFactory; use N1ebieski\KSEFClient\Support\Utility; use N1ebieski\KSEFClient\Testing\Fixtures\Requests\Sessions\Online\Send\SendFakturaSprzedazyTowaruRequestFixture; use N1ebieski\KSEFClient\ValueObjects\Mode; $encryptionKey = EncryptionKeyFactory::makeRandom(); $client = (new ClientBuilder()) ->withMode(Mode::Test) ->withIdentifier('NIP_NUMBER') ->withCertificatePath($_ENV['PATH_TO_CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE']) ->withEncryptionKey($encryptionKey) ->build(); $openResponse = $client->sessions()->online()->open([ 'formCode' => 'FA (3)', ])->object(); $fixture = (new SendFakturaSprzedazyTowaruRequestFixture()) ->withTodayDate() ->withRandomInvoiceNumber(); $sendResponse = $client->sessions()->online()->send([ ...$fixture->data, 'referenceNumber' => $openResponse->referenceNumber, ])->object(); $closeResponse = $client->sessions()->online()->close([ 'referenceNumber' => $openResponse->referenceNumber ]); $statusResponse = Utility::retry(function () use ($client, $openResponse, $sendResponse) { $statusResponse = $client->sessions()->invoices()->status([ 'referenceNumber' => $openResponse->referenceNumber, 'invoiceReferenceNumber' => $sendResponse->referenceNumber ])->object(); if ($statusResponse->status->code === 200) { return $statusResponse; } if ($statusResponse->status->code >= 400) { throw new RuntimeException( $statusResponse->status->description, $statusResponse->status->code ); } }); $upo = $client->sessions()->invoices()->upo([ 'referenceNumber' => $openResponse->referenceNumber, 'invoiceReferenceNumber' => $sendResponse->referenceNumber ])->body(); $faktura = Faktura::from($fixture->getFaktura()); $generateQRCodesHandler = new GenerateQRCodesHandler( qrCodeBuilder: (new QrCodeBuilder()) ->roundBlockSizeMode(RoundBlockSizeMode::Enlarge) ->labelFont(new OpenSans(size: 12)), convertEcdsaDerToRawHandler: new ConvertEcdsaDerToRawHandler() ); /** @var QRCodes $qrCodes */ $qrCodes = $generateQRCodesHandler->handle(GenerateQRCodesAction::from([ 'nip' => $faktura->podmiot1->daneIdentyfikacyjne->nip, 'invoiceCreatedAt' => $faktura->fa->p_1->value, 'document' => $faktura->toXml(), 'ksefNumber' => $statusResponse->ksefNumber ])); // Invoice link file_put_contents(Utility::basePath("var/qr/code1.png"), $qrCodes->code1);
Create an offline invoice and generate both QR codes
<?php use Endroid\QrCode\Builder\Builder as QrCodeBuilder; use Endroid\QrCode\Label\Font\OpenSans; use Endroid\QrCode\RoundBlockSizeMode; use N1ebieski\KSEFClient\Actions\ConvertEcdsaDerToRaw\ConvertEcdsaDerToRawHandler; use N1ebieski\KSEFClient\Actions\GenerateQRCodes\GenerateQRCodesAction; use N1ebieski\KSEFClient\Actions\GenerateQRCodes\GenerateQRCodesHandler; use N1ebieski\KSEFClient\DTOs\QRCodes; use N1ebieski\KSEFClient\DTOs\Requests\Sessions\Online\Faktura; use N1ebieski\KSEFClient\Factories\CertificateFactory; use N1ebieski\KSEFClient\Support\Utility; use N1ebieski\KSEFClient\Testing\Fixtures\Requests\Sessions\Online\Send\SendFakturaSprzedazyTowaruRequestFixture; use N1ebieski\KSEFClient\ValueObjects\CertificatePath; $nip = 'NIP_NUMBER'; // From https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Certyfikaty/paths/~1api~1v2~1certificates~1query/post $certificateSerialNumber = $_ENV['CERTIFICATE_SERIAL_NUMBER']; // Remember: this certificate must be "Offline" type, not "Authentication" $certificate = CertificateFactory::make( CertificatePath::from($_ENV['PATH_TO_CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE']) ); $fixture = (new SendFakturaSprzedazyTowaruRequestFixture()) ->withTodayDate() ->withRandomInvoiceNumber(); $faktura = Faktura::from($fixture->getFaktura()); $generateQRCodesHandler = new GenerateQRCodesHandler( qrCodeBuilder: (new QrCodeBuilder()) ->roundBlockSizeMode(RoundBlockSizeMode::Enlarge) ->labelFont(new OpenSans(size: 12)), convertEcdsaDerToRawHandler: new ConvertEcdsaDerToRawHandler() ); /** @var QRCodes $qrCodes */ $qrCodes = $generateQRCodesHandler->handle(GenerateQRCodesAction::from([ 'nip' => $faktura->podmiot1->daneIdentyfikacyjne->nip, 'invoiceCreatedAt' => $faktura->fa->p_1->value, 'document' => $faktura->toXml(), 'certificate' => $certificate, 'certificateSerialNumber' => $certificateSerialNumber, 'contextIdentifierGroup' => [ 'identifierGroup' => [ 'nip' => $nip ] ] ])); // Invoice link file_put_contents(Utility::basePath("var/qr/code1.png"), $qrCodes->code1); // Certificate verification link file_put_contents(Utility::basePath("var/qr/code2.png"), $qrCodes->code2);
Download and decrypt invoices using the encryption key
<?php use N1ebieski\KSEFClient\Actions\DecryptDocument\DecryptDocumentAction; use N1ebieski\KSEFClient\Actions\DecryptDocument\DecryptDocumentHandler; use N1ebieski\KSEFClient\ClientBuilder; use N1ebieski\KSEFClient\Factories\EncryptionKeyFactory; use N1ebieski\KSEFClient\Support\Utility; use N1ebieski\KSEFClient\ValueObjects\Mode; $encryptionKey = EncryptionKeyFactory::makeRandom(); $client = (new ClientBuilder()) ->withMode(Mode::Test) ->withIdentifier($_ENV['NIP_NUMBER']) ->withCertificatePath($_ENV['PATH_TO_CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE']) ->withEncryptionKey($encryptionKey) ->build(); $initResponse = $client->invoices()->exports()->init([ 'filters' => [ 'subjectType' => 'Subject1', 'dateRange' => [ 'dateType' => 'Invoicing', 'from' => new DateTimeImmutable('-1 day'), 'to' => new DateTimeImmutable() ], ] ])->object(); $statusResponse = Utility::retry(function () use ($client, $initResponse) { $statusResponse = $client->invoices()->exports()->status([ 'operationReferenceNumber' => $initResponse->operationReferenceNumber ])->object(); if ($statusResponse->status->code === 200) { return $statusResponse; } if ($statusResponse->status->code >= 400) { throw new RuntimeException( $statusResponse->status->description, $statusResponse->status->code ); } }); $decryptDocumentHandler = new DecryptDocumentHandler(); // Downloading... foreach ($statusResponse->package->parts as $part) { $contents = file_get_contents($part->url); $contents = $decryptDocumentHandler->handle(new DecryptDocumentAction( document: $contents, encryptionKey: $encryptionKey )); $name = rtrim($part->partName, '.aes'); file_put_contents(Utility::basePath("var/zip/{$name}"), $contents); }
Testing
The package uses unit tests via Pest.
Pest configuration is located in tests/Pest
TestCase is located in tests/AbstractTestCase
Fake request and responses fixtures for resources are located in src/Testing/Fixtures/Requests
Run all tests:
composer install
vendor/bin/pest
Roadmap
- Batch endpoints
- Prepare the package for release candidate
Special thanks
Special thanks to:
- all the helpful people on the 4programmers.net forum
- authors of the repository grafinet/xades-tools for the Xades document signing tool