arekm/ksef-php-client
PHP API client that allows you to interact with the KSEF API Krajowy System e-Faktur
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)
- Support for signatures with both RSA and EC keys
- Support for async batch send multiple invoices
- Logical invoice structure mapped to DTOs and ValueObjects
- Automatic access token refresh
- Automatic XML document validation based on XSD schemas
- CSR (Certificate Signing Request) handling
- KSeF exception handling
- PDF invoice nad UPO generation
- QR codes generation
| KSEF Version | Branch | Release Version |
|---|---|---|
| 2.0 | main | ^0.3 |
| 1.0 | 0.2.x | 0.2.* |
Table of Contents
-
- Auth
- Limits
- Rate Limits
- Security
- Sessions
- Invoices
- Permissions
- Certificates
- Tokens
- Testdata
- Latarnia
-
- Integration with a frontend application using certificate-based authentication
- Conversion of the KSEF certificate and private key from MCU to a .p12 file
- Generate a KSEF certificate and convert to .p12 file
- Send an invoice, check for UPO and generate QR code
- Generate PDF for the invoice and the UPO file
- Generate PDF for the transaction confirmation with both QR codes
- Batch async send multiple invoices and check for UPO
- Create an offline invoice and generate both QR codes
- Generate PDF for the offline invoice file with both QR codes
- Download and decrypt invoices using the encryption key
Get Started
Requires PHP 8.1+
First, install ksef-php-client via the Composer package manager:
composer require n1ebieski/ksef-php-clientEnsure 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/guzzleClient 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
->withLatarniaApiUrl($_ENV['KSEF_LATARNIA_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
->withExceptionHandler(new \ExceptionHandler(...)) // Optional N1ebieski\KSEFClient\Contracts\Exception\ExceptionHandlerInterface implmentation
->withAccessToken($_ENV['ACCESS_TOKEN'], $_ENV['VALID_UNTIL']) // Optional, if present, auto authorization is skipped
->withRefreshToken($_ENV['REFRESH_TOKEN'], $_ENV['VALID_UNTIL']) // Optional, if present, auto refresh access token is enabled
->withKsefToken($_ENV['KSEF_TOKEN']) // Required for API Token authorization. Optional otherwise
->withCertificate($_ENV['CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE']) // Required .p12 contents for Certificate authorization. Optional otherwise
->withCertificatePath($_ENV['PATH_TO_CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE']) // Required path to .p12 file for Certificate authorization. Optional otherwise
->withVerifyCertificateChain(true) // Optional. Explanation https://api-test.ksef.mf.gov.pl/docs/v2/index.html#tag/Uzyskiwanie-dostepu/paths/~1auth~1xades-signature/post
->withEncryptionKey(EncryptionKeyFactory::makeRandom()) // Required for invoice resources. Remember to save this value!
->withIdentifier('NIP_NUMBER') // Required for authorization. Optional otherwise
->withAsyncMaxConcurrency(8) // Optional. Maximum concurrent send operations during asynchronous sending
->withValidateXml(true) // Optional. XML document validation based on XSD schemas
->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 resourcesor:
use N1ebieski\KSEFClient\ClientBuilder;
$client = (new ClientBuilder())
->withCertificate($_ENV['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 resourcesResources
Auth
Auth Challenge
$response = $client->auth()->challenge()->object();
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();Auth Token
Auth Token Redeem
$response = $client->auth()->token()->redeem()->object();
Auth Token Refresh
$response = $client->auth()->token()->refresh()->object();Auth Sessions
Auth Sessions list
https://api-test.ksef.mf.gov.pl/docs/v2/index.html#tag/Aktywne-sesje/paths/~1auth~1sessions/get
use N1ebieski\KSEFClient\Requests\Auth\Sessions\List\ListRequest;
$response = $client->auth()->sessions()->list(
new ListRequest(...)
)->object();
Auth Sessions revoke current
$response = $client->auth()->sessions()->revokeCurrent()->status();
Auth Sessions revoke
use N1ebieski\KSEFClient\Requests\Auth\Sessions\Revoke\RevokeRequest;
$response = $client->auth()->sessions()->revoke(
new RevokeRequest(...)
)->status();Limits
Limits Context
use N1ebieski\KSEFClient\Requests\Limits\Context\ContextRequest;
$response = $client->limits()->context(
new ContextRequest(...)
)->object();
Limits Subject
use N1ebieski\KSEFClient\Requests\Limits\Subject\SubjectRequest;
$response = $client->limits()->subject(
new SubjectRequest(...)
)->object();
Rate Limits
https://api-test.ksef.mf.gov.pl/docs/v2/index.html#tag/Limity-i-ograniczenia/paths/~1rate-limits/get
$response = $client->rateLimits()->object();Security
Security Public Key Certificates
$response = $client->security()->publicKeyCertificates()->object();Sessions
Sessions List
https://api-test.ksef.mf.gov.pl/docs/v2/index.html#tag/Status-wysylki-i-UPO/paths/~1sessions/get
use N1ebieski\KSEFClient\Requests\Sessions\List\ListRequest;
$response = $client->sessions()->list(
new ListRequest(...)
)->object();Sessions Invoices
Sessions Invoices List
use N1ebieski\KSEFClient\Requests\Sessions\Invoices\List\ListRequest;
$response = $client->sessions()->invoices()->list(
new ListRequest(...)
)->object();
Sessions Invoices Failed
use N1ebieski\KSEFClient\Requests\Sessions\Invoices\Failed\FailedRequest;
$response = $client->sessions()->invoices()->failed(
new FailedRequest(...)
)->object();
Sessions Invoices Upo
use N1ebieski\KSEFClient\Requests\Sessions\Invoices\Upo\UpoRequest;
$response = $client->sessions()->invoices()->upo(
new UpoRequest(...)
)->body();
Sessions Invoices Ksef Upo
use N1ebieski\KSEFClient\Requests\Sessions\Invoices\KsefUpo\KsefUpoRequest;
$response = $client->sessions()->invoices()->ksefUpo(
new KsefUpoRequest(...)
)->body();
Sessions Invoices Status
use N1ebieski\KSEFClient\Requests\Sessions\Invoices\Status\StatusRequest;
$response = $client->sessions()->invoices()->status(
new StatusRequest(...)
)->object();Sessions Online
Sessions Online Open
use N1ebieski\KSEFClient\Requests\Sessions\Online\Open\OpenRequest;
$response = $client->sessions()->online()->open(
new OpenRequest(...)
)->object();
Sessions Online Close
use N1ebieski\KSEFClient\Requests\Sessions\Online\Close\CloseRequest;
$response = $client->sessions()->online()->close(
new CloseRequest(...)
)->status();
Sessions Online Send invoices
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 Batch
Sessions Batch Open (and send multiple invoices)
https://api-test.ksef.mf.gov.pl/docs/v2/index.html#tag/Wysylka-wsadowa/paths/~1sessions~1batch/post
for DTOs invoices:
use N1ebieski\KSEFClient\Requests\Sessions\Batch\OpenAndSend\OpenAndSendRequest;
$response = $client->sessions()->batch()->openAndSend(
new OpenAndSendRequest(...)
)->object();for XMLs invoices:
use N1ebieski\KSEFClient\Requests\Sessions\Batch\OpenAndSend\OpenAndSendXmlRequest;
$response = $client->sessions()->batch()->openAndSend(
new OpenAndSendXmlRequest(...)
)->object();for ZIP invoices:
use N1ebieski\KSEFClient\Requests\Sessions\Batch\OpenAndSend\OpenAndSendZipRequest;
$response = $client->sessions()->batch()->openAndSend(
new OpenAndSendZipRequest(...)
)->object();
Sessions Batch Close
use N1ebieski\KSEFClient\Requests\Sessions\Batch\Close\CloseRequest;
$response = $client->sessions()->batch()->close(
new CloseRequest(...)
)->status();
Sessions Status
use N1ebieski\KSEFClient\Requests\Sessions\Status\StatusRequest;
$response = $client->sessions()->status(
new StatusRequest(...)
)->object();
Sessions Upo
use N1ebieski\KSEFClient\Requests\Sessions\Upo\UpoRequest;
$response = $client->sessions()->upo(
new UpoRequest(...)
)->body();Invoices
Invoices Download
use N1ebieski\KSEFClient\Requests\Invoices\Download\DownloadRequest;
$response = $client->invoices()->download(
new DownloadRequest(...)
)->body();Invoices Query
Invoices Query Metadata
use N1ebieski\KSEFClient\Requests\Invoices\Query\Metadata\MetadataRequest;
$response = $client->invoices()->query()->metadata(
new MetadataRequest(...)
)->object();Invoices Exports
Invoices Exports Init
use N1ebieski\KSEFClient\Requests\Invoices\Exports\Init\InitRequest;
$response = $client->invoices()->exports()->init(
new InitRequest(...)
)->object();
Invoices Exports Status
use N1ebieski\KSEFClient\Requests\Invoices\Exports\Status\StatusRequest;
$response = $client->invoices()->exports()->status(
new StatusRequest(...)
)->object();Permissions
Permissions Persons
Permissions Persons Grants
use N1ebieski\KSEFClient\Requests\Permissions\Persons\Grants\GrantsRequest;
$response = $client->permissions()->persons()->grants(
new GrantsRequest(...)
)->object();Permissions Entities
Permissions Entities Grants
use N1ebieski\KSEFClient\Requests\Permissions\Entities\Grants\GrantsRequest;
$response = $client->permissions()->entities()->grants(
new GrantsRequest(...)
)->object();Permissions Authorizations
Permissions Authorizations Grants
use N1ebieski\KSEFClient\Requests\Permissions\Authorizations\Grants\GrantsRequest;
$response = $client->permissions()->authorizations()->grants(
new GrantsRequest(...)
)->object();
Permissions Authorizations Grants Revoke
use N1ebieski\KSEFClient\Requests\Permissions\Authorizations\Revoke\RevokeRequest;
$response = $client->permissions()->authorizations()->revoke(
new RevokeRequest(...)
)->object();Permissions Indirect
Permissions Indirect Grants
use N1ebieski\KSEFClient\Requests\Permissions\Indirect\Grants\GrantsRequest;
$response = $client->permissions()->indirect()->grants(
new GrantsRequest(...)
)->object();Permissions Subunits
Permissions Subunits Grants
use N1ebieski\KSEFClient\Requests\Permissions\Subunits\Grants\GrantsRequest;
$response = $client->permissions()->subunits()->grants(
new GrantsRequest(...)
)->object();Permissions EuEntities
Permissions EuEntities Grants
use N1ebieski\KSEFClient\Requests\Permissions\EuEntities\Grants\GrantsRequest;
$response = $client->permissions()->euEntities()->grants(
new GrantsRequest(...)
)->object();Permissions EuEntities Administration
Permissions EuEntities Administration Grants
use N1ebieski\KSEFClient\Requests\Permissions\EuEntities\Administration\Grants\GrantsRequest;
$response = $client->permissions()->euEntities()->administration()->grants(
new GrantsRequest(...)
)->object();Permissions Common
Permissions Common Grants Revoke
use N1ebieski\KSEFClient\Requests\Permissions\Common\Revoke\RevokeRequest;
$response = $client->permissions()->common()->revoke(
new RevokeRequest(...)
)->object();Permissions Query
Permissions Query Personal
Permissions Query Personal Grants
use N1ebieski\KSEFClient\Requests\Permissions\Query\Personal\Grants\GrantsRequest;
$response = $client->permissions()->query()->personal()->grants(
new GrantsRequest(...)
)->object();Permissions Query Subunits
Permissions Query Subunits Grants
use N1ebieski\KSEFClient\Requests\Permissions\Query\Subunits\Grants\GrantsRequest;
$response = $client->permissions()->query()->subunits()->grants(
new GrantsRequest(...)
)->object();Permissions Operations
Permissions Operations Status
use N1ebieski\KSEFClient\Requests\Permissions\Operations\Status\StatusRequest;
$response = $client->permissions()->operations()->status(
new StatusRequest(...)
)->object();Permissions Attachments
Permissions Attachments Status
use N1ebieski\KSEFClient\Requests\Permissions\Attachments\Status\StatusRequest;
$response = $client->permissions()->attachments()->status(
new StatusRequest(...)
)->object();Certificates
Certificates Limits
https://api-test.ksef.mf.gov.pl/docs/v2/index.html#tag/Certyfikaty/paths/~1certificates~1limits/get
$response = $client->certificates()->limits()->object();Certificates Enrollments
Certificates Enrollments Data
$response = $client->certificates()->enrollments()->data()->object();
Certificates Enrollments Send
use N1ebieski\KSEFClient\Requests\Certificates\Enrollments\Send\SendRequest;
$response = $client->certificates()->enrollments()->send(
new SendRequest(...)
)->object();
Certificates 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
https://api-test.ksef.mf.gov.pl/docs/v2/index.html#tag/Certyfikaty/paths/~1certificates~1query/post
use N1ebieski\KSEFClient\Requests\Certificates\Query\QueryRequest;
$response = $client->certificates()->query(
new QueryRequest(...)
)->object();Tokens
Tokens Create
https://api-test.ksef.mf.gov.pl/docs/v2/index.html#tag/Tokeny-KSeF/paths/~1tokens/post
use N1ebieski\KSEFClient\Requests\Tokens\Create\CreateRequest;
$response = $client->tokens()->create(
new CreateRequest(...)
)->object();
Tokens List
https://api-test.ksef.mf.gov.pl/docs/v2/index.html#tag/Tokeny-KSeF/paths/~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
Testdata Attachment
Testdata Attachment Approve
use N1ebieski\KSEFClient\Requests\Testdata\Attachment\Grant\ApproveRequest;
$response = $client->testdata()->attachment()->approve(
new ApproveRequest(...)
)->status();
Testdata Attachment Revoke
use N1ebieski\KSEFClient\Requests\Testdata\Attachment\Revoke\RevokeRequest;
$response = $client->testdata()->attachment()->revoke(
new RevokeRequest(...)
)->status();Testdata Person
Testdata Person Create
https://api-test.ksef.mf.gov.pl/docs/v2/index.html#tag/Dane-testowe/paths/~1testdata~1person/post
use N1ebieski\KSEFClient\Requests\Testdata\Person\Create\CreateRequest;
$response = $client->testdata()->person()->create(
new CreateRequest(...)
)->status();
Testdata Person Remove
use N1ebieski\KSEFClient\Requests\Testdata\Person\Remove\RemoveRequest;
$response = $client->testdata()->person()->remove(
new RemoveRequest(...)
)->status();Testdata Context
Testdata Context Block
use N1ebieski\KSEFClient\Requests\Testdata\Context\Block\BlockRequest;
$response = $client->testdata()->context()->block(
new BlockRequest(...)
)->status();
Testdata Context Unblock
use N1ebieski\KSEFClient\Requests\Testdata\Context\Unblock\UnblockRequest;
$response = $client->testdata()->context()->unblock(
new UnblockRequest(...)
)->status();Testdata Limits
Testdata Limits Context
Testdata Limits Context Session
Testdata Limits Context Session Limits
use N1ebieski\KSEFClient\Requests\Testdata\Limits\Context\Session\Limits\LimitsRequest;
$response = $client->testdata()->limits()->context()->session()->limits(
new LimitsRequest(...)
)->status();
Testdata Limits Context Session Reset
use N1ebieski\KSEFClient\Requests\Testdata\Limits\Context\Session\Reset\ResetRequest;
$response = $client->testdata()->limits()->context()->session()->reset(
new ResetRequest(...)
)->status();Testdata Limits Subject
Testdata Limits Subject Certificate
Testdata Limits Subject Certificate Limits
use N1ebieski\KSEFClient\Requests\Testdata\Limits\Subject\Certificate\Limits\LimitsRequest;
$response = $client->testdata()->limits()->subject()->certificate()->limits(
new LimitsRequest(...)
)->status();
Testdata Limits Subject Certificate Reset
use N1ebieski\KSEFClient\Requests\Testdata\Limits\Subject\Certificate\Reset\ResetRequest;
$response = $client->testdata()->limits()->subject()->certificate()->reset(
new ResetRequest(...)
)->status();Testdata Rate Limits
Testdata Rate Limits Limits
use N1ebieski\KSEFClient\Requests\Testdata\RateLimits\Limits\LimitsRequest;
$response = $client->testdata()->rateLimits()->limits(
new LimitsRequest(...)
)->status();
Testdata Rate Limits Reset
use N1ebieski\KSEFClient\Requests\Testdata\RateLimits\Reset\ResetRequest;
$response = $client->testdata()->rateLimits()->reset(
new ResetRequest(...)
)->status();
Testdata Rate Limits Production
use N1ebieski\KSEFClient\Requests\Testdata\RateLimits\Production\ProductionRequest;
$response = $client->testdata()->rateLimits()->production(
new ProductionRequest(...)
)->status();Latarnia
Latarnia Status
https://github.com/CIRFMF/ksef-latarnia/blob/main/scenariusze.md
$response = $client->latarnia()->status()->object();
Latarnia Messages
https://github.com/CIRFMF/ksef-latarnia/blob/main/scenariusze.md
$response = $client->latarnia()->messages()->object();Examples
Integration with a frontend application using certificate-based authentication
https://github.com/N1ebieski/ksef-app-example.test
Conversion of the KSEF certificate and private key from MCU to a .p12 file
use N1ebieski\KSEFClient\Actions\ConvertCertificateToPkcs12\ConvertCertificateToPkcs12Action;
use N1ebieski\KSEFClient\Actions\ConvertCertificateToPkcs12\ConvertCertificateToPkcs12Handler;
use N1ebieski\KSEFClient\Support\Utility;
use N1ebieski\KSEFClient\Factories\CertificateFactory;
$certificate = file_get_contents(Utility::basePath('config/certificates/certificate.crt'));
$privateKey = file_get_contents(Utility::basePath('config/certificates/privateKey.key'));
$certificateToPkcs12 = (new ConvertCertificateToPkcs12Handler())->handle(
new ConvertCertificateToPkcs12Action(
certificate: CertificateFactory::makeFromPkcs8($certificate, $privateKey, 'password'),
passphrase: 'password'
)
);
file_put_contents(Utility::basePath('config/certificates/ksef-certificate.p12'), $certificateToPkcs12);
Generate a KSEF certificate and convert to a .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\Factories\CertificateFactory;
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: CertificateFactory::makeFromPkcs8($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\Faktura;
use N1ebieski\KSEFClient\Factories\EncryptionKeyFactory;
use N1ebieski\KSEFClient\Support\Utility;
use N1ebieski\KSEFClient\Testing\Fixtures\DTOs\Requests\Sessions\FakturaSprzedazyTowaruFixture;
use N1ebieski\KSEFClient\ValueObjects\Mode;
use N1ebieski\KSEFClient\ValueObjects\Requests\KsefNumber;
$encryptionKey = EncryptionKeyFactory::makeRandom();
$nip = 'NIP_NUMBER';
$client = (new ClientBuilder())
->withMode(Mode::Test)
->withIdentifier($nip)
->withCertificatePath($_ENV['PATH_TO_CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE'])
->withEncryptionKey($encryptionKey)
->build();
$openResponse = $client->sessions()->online()->open([
'formCode' => 'FA (3)',
])->object();
$fakturaFixture = (new FakturaSprzedazyTowaruFixture())
->withRandomInvoiceNumber()
->withNip($nip)
->withTodayDate();
$faktura = Faktura::from($fakturaFixture->data);
// For sending invoice as DTO use SendRequest or array
// For sending invoice as XML use SendXmlRequest
$sendResponse = $client->sessions()->online()->send([
'faktura' => $faktura,
'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();
$generateQRCodesHandler = new GenerateQRCodesHandler(
qrCodeBuilder: (new QrCodeBuilder())
->roundBlockSizeMode(RoundBlockSizeMode::Enlarge)
->labelFont(new OpenSans(size: 12)),
convertEcdsaDerToRawHandler: new ConvertEcdsaDerToRawHandler()
);
$ksefNumber = KsefNumber::from($statusResponse->ksefNumber);
// For generating QR code by document use GenerateQRCodesAction
// For generating QR code by invoice hash use GenerateQRCodesByInvoiceHashAction
/** @var QRCodes $qrCodes */
$qrCodes = $generateQRCodesHandler->handle(new GenerateQRCodesAction(
mode: Mode::Test,
nip: $faktura->podmiot1->daneIdentyfikacyjne->nip,
invoiceCreatedAt: $faktura->fa->p_1->value,
document: $faktura->toXml(),
ksefNumber: $ksefNumber
));
// Invoice link
file_put_contents(Utility::basePath("var/qr/code1.png"), $qrCodes->code1->raw);
Generate PDF for the invoice and the UPO file
PDF invoice example.pdf
UPO-example.pdf
Install n1ebieski/ksef-pdf-generator
use N1ebieski\KSEFClient\Actions\GeneratePDF\GeneratePDFAction;
use N1ebieski\KSEFClient\Actions\GeneratePDF\GeneratePDFHandler;
use N1ebieski\KSEFClient\Support\Utility;
use N1ebieski\KSEFClient\ValueObjects\KsefFeInvoiceConverterPath;
// Send an invoice using example https://github.com/N1ebieski/ksef-php-client?tab=readme-ov-file#send-an-invoice-check-for-upo-and-generate-qr-code
// and then...
$ksefFeInvoiceConverterPath = KsefFeInvoiceConverterPath::from(Utility::basePath('../ksef-fe-invoice-converter/dist/cli/index.js'));
$pdfs = (new GeneratePDFHandler())->handle(new GeneratePDFAction(
ksefFeInvoiceConverterPath: $ksefFeInvoiceConverterPath,
invoiceDocument $faktura->toXml(),
upoDocument: $upo,
qrCodes: $qrCodes,
ksefNumber: $ksefNumber
));
file_put_contents(Utility::basePath("var/pdf/{$ksefNumber->value}.pdf"), $pdfs->invoice);
file_put_contents(Utility::basePath("var/pdf/UPO-{$sendResponse->referenceNumber}.pdf"), $pdfs->upo);
Generate PDF for the transaction confirmation with both QR codes
Read https://ksef.podatki.gov.pl/informacje-ogolne-ksef-20/potwierdzenie-transakcji/
Install n1ebieski/ksef-pdf-generator
use N1ebieski\KSEFClient\Actions\GeneratePDF\GeneratePDFAction;
use N1ebieski\KSEFClient\Actions\GeneratePDF\GeneratePDFHandler;
use N1ebieski\KSEFClient\Support\Utility;
use N1ebieski\KSEFClient\ValueObjects\KsefFeInvoiceConverterPath;
// Create an online invoice using example https://github.com/N1ebieski/ksef-php-client?tab=readme-ov-file#send-an-invoice-check-for-upo-and-generate-qr-code
// and then...
$ksefFeInvoiceConverterPath = KsefFeInvoiceConverterPath::from(Utility::basePath('../ksef-fe-invoice-converter/dist/cli/index.js'));
$pdfs = (new GeneratePDFHandler())->handle(new GeneratePDFAction(
ksefFeInvoiceConverterPath: $ksefFeInvoiceConverterPath,
confirmationDocument: $faktura->toXml(),
qrCodes: $qrCodes
));
file_put_contents(Utility::basePath("var/pdf/CONFIRMATION-{$faktura->fa->p_2->value}.pdf"), $pdfs->confirmation);
Batch async send multiple invoices and check for UPO
<?php
use N1ebieski\KSEFClient\ClientBuilder;
use N1ebieski\KSEFClient\Factories\EncryptionKeyFactory;
use N1ebieski\KSEFClient\Support\Utility;
use N1ebieski\KSEFClient\Testing\Fixtures\DTOs\Requests\Sessions\FakturaSprzedazyTowaruFixture;
use N1ebieski\KSEFClient\ValueObjects\Mode;
$encryptionKey = EncryptionKeyFactory::makeRandom();
$nip = 'NIP_NUMBER';
$client = (new ClientBuilder())
->withMode(Mode::Test)
->withIdentifier($nip)
->withCertificatePath($_ENV['PATH_TO_CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE'])
->withEncryptionKey($encryptionKey)
->build();
$faktury = array_map(
fn () => (new FakturaSprzedazyTowaruFixture())
->withTodayDate()
->withNip($nip)
->withRandomInvoiceNumber()
->data,
range(1, 100)
);
// For sending invoices as DTOs use OpenAndSendRequest or array
// For sending invoices as XMLs use OpenAndSendXmlRequest
// For sending invoices as ZIP use OpenAndSendZipRequest
$openAndSendResponse = $client->sessions()->batch()->openAndSend([
'formCode' => 'FA (3)',
'faktury' => $faktury
]);
$openResponse = $openAndSendResponse->object();
$partUploadResponses = $openAndSendResponse->partUploadResponses;
$client->sessions()->batch()->close([
'referenceNumber' => $openResponse->referenceNumber
]);
$statusResponse = Utility::retry(function () use ($client, $openResponse) {
$statusResponse = $client->sessions()->status([
'referenceNumber' => $openResponse->referenceNumber,
])->object();
if ($statusResponse->status->code === 200) {
return $statusResponse;
}
if ($statusResponse->status->code >= 400) {
throw new RuntimeException(
$statusResponse->status->description,
$statusResponse->status->code
);
}
});
$upo = file_get_contents($statusResponse->upo->pages[0]->downloadUrl);
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\Auth\ContextIdentifierGroup;
use N1ebieski\KSEFClient\DTOs\Requests\Sessions\Faktura;
use N1ebieski\KSEFClient\Factories\CertificateFactory;
use N1ebieski\KSEFClient\Support\Utility;
use N1ebieski\KSEFClient\Testing\Fixtures\DTOs\Requests\Sessions\FakturaSprzedazyTowaruFixture;
use N1ebieski\KSEFClient\ValueObjects\CertificatePath;
use N1ebieski\KSEFClient\ValueObjects\CertificateSerialNumber;
use N1ebieski\KSEFClient\ValueObjects\Mode;
use N1ebieski\KSEFClient\ValueObjects\NIP;
$nip = 'NIP_NUMBER';
// From https://api-test.ksef.mf.gov.pl/docs/v2/index.html#tag/Certyfikaty/paths/~1certificates~1query/post
$certificateSerialNumber = CertificateSerialNumber::from($_ENV['CERTIFICATE_SERIAL_NUMBER']);
// Remember: this certificate must be "Offline" type, not "Authentication"
$certificate = CertificateFactory::makeFromCertificatePath(
CertificatePath::from($_ENV['PATH_TO_CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE'])
);
$fakturaFixture = (new FakturaSprzedazyTowaruFixture())
->withTodayDate()
->withNip($nip)
->withRandomInvoiceNumber();
$faktura = Faktura::from($fakturaFixture->data);
$generateQRCodesHandler = new GenerateQRCodesHandler(
qrCodeBuilder: (new QrCodeBuilder())
->roundBlockSizeMode(RoundBlockSizeMode::Enlarge)
->labelFont(new OpenSans(size: 12)),
convertEcdsaDerToRawHandler: new ConvertEcdsaDerToRawHandler()
);
$contextIdentifierGroup = ContextIdentifierGroup::fromIdentifier(NIP::from($nip));
// For generating QR codes by document use GenerateQRCodesAction
// For generating QR codes by invoice hash use GenerateQRCodesByInvoiceHashAction
/** @var QRCodes $qrCodes */
$qrCodes = $generateQRCodesHandler->handle(new GenerateQRCodesAction(
mode: Mode::Test,
nip: $faktura->podmiot1->daneIdentyfikacyjne->nip,
invoiceCreatedAt: $faktura->fa->p_1->value,
document: $faktura->toXml(),
certificate: $certificate,
certificateSerialNumber: $certificateSerialNumber,
contextIdentifierGroup: $contextIdentifierGroup
));
// Invoice link
file_put_contents(Utility::basePath("var/qr/code1.png"), $qrCodes->code1->raw);
// Certificate verification link
file_put_contents(Utility::basePath("var/qr/code2.png"), $qrCodes->code2->raw);
Generate PDF for the offline invoice file with both QR codes
PDF offline invoice EC certificate example.pdf
PDF offline invoice RSA certificate example.pdf
Install n1ebieski/ksef-pdf-generator
use N1ebieski\KSEFClient\Actions\GeneratePDF\GeneratePDFAction;
use N1ebieski\KSEFClient\Actions\GeneratePDF\GeneratePDFHandler;
use N1ebieski\KSEFClient\Support\Utility;
use N1ebieski\KSEFClient\ValueObjects\KsefFeInvoiceConverterPath;
// Create an offline invoice using example https://github.com/N1ebieski/ksef-php-client?tab=readme-ov-file#create-an-offline-invoice-and-generate-both-qr-codes
// and then...
$ksefFeInvoiceConverterPath = KsefFeInvoiceConverterPath::from(Utility::basePath('../ksef-fe-invoice-converter/dist/cli/index.js'));
$pdfs = (new GeneratePDFHandler())->handle(new GeneratePDFAction(
ksefFeInvoiceConverterPath: $ksefFeInvoiceConverterPath,
invoiceDocument: $faktura->toXml(),
qrCodes: $qrCodes
));
file_put_contents(Utility::basePath("var/pdf/{$faktura->fa->p_2->value}.pdf"), $pdfs->invoice);
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([
'referenceNumber' => $initResponse->referenceNumber
])->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();
$zipContents = '';
// Downloading...
foreach ($statusResponse->package->parts as $part) {
$contents = file_get_contents($part->url);
$contents = $decryptDocumentHandler->handle(new DecryptDocumentAction(
document: $contents,
encryptionKey: $encryptionKey
));
$zipContents .= $contents;
}
file_put_contents(Utility::basePath("var/zip/invoices.zip"), $zipContents);
var_dump($statusResponse);Testing
The package uses unit and feature tests via Pest.
Pest configuration is located in tests/Pest
Fake request and responses fixtures for resources are located in src/Testing/Fixtures/Requests
Run all tests:
composer installvendor/bin/pest --parallelRoadmap
- Implementation of other 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
- Łukasz Wojtanowski - author of a modified version of the official ksef-pdf-generator, available at lukasz-wojtanowski-softvig/ksef-pdf-generator, which adds support for generating invoice and UPO PDFs via CLI
