Search by

beziworld / laravel-edoreczenia

kamcw

Laravel client for Polish e-Doręczenia: UA API mailbox (send, receive, attachments, evidences), BAE address search, signed JWT auth, poller and events

Package info

github.com/Beziworld/laravel-edoreczenia

Homepage

Issues

pkg:composer/beziworld/laravel-edoreczenia

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.0 2026-11-04 18:26 UTC

This package is auto-updated.

Last update: 2026-11-04 18:26:33 UTC


README

tests Latest Version License

Laravel client for e-Doręczenia, the Polish registered electronic delivery service (KSDE). Connect a mailbox (skrzynka do doręczeń elektronicznych) of your company or institution to a Laravel application as an external system, the same way EZD document management systems do:

  • signed JWT client assertion (RFC 7523) with the certificate key of the system, token caching, INT and PROD environments
  • many mailboxes: named connections, EDoreczenia::connection('name') and EDoreczenia::using([...]) for tenants
  • User Agent API 3: send messages with attachments to electronic delivery addresses (ADE), sending tasks, list and read received messages, notices of arrival, mark as read, delete, drafts, attachments up to 500 MB
  • evidences: lists per message and per mailbox, signed evidence files (A.1 ... E.2, proofs of sending and receipt BP.W*/BP.O*, hybrid H.PN/H.EPO), technical evidences and message archives as ZIP
  • Search Engine API 4: search the electronic address database (BAE) by NIP, KRS, REGON, name and postal address, verify up to 1000 addresses, read the data of your own mailbox
  • edoreczenia:poll for the scheduler with cursors and the MessageReceived and EvidenceReceived events
  • edoreczenia notification channel for Laravel notifications
  • push notification subscriptions, mailbox settings and a generic request() for every other operation
  • validation of the documented rules (recipients, text length, attachment names, mass and hybrid modes) before sending

Polska wersja dokumentacji

Why this package

The only other PHP package on Packagist, b44x/edoreczenia, is a framework-agnostic PSR-18 SDK. This package is built for Laravel: the HTTP client (Http::fake() works in your tests), the cache for tokens and cursors, a manager with named and runtime connections, events and a scheduler-ready poller, a notification channel, a facade with full @method annotations and configuration through .env.

Before you start

e-Doręczenia has no public sandbox. You need:

  1. An electronic delivery address (ADE) of the entity, for example AE:PL-12345-67890-ABCDE-12.
  2. The external system added to the mailbox in the permission module (Moduł uprawnień) by the mailbox administrator: generate an RSA key pair and a CSR, the administrator uploads the CSR and gives the system a name. In the production environment non-public entities need a commercially obtained qualified certificate; one certificate serves one system.
  3. For the integration environment (INT): an application to the Ministry of Digital Affairs (test.edoreczenia@cyfra.gov.pl, forms on gov.pl), with the public IP addresses of your servers. INT is reachable only from whitelisted addresses.

The host clock must be synchronised (NTP): the authorisation server checks iat, nbf and exp of the assertion.

Installation

composer require beziworld/laravel-edoreczenia
php artisan vendor:publish --tag=edoreczenia-config   # optional
EDORECZENIA_ENVIRONMENT=int                      # int or prod
EDORECZENIA_ADDRESS=AE:PL-12345-67890-ABCDE-12   # ADE of the mailbox
EDORECZENIA_SYSTEM_NAME=EZD                      # name given in the permission module
EDORECZENIA_PRIVATE_KEY=/path/to/system-key.pem  # or the PEM itself (\n escapes are accepted)
EDORECZENIA_PRIVATE_KEY_PASSPHRASE=
EDORECZENIA_POLL_INITIAL=all                     # all or now, see "Polling"
EDORECZENIA_POLL_CACHE_STORE=redis               # persistent store for poller cursors

The package signs {"iss": "<ADE>.SYSTEM.<name>", "sub": same, "aud": "<realm URL>", "iat", "nbf", "exp", "jti"} with RS256 and posts it to the token endpoint with ?login_hint=ADE.<ADE>. The 5-minute access token is cached and reused until shortly before it expires.

INT PROD
Token endpoint int-ow.edoreczenia.gov.pl/auth/realms/EDOR ow.edoreczenia.gov.pl/auth/realms/EDOR
UA API (mailbox) uaapi-int-ow.poczta-polska.pl/api/v3 uaapi-ow.poczta-polska.pl/api/v3
SE API (address search) int-ow.edoreczenia.gov.pl/api/se/v4 ow.edoreczenia.gov.pl/api/se/v4

Every endpoint can be overridden (EDORECZENIA_TOKEN_URL, EDORECZENIA_AUDIENCE, EDORECZENIA_UA_URL, EDORECZENIA_SE_URL).

Sending messages

use Beziworld\EDoreczenia\Facades\EDoreczenia;
use Beziworld\EDoreczenia\Messages\OutgoingMessage;

$result = EDoreczenia::send(
    OutgoingMessage::create('Faktura FV/1/2026')
        ->toRecipient('AE:PL-98765-43210-SFVYC-19', companyName: 'Przykładowa Spółka')
        ->text('W załączeniu faktura.')
        ->caseId('FV/1/2026')
        ->attachFile(storage_path('invoices/fv-1-2026.pdf'))
);

$result->messageTaskId;   // sending is asynchronous (HTTP 202)

EDoreczenia::sendTaskStatus($result->messageTaskId);   // MessageTaskStatus::Pending / Finished
foreach (EDoreczenia::sendTask($result->messageTaskId) as $sent) {
    $sent->messageId;             // id of the message created for the recipient
    $sent->succeeded();           // false when the recipient could not be served
    $sent->errorDescription;
}

Replies keep the thread: OutgoingMessage::replyTo($message)->text('...'). Up to 15 recipients are sent in the standard mode; 16 to 1000 recipients use the mass mode, which requires exactly one attachment of at most 15 MB. Attachment names may have up to 128 characters, without spaces and ~"#%&*:<>?!/\{|}, with a lower-case extension. The text body has at most 5000 characters. A violation throws InvalidMessage before anything is sent.

Hybrid messages (printed and delivered by post): ->toPostalAddress(new PostalAddress(...)), ->fromAddress(...) and ->hybrid([...HybridShipment options...]); only PDF attachments are accepted.

Drafts: createDraft(), updateDraft(), addDraftAttachment() (Base64), uploadDraftAttachment() (multipart, for large files), deleteDraftAttachment(), sendDraft(), drafts(), draft(), deleteDrafts().

Receiving messages

use Beziworld\EDoreczenia\Enums\MessageFormat;

$list = EDoreczenia::inbox(limit: 50, filters: ['opened' => false]);   // metadata, does not open messages
$list->total;              // matching messages
$list->electronicCount;    // messages still waiting in the delivery service

foreach ($list->messages as $message) {
    $message->id;
    $message->subject;
    $message->from?->displayName();
    $message->createdAt;        // CarbonImmutable, Europe/Warsaw
    $message->isNotice();       // notice of arrival (Stub), daysToReceive until deemed delivery
    $message->messageStatus();  // MessageStatus::Delivered, ...
}

$message = EDoreczenia::message($id, MessageFormat::FullExtended);   // content with attachments
$message->textBody;
$message->attachments[0]->filename;
$message->attachments[0]->content;   // decoded binary

EDoreczenia::downloadAttachment($id, $attachmentId)->saveTo('/tmp/file.pdf');   // binary, for files above 15 MB
EDoreczenia::markAsRead([$id1, $id2]);
EDoreczenia::deleteMessages([$id1, $id2]);

Reading is a legal act. There is no separate "mark as read" call: the mailbox sets opened when a message is read in the Full or FullExtended format. Reading a notice of arrival also transfers the message from the delivery service and issues evidence E.1 (delivery). markAsRead() therefore reads the messages in the Full format. Use MessageFormat::Metadata to inspect messages without opening them. When the transfer of a notice takes longer than a few seconds, the API answers HTTP 202 and the client throws MessagePending: repeat the request later.

Evidences

foreach (EDoreczenia::evidences($messageId) as $evidence) {
    $evidence->evidenceType();   // EvidenceType::ProofOfReceiptPdf (BP.OP), ContentHandover (E.1), ...
    $evidence->eventAt;
}

$file = EDoreczenia::evidenceFile($evidence);               // signed XML or PDF; puh/purde chosen from the type
EDoreczenia::evidenceFile($hpn, pdf: true);                 // PDF visualisation of H.PN
EDoreczenia::hybridDeliveryConfirmation($messageId);        // H.EPO
EDoreczenia::mailboxEvidences(downloaded: false);           // evidences of every message in the mailbox
EDoreczenia::technicalEvidences($messageId);                // ZIP
EDoreczenia::messageArchive($messageId);                    // ZIP with visualisation, text, attachments, evidences

The proofs of sending and receipt (BP.WX/BP.WP and BP.OX/BP.OP) replace the "UPD/UPN" confirmations known from ePUAP.

Address search (BAE)

$search = EDoreczenia::search();

$result = $search->byNip('525-224-84-81');           // public institutions, companies and organisations
$search->byKrs('28860');
$search->byRegon('012100784');
$search->byNameAndAddress('Przykładowa Spółka', ['city' => 'Warszawa', 'buildingNumber' => '12A', 'postalCode' => '00-001']);
$search->byPersonAndAddress('Jan', 'Kowalski', ['city' => 'Kraków', 'buildingNumber' => '1']);   // public senders only
$search->byAddresses(['AE:PL-98765-43210-SFVYC-19']);
$search->verifyAddresses($upTo1000Addresses);         // status and blocking warnings
$search->ownData();                                   // data of your mailbox

$entry = $result->first();
$entry->address;               // AE:PL-...
$entry->isActive();
$entry->isBlocked();           // BLOCK_WARNING: do not send
$entry->entity()?->displayName();
$entry->entity()?->nip();

The SE API is not public: it uses the token of the mailbox and the mailbox address as the sender. "No address found" (SEAPI-00010) returns an empty result. search()->search([...]) accepts the raw BaeSearch structure.

Polling and events

// routes/console.php
Schedule::command('edoreczenia:poll')->everyFiveMinutes()->withoutOverlapping();
use Beziworld\EDoreczenia\Events\EvidenceReceived;
use Beziworld\EDoreczenia\Events\MessageReceived;

Event::listen(function (MessageReceived $event) {
    $event->connection;   // connection name
    $event->message;      // metadata only; the message is not opened
});

Event::listen(function (EvidenceReceived $event) {
    $event->evidence->evidenceType();
});

The poller lists the inbox newest first (metadata, so nothing is opened) and walks back to the stored cursor; the evidences of the mailbox have their own cursor. Events are dispatched oldest first, at least once: make listeners idempotent. On the first run EDORECZENIA_POLL_INITIAL=all dispatches what is already in the inbox (up to max_pages x page_size), now only remembers it. Options: --messages, --evidences, --reset, a connection name. MailboxPoller::setCursor() skips history.

Push notifications are available when the application is reachable from the internet: EDoreczenia::subscribe(['messageCallbackUrl' => ..., 'evidenceCallbackUrl' => ...]), subscriptions(), toggleSubscription(), unsubscribe(). The callback answers HTTP 200 with {"notification": "on"}.

Notifications

use Beziworld\EDoreczenia\Messages\OutgoingMessage;
use Beziworld\EDoreczenia\Notifications\EDoreczeniaChannel;

class InvoiceIssued extends Notification
{
    public function via($notifiable): array
    {
        return [EDoreczeniaChannel::NAME];
    }

    public function toEDoreczenia($notifiable): OutgoingMessage
    {
        return OutgoingMessage::create('Faktura')->text('...')->attachFile($this->path);
    }
}

// on the notifiable model
public function routeNotificationForEdoreczenia(): ?string
{
    return $this->ade;   // AE:PL-...
}

Notification::route('edoreczenia', 'AE:PL-...') works for on-demand notifications; ->connection('second') on the message selects another mailbox. The channel returns the SendResult.

Many mailboxes

// config/edoreczenia.php: connections.default, connections.branch, ...
EDoreczenia::connection('branch')->inbox();

// mailbox of a tenant stored in the database
EDoreczenia::using([
    'address' => $tenant->ade,
    'system_name' => $tenant->ade_system,
    'private_key' => decrypt($tenant->ade_key),
    'environment' => 'prod',
], 'tenant-' . $tenant->id)->send($message);

Other operations

Threads, folders, rules, archive tasks and other UA API operations are available through the generic call:

EDoreczenia::request('GET', '/{address}/threads', ['limit' => 20])->json();
EDoreczenia::request('GET', '/{address}/numberMessages')->json();
EDoreczenia::updateControlData($ids, ['op' => 'replace', 'path' => '...', 'value' => 'TRASH']);

{address} is replaced with the mailbox address. Only GET requests are retried.

Errors

Exception When
InvalidConfiguration missing or malformed address, system name, key, environment or algorithm; unknown connection
AuthenticationException the token endpoint rejected the assertion (error(): invalid_client, ...) or is unreachable
ApiException UA or SE API error: httpStatus(), errorCode() (UA error or SEAPI-000xx), errors(), retryAfter(), isRetryable()
MessagePending HTTP 202: the message of a notice is still being transferred; repeat later
InvalidMessage a documented rule is violated before sending (recipients, attachments, text, more than 50 ids)
CouldNotSendNotification the notification has no toEDoreczenia() or returns something else

All extend EDoreczeniaException. Read calls are retried on connection errors and HTTP 5xx; sending, deleting and changing are never retried. A 401 drops the cached token and repeats the call once with a new token.

Scope and limits

Implemented from the published contracts: UA API 3.0.8 (uaapi_3.0.8.2.yaml), SE API 4.0.5 and the integration guides for INT and PROD of the Ministry of Digital Affairs and the Central IT Centre (COI). Points not covered by the public documentation:

  • the JSON Pointer used by message_control_data to move messages between folders is not published, so only the generic updateControlData() is offered;
  • the colon of the address is sent unencoded in the URL path (allowed by RFC 3986); the encoding expected by the server has not been confirmed on INT;
  • rate limits are not documented; HTTP 429 is reported with retryAfter();
  • the message format between providers (ETSI EN 319 522) is handled by the operators, not by the client API.

Development

composer install
composer ci   # pint --test, phpstan (level 8), pest

Tests use Http::fake() with payloads built from the official OpenAPI contracts; no request leaves the test suite.

Author

Created and maintained by Kamil Ćwiertnia, beziworld.eu.

Need an integration? Get in touch through beziworld.eu.

Other packages by the author

laravel-ksef · laravel-krs · laravel-ceidg · laravel-company-data

License

MIT. See LICENSE.md. e-Doręczenia is a service of the Polish Ministry of Digital Affairs (Ministerstwo Cyfryzacji) operated with Poczta Polska as the designated operator. This package is not affiliated with or endorsed by the Ministry of Digital Affairs, the Central IT Centre (COI) or Poczta Polska.