beziworld / laravel-ceidg
Laravel client for the official CEIDG API v3: look up Polish sole proprietorships by NIP or REGON, search the register, validate and watch business status
Requires
- php: ^8.3
- ext-json: *
- guzzlehttp/guzzle: ^7.8
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/events: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.7|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
- phpstan/phpstan-strict-rules: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-04 17:18:34 UTC
README
Laravel client for the official API v3 of CEIDG (Centralna Ewidencja i Informacja o Działalności Gospodarczej), the Polish register of sole proprietorships, published by the Ministry of Development and Technology as the CEIDG data warehouse. Check a sole trader before you sign a contract or issue an invoice:
- lookup by NIP or REGON, details by entry identifier, several entries in one request
- owner, NIP and REGON with revoked or invalidated NIP flags, citizenships
- status (
AKTYWNY,ZAWIESZONY,WYKRESLONY,OCZEKUJE_NA_ROZPOCZECIE_DZIALANOSCI,WYLACZNIE_W_FORMIE_SPOLKI) as an enum, with the dates of start, suspension, resumption, end and removal - main, correspondence and additional business addresses with TERC, SIMC and ULIC codes
- PKD codes, civil partnerships (NIP, REGON) of the entrepreneur, contact data and e-Delivery address
- marital property community, bans, bankruptcy information, succession management, RDR permissions and restrictions
- search with filters as a
LazyCollectionthat reads further pages only while you iterate - identifiers of entries changed in a period, typed
final readonlyDTOs, the raw response for unmapped fields - caching, retries of read-only calls, rate-limit aware errors (
RateLimitedwithretryAfter) ActiveInCeidgvalidation rule,ceidg:showandceidg:watchArtisan commands,CeidgStatusChangedevent
Why another CEIDG package
Existing packages on Packagist (netborg/laravel-ceidg-sdk, sigrun/ceidg-api, dominservice/ceidg-api) wrap the
retired SOAP API v1 and target PHP 7 and Laravel 5/6; dominservice/ceidg_data_warehouse is a plain cURL client
without Laravel integration. This package targets API v3 on PHP 8.3+ and Laravel 12/13, maps the whole documented
response to typed objects and adds what an application needs around it: pagination, caching, error handling with the
request limit in mind, validation and a status watcher.
Installation
composer require beziworld/laravel-ceidg
php artisan vendor:publish --tag=ceidg-config # optional
Getting the free API token
- Sign in to biznes.gov.pl (trusted profile, e-ID or bank login).
- Register in the data warehouse at dane.biznes.gov.pl and request access to the API.
- The token (JWT, "klucz do API HD") arrives by e-mail. Access is free of charge.
The token is personal. Keep it in .env and never commit it.
CEIDG_TOKEN=eyJhbGciOi... CEIDG_ENVIRONMENT=production # or "test" for https://test-dane.biznes.gov.pl CEIDG_CACHE_TTL=86400 # seconds; 0 disables caching CEIDG_CACHE_STORE= # cache store, empty = default store CEIDG_RETRIES=2 # retries after connection errors and HTTP 5xx CEIDG_TIMEOUT=30 CEIDG_WATCH=3563457932,2367852376 # NIP numbers checked by ceidg:watch CEIDG_WATCH_STORE= # persistent store for the last known statuses CEIDG_URL= # overrides the endpoint of the environment
Lookup by NIP or REGON
use Beziworld\Ceidg\Facades\Ceidg; $business = Ceidg::findByNip('356-345-79-32'); // separators and the "PL" prefix are accepted; null when not registered $business->id; // "31F87519-9395-4FCF-8E19-6D5C0522FA7A" $business->name; // "Adam IntegracjaMGMF Usługi Informatyczne" $business->owner->name(); // "Adam IntegracjaMGMF" $business->nip(); // "3563457932" $business->regon(); // "618155359" $business->owner->nipRevoked; // false $business->status; // BusinessStatus::Active $business->status->label(); // "active" $business->isActive(); // also isSuspended(), isDeleted(), isPending(), isOnlyInPartnership() $business->startedAt; // CarbonImmutable 2019-03-01 $business->suspendedAt; // also resumedAt, endedAt, deletedAt (string) $business->address; // "ul. Zwierzyniecka 1/2, 15-333 Białystok" $business->address->terc; // "2061011" $business->correspondenceAddress; // Address or null $business->additionalAddresses; // list<Address> $business->mainPkd->formatted(); // "62.01.Z" $business->pkdCodes(); // ["6201Z", "6202Z"] $business->partnerships; // list<Partnership>: nip, regon, suspendedAt $business->email; // also phone, website, eDeliveryAddress, otherContact $business->propertyCommunity; // PropertyCommunity::Yes, No or NotApplicable $business->bans; // list<Ban> $business->bankruptcies; // list<Bankruptcy> $business->successionManager; // SuccessionManager or null $business->permissions; // list<RdrEntry>; restrictions as well $business->raw; // decoded API object
One NIP can have several entries, for example a business closed years ago and a new one. findByNip() and
findByRegon() return the most relevant entry (active first, then by status, then the most recently started);
entriesByNip() and entriesByRegon() return all of them. A NIP or REGON with a wrong checksum throws
InvalidIdentifier before any request.
Ceidg::company('31F87519-9395-4FCF-8E19-6D5C0522FA7A'); // by entry identifier; throws NotFound Ceidg::companies([$id1, $id2]); // several entries in one request
Search
use Beziworld\Ceidg\Enums\BusinessStatus; use Beziworld\Ceidg\Query\SearchFilters; $filters = SearchFilters::make() ->city('Białystok') ->pkd('62.01.Z') // dots are optional ->status(BusinessStatus::Active) ->startedBetween('2024-01-01', '2024-12-31') ->pageSize(50); foreach (Ceidg::search($filters) as $business) { // pages are requested only while iterating } Ceidg::search(['lastName' => 'Kowalski', 'voivodeship' => 'podlaskie'])->take(10)->all(); Ceidg::search(['partnershipNip' => '8567773578'])->all(); // partners of a civil partnership
Available criteria: nip, regon, partnershipNip, partnershipRegon, firstName, lastName, name, street,
building, flat, city, voivodeship, county, commune, postalCode, pkd, status, startedFrom,
startedTo, pageSize (the API names such as miasto or dataod work too). Several values of one criterion are
combined with OR, different criteria with AND. The API compares values case sensitively.
Search results carry the summary only (identifier, name, owner, main address, start date, status, isDetailed() is
false); read the details with Ceidg::company($business->id).
Changed entries
Ceidg::changed('2026-11-02', '2026-11-03')->each(function (string $id) { $business = Ceidg::company($id); });
The API recommends periods of at most five days.
Validation
use Beziworld\Ceidg\Rules\ActiveInCeidg; $request->validate([ 'nip' => ['required', new ActiveInCeidg()], // status AKTYWNY 'partner_nip' => ['required', (new ActiveInCeidg())->allowPartnershipOnly()], // also WYLACZNIE_W_FORMIE_SPOLKI 'supplier_nip' => ['required', (new ActiveInCeidg())->allowSuspended()->strict()], ]);
The rule passes when the API cannot be reached or the request limit is exhausted, so an outage does not block your
forms; strict() fails the validation instead. A rejected token throws AuthenticationFailed, because it is a
configuration error. Companies registered in KRS are not in CEIDG; validate them with
laravel-krs.
Status watcher
ceidg:watch reads the current entry of every watched NIP, compares its status with the status recorded on the
previous run and dispatches Beziworld\Ceidg\Events\CeidgStatusChanged when it differs. The first run only records
the statuses.
use Beziworld\Ceidg\CeidgWatcher; use Beziworld\Ceidg\Events\CeidgStatusChanged; // AppServiceProvider::boot(): watch the sole traders stored in the database CeidgWatcher::watchUsing(fn () => Contractor::whereNotNull('nip')->pluck('nip')); Event::listen(function (CeidgStatusChanged $event): void { $event->previous; // BusinessStatus::Active $event->current; // BusinessStatus::Suspended, or null when the entry disappeared $event->business; // fresh Business or null $event->stoppedOperating(); // true });
// routes/console.php Schedule::command('ceidg:watch')->dailyAt('06:00');
Every watched NIP costs one request, so with the limit of 1000 requests per hour split large lists into batches
(for example several resolvers run at different hours). Keep CEIDG_WATCH_STORE on a persistent store such as
database or redis.
Artisan
php artisan ceidg:show 3563457932
php artisan ceidg:show 3563457932 --all # every entry, including removed ones
php artisan ceidg:show 618155359 --regon
php artisan ceidg:show 31F87519-9395-4FCF-8E19-6D5C0522FA7A --id
php artisan ceidg:watch 3563457932 2367852376
Caching and limits
The API allows 1000 requests per 60 minutes per token. Details read by NIP, REGON or identifier are cached for
CEIDG_CACHE_TTL seconds (one day by default); answers without data are not cached, so a newly registered business
shows up immediately. Search results and change lists are not cached. Ceidg::forget($nipRegonOrId) clears one
entry by hand; the watcher always reads fresh data.
HTTP 429 is never retried, because repeated requests may extend the block. It throws RateLimited with
retryAfter in seconds when the gateway announces it (Retry-After or X-Rate-Limit-Reset). When the gateway sends
quota headers, Ceidg::rateLimit() returns the limit, the remaining requests and the reset time of the last
response. Connection failures and HTTP 5xx answers are retried CEIDG_RETRIES times.
Errors
| Situation | Result |
|---|---|
| No entry for a NIP or REGON (HTTP 204) | findByNip() / findByRegon() return null, entriesBy*() return [] |
| No entry for an identifier (HTTP 204 or 404) | NotFound (identifier, type) |
| NIP, REGON or identifier malformed | InvalidIdentifier, before any request |
| Token missing | InvalidConfiguration |
| Token rejected (HTTP 401) or without access (HTTP 403) | AuthenticationFailed |
| Request limit exhausted (HTTP 429) | RateLimited (retryAfter) |
| HTTP 400 | ApiException with errorCode, for example NIEPOPRAWNY_NUMER_NIP |
| HTTP 5xx after retries, invalid JSON | ApiException (httpStatus, path, isServerError()) |
| Connection failure or timeout after retries | ApiException wrapping the ConnectionException |
| Invalid search filter, date or empty search | InvalidArgumentException |
AuthenticationFailed and RateLimited extend ApiException; all package exceptions extend
Beziworld\Ceidg\Exceptions\CeidgException.
Personal data
CEIDG entries describe natural persons. The register is public, but storing and processing its data in your application is subject to the GDPR: keep only what you need and document the purpose.
Related packages
- laravel-company-data: GUS REGON (BIR), VAT white list with bank accounts and VIES, without a CEIDG token.
- laravel-krs: companies, foundations and associations from the National Court Register.
- laravel-polish-validation: format validation of NIP, REGON, PESEL and more, without an HTTP call.
Development
composer install
composer ci # pint --test, phpstan (level 8), pest
Tests use fixtures shaped after the responses of the official integrator documentation; no request leaves the test suite.
Author
Created and maintained by Kamil Ćwiertnia, beziworld.eu.
- E-mail: biuro@beziworld.eu
- Issues: GitHub issues
Need an integration with a Polish public register or business API? Get in touch through beziworld.eu.
Other packages by the author
laravel-company-data · laravel-krs · laravel-polish-validation · laravel-uldk
License
MIT. See LICENSE.md. The data comes from CEIDG, published through the data warehouse API (dane.biznes.gov.pl) of the Polish Ministry of Development and Technology (Ministerstwo Rozwoju i Technologii). This package is not affiliated with or endorsed by the Ministry or by biznes.gov.pl.