Looking to hire Laravel developers? Try LaraJobs

laravel-nfz maintained by beziworld

Description
Laravel integration with the Polish National Health Fund (NFZ): waiting times, contracts and statistics open APIs plus eWUS patient insurance verification, without ext-soap.
Last update
2026/11/04 22:12 (dev-master)
License
Downloads
0

Comments
comments powered by Disqus

Laravel NFZ

tests Latest Version License

Laravel integration with the Polish National Health Fund (Narodowy Fundusz Zdrowia, NFZ):

  • Waiting times ("Terminy leczenia", API 1.4): search waiting lists by benefit, voivodeship, locality, county, provider, urgent or stable case and children age groups, with the predicted waiting time (PCUS), waiting list statistics, contact details and accessibility of the place
  • Contracts ("Umowy NFZ", API 1.2): contracts of providers with values and plans per product and month, providers, service types, contract products, orthopaedic supplies
  • Statistics: generic read-only access to the benefit, pharmacy refund, chemotherapy and drug programme APIs
  • eWUS: verification of a patient's entitlement to publicly funded health care by PESEL, login with session caching and automatic re-login, MFA (TOTP) support, password change, many operator accounts, the signed NFZ confirmation document kept for archiving
  • InsuredInNfz validation rule, nfz:check-insurance and nfz:waiting-times Artisan commands
  • typed final readonly DTOs, lazy pagination with LazyCollection, caching, retries of read-only calls, SOAP without ext-soap, every HTTP call fakeable with Http::fake()

Polska wersja dokumentacji

Why this package

this package gilek/ewus dartui/ewus nextgen-tech/ewus-laravel
Laravel 12 / 13 yes no Laravel integration Laravel 5 only (2018) Laravel 6 to 8 (2020)
eWUS check, password change yes yes via gilek 2.0-alpha yes
MFA (TOTP) login, query block codes yes no no no
session cache, re-login, daily result cache yes no no no
NFZ open data APIs (waiting times, contracts) yes no no no

No other Packagist package covers the NFZ open data APIs.

Installation

composer require beziworld/laravel-nfz
php artisan vendor:publish --tag=nfz-config   # optional

The open data APIs work without configuration. eWUS needs the operator account of the provider portal of your NFZ branch:

NFZ_EWUS_DOMAIN=07            # NFZ branch code, 01-16
NFZ_EWUS_LOGIN=
NFZ_EWUS_PASSWORD=
NFZ_EWUS_OPERATOR_TYPE=       # SWD (provider) or LEK (doctor); required by branches 01, 04, 05, 06, 08, 09, 11, 12
NFZ_EWUS_OPERATOR_ID=         # provider (idntSwd) or doctor (idntLek) identifier
NFZ_EWUS_MFA_SECRET=          # Base32 TOTP secret for accounts with MFA; a code is generated for every login
NFZ_EWUS_TEST=false           # true = NFZ test environment
NFZ_EWUS_SESSION_TTL=600      # seconds a session is reused
NFZ_EWUS_CACHE_RESULTS=true   # cache results until midnight (one check per patient per day)

NFZ_CACHE_TTL=21600           # open data cache in seconds; 0 disables it
NFZ_CACHE_STORE=
NFZ_RETRIES=2                 # retries of read-only calls after connection errors and HTTP 5xx
NFZ_TIMEOUT=30

Waiting times

use Beziworld\Nfz\Facades\Nfz;
use Beziworld\Nfz\Enums\ChildAgeRange;
use Beziworld\Nfz\Query\QueueSearch;

$search = QueueSearch::for('PORADNIA KARDIOLOGICZNA')   // full-text fragment, e.g. "kardiolog"
    ->in('mazowieckie')                                 // code "07", name or Province::Mazowieckie
    ->locality('WARSZAWA')
    ->urgent()                                          // stable is the default
    ->forChildren(ChildAgeRange::UpTo3);                // or forAdults()

foreach (Nfz::waitingTimes()->search($search)->take(50) as $queue) {   // LazyCollection, pages of 25
    $queue->benefit;                     // "PORADNIA KARDIOLOGICZNA"
    $queue->provider;                    // provider name, ->providerCode, ->providerNip, ->providerRegon
    $queue->place->name;                 // "PORADNIA KARDIOLOGICZNA DLA DZIECI"
    $queue->place->address;              // "DALEKA 11", ->locality, ->county, ->latitude, ->longitude
    $queue->place->bookingPhone();       // registration phone, otherwise the general one
    $queue->place->accessibility->elevator;   // true / false / null (no information)
    $queue->forecast->text;              // predicted waiting time as published, e.g. "14 dni"
    $queue->forecast->days;              // 14
    $queue->forecast->expectedDate();    // CarbonImmutable
    $queue->statistics->awaiting;        // people on the list at the end of the month
    $queue->statistics->averageWaitDays;
}

$page = Nfz::waitingTimes()->searchPage(['benefit' => 'ortoped', 'province' => '12'], page: 2, limit: 25);
$page->total; $page->lastPage(); $page->hasMorePages();

$queue = Nfz::waitingTimes()->queue($id);          // null for an unknown id
$other = Nfz::waitingTimes()->otherPlaces($id);    // the same benefit in other places of the provider

Since version 1.4 of the API (July 2026) NFZ publishes the predicted waiting time (PCUS) instead of the former "first available date"; the package follows the current version.

Dictionaries (v2, with province, county, locality and street):

Nfz::waitingTimes()->benefits('kardio');                    // LazyCollection<string>
Nfz::waitingTimes()->localities('krak', province: '06');    // LazyCollection<DictionaryItem>
Nfz::waitingTimes()->counties(province: '06');
Nfz::waitingTimes()->streets('długa', '06', locality: 'KRAKÓW');
Nfz::waitingTimes()->places('poradnia', '06');
Nfz::waitingTimes()->providers('szpital', '06');            // ->code holds the provider code

The terms of the API require citing the source of the data: https://api.nfz.gov.pl/.

Contracts

$agreements = Nfz::contracts()->agreements([
    'year' => 2025,
    'branch' => '06',                 // required, NFZ branch code or name
    'providerName' => 'szpital',      // also: serviceType, productCode, providerCode, place, nip, regon,
    'updatedSince' => now()->subWeek(),   // updatedSince (date), sort ("attribute:asc|desc")
]);

foreach ($agreements as $agreement) {
    $agreement->code;          // "064/100032/PPZ/08/2025"
    $agreement->serviceName;   // "Programy pilotażowe"
    $agreement->amount;        // "54720.00" (decimal string), ->currency "PLN"
}

$details = Nfz::contracts()->agreement($agreement->id);   // ->plans (product, quantity, price, from, to)
$plan = Nfz::contracts()->plan($details->plans[0]->id);   // ->months, all pages read
$month = Nfz::contracts()->month($plan->months[0]->id);   // ->packages with unit products and weights
$provider = Nfz::contracts()->provider(2025, '064/100032', '06');   // provider with all its contracts

Nfz::contracts()->providers(['year' => 2025, 'branch' => '06']);   // with the yearly total ("amount")
Nfz::contracts()->serviceTypes(2025);                               // "01 Podstawowa Opieka Zdrowotna", ...
Nfz::contracts()->contractProducts(2025, name: 'rehabilitacja');
Nfz::contracts()->orthopedicSupplies(2025);
Nfz::contracts()->availableYears();                                 // [2008, ..., 2026]

Statistics

The statistical APIs expose many report-like resources, so results are returned as arrays. Resource names and parameters follow the Swagger documentation of each API.

use Beziworld\Nfz\Enums\StatisticsApi;

Nfz::statistics(StatisticsApi::PharmacyRefunds)->all('medicine-products', ['name' => 'apap']);   // LazyCollection
Nfz::statistics(StatisticsApi::Benefits)->page('sections', page: 1);
Nfz::statistics(StatisticsApi::DrugPrograms)->get('available-periods');

eWUS

$check = Nfz::ewus()->check('79060804378');   // validated locally first; nothing is sent for a malformed PESEL

$check->status;             // InsuranceStatus::Insured, NotInsured, NotFound (not in CWU), PeselAnnulled
$check->isInsured();
$check->validUntil;         // day of the confirmation
$check->firstName;          // "ImięTAK", ->lastName, ->fullName()
$check->prescriptionMark;   // "DN" for entitled persons under 18, ->hasDnMark()
$check->additionalInfo;     // list of AdditionalInfo: code ("PESEL-UKR-DOROSLY"), level (Information/Warning), value
$check->operationId;        // "L1712M01200000001"
$check->checkedAt;
$check->document;           // the signed NFZ confirmation (status_cwu_odp XML), cut byte for byte for archiving
$check->maskedPesel();      // "********378", use it in logs

Nfz::ewus()->isInsured($pesel);
Nfz::ewus()->check($pesel, fresh: true);   // ignore the result cached for today

The client logs in on the first call and reuses the session (NFZ_EWUS_SESSION_TTL). When eWUS reports an expired session or token, it logs in again and repeats the check once. Logins are never retried after an authentication error. NFZ allows checking only patients served on the given day and at most once per patient per day, so results are cached until midnight under a keyed hash of the PESEL (NFZ_EWUS_CACHE_RESULTS=false disables it).

Nfz::ewus()->login();                              // Session: ->messageCode, ->passwordExpiresInDays
Nfz::ewus()->logout();
Nfz::ewus()->changePassword('new password');        // also changes the password in the branch portal
Nfz::ewus()->changeExpiredPassword('new password'); // after login threw PasswordExpired

Many accounts

// config/nfz.php
'ewus' => [
    'connections' => [
        'clinic' => ['domain' => '07', 'login' => '...', 'password' => '...'],
        'lab' => ['domain' => '06', 'operator_type' => 'SWD', 'operator_id' => '...', 'login' => '...', 'password' => '...'],
    ],
],

Nfz::ewus('lab')->check($pesel);

// credentials stored per tenant
Nfz::ewusUsing(['domain' => $tenant->nfz_branch, 'login' => $tenant->ewus_login, 'password' => $tenant->ewus_password])->check($pesel);

Test environment

NFZ publishes a test environment with shared accounts (domain 15, login TEST1, password qwerty!@#; MFA variants TEST1_MFA with the TOTP secret from the interface specification). In the test environment any even PESEL is insured and any odd one is not; 01010153201 is not in the register, 19323197971 has the DN mark, 00060958187 is annulled and 50323199945 triggers the daily query block.

NFZ_EWUS_TEST=true
NFZ_EWUS_DOMAIN=15
NFZ_EWUS_LOGIN=TEST1
NFZ_EWUS_PASSWORD="qwerty!@#"

Validation

use Beziworld\Nfz\Rules\InsuredInNfz;

$request->validate(['pesel' => ['required', new InsuredInNfz()]]);   // passes when eWUS is unreachable or blocked

(new InsuredInNfz())->strict();            // fails when eWUS cannot confirm the entitlement
(new InsuredInNfz())->connection('lab');   // named eWUS connection

The PESEL format (checksum and date of birth) is validated before any request. Rejected credentials and an expired password are configuration errors and are thrown.

Artisan

php artisan nfz:check-insurance 79060804378               # PESEL masked in the output; exit code 1 when not insured
php artisan nfz:check-insurance 79060804378 --with-name --connection=lab --fresh
php artisan nfz:waiting-times kardiolog --province=mazowieckie --locality=WARSZAWA --urgent --limit=20

Errors

Exception When
InvalidConfiguration missing or invalid option, unknown connection; names the environment variable
InvalidPesel malformed PESEL, raised before sending; the message holds the masked number only
ApiException open data API error: httpStatus, errorCode (e.g. 1200007), reason, solution
RateLimited HTTP 429 (limit of 10 requests per second per IP), never retried; retryAfter
AuthenticationFailed eWUS rejected the credentials; wait at least 15 minutes after a temporary lock
PasswordExpired the operator password has expired; use changeExpiredPassword()
SessionExpired session or token expired (handled automatically during checks)
AccessDenied authorisation refused; blockCode -100 to -105 for query blocks, blockDetail, isAdministrativeBlock()
InvalidInput eWUS rejected the input
EwusException other SOAP faults and transport errors: faultType, faultCode, operation

All exceptions extend Beziworld\Nfz\Exceptions\NfzException. eWUS messages never contain a PESEL.

Development

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

Tests use responses captured from the live open data APIs and from the eWUS test environment (tests/Fixtures); 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-polish-validation · laravel-teryt · laravel-krs · laravel-ceidg

License

MIT. See LICENSE.md. The open data comes from https://api.nfz.gov.pl/ and eWUS is operated by the National Health Fund (Narodowy Fundusz Zdrowia). This package is not affiliated with or endorsed by NFZ.