# Poland KRS Scraper & Bulk Company Lookup - Court Register (`neverempty/poland-krs-scraper`) Actor

Bulk lookup of Polish companies, foundations and associations by KRS number from the Ministry of Justice open API: name, legal form, NIP, REGON, address, share capital, PKD, board and shareholders, liquidation, suspension and arrears flags, and struck-off entities with their deletion date.

- **URL**: https://apify.com/neverempty/poland-krs-scraper.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Lead generation, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.38 / 1,000 entity returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Poland KRS Scraper & Bulk Company Lookup

Look up Polish companies, foundations and associations by KRS number in bulk and get one row per entity from the
National Court Register (Krajowy Rejestr Sądowy): registered name, legal form, NIP, REGON, registered address,
e-mail and website where filed, share capital, PKD activities, the management board and supervisory board (with
the register's own masking of personal names), proxies (prokurenci), shareholders and partners, liquidation,
dissolution, suspension and tax-arrears flags, filing history of financial statements, and for entities that have
been struck off, the last recorded values and the deletion date. The data comes from the Ministry of Justice open
API (api-krs.ms.gov.pl). No API key, no proxy, no scraping of the court portal.

```json
{
  "krsNumbers": ["0000028860", "0000026438", "0000006865"],
  "registry": "P"
}
```

Paste KRS numbers one per line or separated by commas; `28860`, `KRS 28860`, `0000-028-860` and `0000028860` are the
same entity. A number with a decimal point (`28860.0`, which is what a spreadsheet or pandas writes when the column is
numeric) is refused with a reason instead of being guessed - see the table in 5 below.

**Only the KRS number can be looked up.** The register's API has no search by name, NIP or REGON, and neither has
this Actor. If you only have a company name, find its KRS number first (for example on the court register portal),
then paste the numbers here.

### What this does that a plain API call does not

Everything below was measured against the live register on 2026-09-12, not read from documentation. **Every count
below is out of the same 93 entities**: the 93 current extracts the register returned that day (well-known companies,
random numbers from eight ranges of the register, and three entities from the S register), counted once per KRS
number. Two further entities are covered only by a full extract, because they have been struck off and their current
extract is empty (see 1 below); they are not part of the 93. All of them are kept in this Actor's test fixtures and
recounted by its tests, so every number here can be checked against the data it was measured from.

#### 1. Struck-off entities are not reported as "not found"

For an entity that has been struck off the register, the current extract endpoint answers **HTTP 204 with an empty
body** - a status the official documentation does not list. Many tools turn that into "no such company". Here a 204
triggers a read of the full extract (odpis pełny), which the register still serves for deleted entities, and the
row comes back with `deleted: true`, `deletedOn` (the date of the entry "WYKREŚLENIE Z KRAJOWEGO REJESTRU SĄDOWEGO"),
the last recorded name, legal form, NIP, REGON, address and share capital, the previous names, and the full list of
register entries. Grupa Lotos (0000106150, merged into Orlen in 2022) and PGNiG (0000059492) are examples. These rows
are charged like any other, because they answer the question you asked; the officers and shareholders are left
empty because that board no longer exists.

#### 2. A number in the other register is found anyway

Every KRS number is in exactly one of two registers: **P** (entrepreneurs: companies, cooperatives, foreign branches)
or **S** (associations, foundations, other non-profit organisations, public healthcare institutions). Asking the API
for a foundation in the P register returns 404. Here, when the chosen register answers 404, the other register is
tried once; if the entity is there, the row is delivered with `registry` set to where it was found and a note saying
so. Only when both answer 404 is the row `not-found`.

#### 3. Personal data: what the register masks stays masked, and nothing more is added

The open API of the Ministry of Justice anonymises natural persons: board members, supervisory board members,
proxies, liquidators and individual shareholders come back as `I******* F*****`, and PESEL numbers as `5**********`
(the API with full names is available only to bodies designated by a decision of the Minister). This Actor returns
exactly those masked names together with the role, and **never returns a PESEL, masked or not**. Free-text fields
that in the sample contained unmasked personal names - the description of a liquidation (which named a foundation's
founder) and the description of a proxy's powers (which named a branch proxy) - are not returned as text; only the
liquidation date, the dissolution circumstance and the type of proxy (the part before the first comma, for example
`PROKURA SAMOISTNA`) are.

#### 4. Values the raw data gets awkward

- **Amounts** arrive as strings with a decimal comma (`1451177561,25`). They are returned as numbers
  (`shareCapital: 1451177561.25`) with the currency and the raw string.
- **Dates** arrive as `dd.mm.yyyy` and are returned as ISO `yyyy-mm-dd`.
- **PKD codes** arrive as three parts (dział, klasa, podklasa) and are joined as `19.20.Z`. When the register holds
  a group-level entry (klasa with one digit) the code is returned as the group, `20.1`, not padded into a class that
  was never registered; old section-level entries from PKD 2004 (a single letter such as `D`) are returned as that
  letter.
- **A board seat is not always a person.** In partnerships the general partner is a company (Lidl sp. z o.o.
  represents Lidl sp. z o.o. sp.k.), so an officer can carry `name`, `krs` and `regon` instead of `nameMasked`; and
  the register sometimes lists a seat with no published data at all, which is kept as an all-null entry so that
  `officerCount` matches the register.
- **REGON** is stored as 14 digits ending in `00000`; both the 14-digit form (`regon`) and the 9-digit form (`regon9`)
  are returned. A revoked NIP carries `nipStatus: "UCHYLONY"` with the decision date: 4 of 93 had a revoked NIP.
- **Foreign parent shareholders** are stored with the placeholder KRS `0000000000`; it is returned as `null`.
- **Suspension of activity** is a list of suspension/resumption pairs; `activitySuspended` is true only when the
  latest suspension has no resumption date. 11 of 93 carry a suspension entry, most of them resumed.
- **Section 4 (arrears and enforcement)** is summarised as `arrearsCount`, `arrearsTotal` and `arrearsCurrency`, and
  the raw section keys are listed in `section4Keys` so nothing is hidden: 3 of 93 have section 4 entries.
  Section 6 (liquidation, dissolution, mergers, restructuring, suspension) is summarised the same way in
  `section6Keys`; 3 of 93 are in liquidation.
- **E-mail and website** are returned only when the entity filed them: 23 of 93 have an email, 39 a website.

#### 5. Typos and repeats are never reported as missing companies

| What you give | What you get | Charged |
|---|---|---|
| `ORLEN` or `7740001454X` (not digits) | `bad-input`, not sent to the register | No |
| `61018820100000` (a REGON, 14 digits) | `bad-input`, explaining that only the KRS number can be looked up | No |
| `28860.0` (a spreadsheet or pandas wrote a numeric column) | `bad-input`, saying a KRS number is a whole number. The point is **not** removed and the number is **not** guessed to be `28860`: `0000288600` is a different company that really exists, so a silent "fix" would sell you the wrong entity | No |
| `28860` | Padded to `0000028860` and looked up | Yes, if found |
| `0000000001` (never assigned) | `not-found`, after both registers answered 404 | No |
| A number the register refuses (HTTP 400) | `rejected-by-source`, with the register's own words. **This does not happen in practice**: every input the register would refuse is stopped here as `bad-input` before anything is sent, so the row exists only in case the register starts refusing something it accepts today | No |
| The register does not answer (no response, 429, 5xx) | Retried after 2, 5, 10 and 20 seconds; if it still fails, `failed` with every status seen | No |
| The same number twice, formatted differently | Looked up once, charged once; the second gets `duplicate-input` | No |
| A 200 answer whose `numerKRS` differs from the number asked | `failed` - a different entity is never delivered under your number | No |

#### 6. The register is used gently

Lookups are made one at a time, never in parallel, with a 0.25-second pause between them. Measured in production on
2026-09-12, 38 numbers took 22.6 seconds end to end over 44 requests (a number that is not in the register you chose
is looked up in the other one as well) - **0.59 seconds per lookup including the pause** - so the cap of
**2,000 numbers** takes about 20 minutes, and **1,000 with `fullExtract`** (two requests per number, about 1.1 seconds
each) about 18 minutes, both well inside Apify's default one-hour run limit. Anything beyond the cap is not looked up
and is listed in an uncharged row saying how many. In a test of 30 back-to-back requests from a home connection on the
same day, all 30 answered HTTP 200 with no rate limiting and no Retry-After, taking 1.35-2.53 seconds each (average
1.78); the register is slower to reach from outside a data centre than from Apify.

### What you get

Identity: `krs`, `registry` (P or S), `registryName`, `extract` (current or full), `deleted`, `deletedOn`, `name`,
`legalForm`, `isForeignBranch`, `foreignParentName`, `foreignRegister`, `regon`, `regon9`, `nip`, `nipStatus`,
`nipStatusOn`, `previousRegister`, `previousRegisterNumber`, `publicBenefitStatus` (OPP), `duration`.

Register header: `registeredInKrsOn`, `lastEntryNumber`, `lastEntryOn`, `dataAsOf`, `positionStateCode` (the
register's own `stanPozycji`, returned as is), `registryCourt`.

Address: `country`, `voivodeship`, `county`, `municipality`, `city`, `street`, `buildingNumber`, `unitNumber`,
`postalCode`, `postOffice`, `addressLine`, `email`, `website`, `eDeliveryAddress`.

Capital: `shareCapital`, `shareCapitalCurrency`, `shareCapitalRaw`, `paidCapital`, `totalShares`, `shareNominalValue`.

People and owners: `representationBody`, `representationRule`, `officerCount`, `officers` (masked name, role,
suspended), `supervisoryBody`, `supervisorCount`, `supervisors`, `proxyCount`, `proxies` (masked name, type),
`shareholderCount`, `shareholderCompanies`, `shareholderPersons`, `shareholders` (type, name or masked name, KRS,
REGON, shares held, whether they hold all shares, limited-partner flag and sums for partnerships).

Activity: `mainActivityPkd`, `mainActivity`, `otherActivityCount`, `otherActivities`, `purpose` (statutory purpose of
an organisation in the S register), `firstFinancialYearEnd`, `financialStatementCount`,
`latestFinancialStatementFiledOn`, `latestFinancialStatementPeriod`.

Status flags: `inLiquidation`, `liquidationOpenedOn`, `liquidatorCount`, `dissolutionCircumstance`, `dissolutionDate`,
`activitySuspended`, `activitySuspendedOn`, `activityResumedOn`, `mergerDivisionTransformationCount`, `arrearsCount`,
`arrearsTotal`, `arrearsCurrency`, `section4Keys`, `section5Keys`, `section6Keys`, `branchCount`, `branches`.

History (with `fullExtract`, and always for struck-off entities): `entryCount`, `entries` (number, description,
date, court, case number, date it became final), `previousNames`.

Every row also has `status`, `note`, `input` (what you typed), `source`, `sourceUrl` and `checkedAt`.

Only rows with `status` = `ok` are charged. Rows that explain an invalid number, a number in neither register, a
refusal, a failed lookup, a repeated input or an input that was not looked up are free.

### Input

| Field | What it does |
|---|---|
| `krsNumbers` | KRS numbers, one per line or separated by commas or semicolons, with or without leading zeros or a `KRS` prefix. Up to 2,000 per run (1,000 with fullExtract). If left empty, ORLEN S.A. (0000028860) is looked up as an example, and the log says so. |
| `registry` | `P` (entrepreneurs, default) or `S` (associations and foundations). If the number is not in the chosen register the other one is tried. |
| `fullExtract` | Also read the full extract and add the entry history and previous names to each row (two requests per number). Default off. |
| `maxResults` | Stop after this many lookups; the rest are listed in an uncharged row. Default and maximum 2,000; a value below 1 is treated as 1. |

`krsNumbers` carries no default value, so nothing is ever added to your list that you did not type.

### Limits worth knowing

- The register's API is a lookup by number only. There is no search by name, NIP, REGON, city or PKD.
- `dataAsOf` (`stanZDnia`) is the register's own "state as of" date for the extract; the data is as current as the
  court's last entry.
- `positionStateCode` is the register's `stanPozycji` returned without interpretation: 1 was observed on every active
  entity in the P register, 3 on entities in the S register, 4 on foundations registered in the P register and 2 on
  struck-off entities in the full extract. Its official meaning is not published with the API.
- Personal names are masked by the source (see 3 above). If you need the full names of board members, this Actor
  cannot provide them.

### Source and licence

The data is served by the open API of the National Court Register run by the Polish Ministry of Justice
(`api-krs.ms.gov.pl`, documented at prs.ms.gov.pl/krs/openApi), launched under the Act of 11 August 2021 on open
data and the re-use of public sector information. The dataset entry on the national open-data portal
(dane.gov.pl, dataset 27606) states the licence as **CC0 1.0** and that the API "does not require a token and has no
download limits; the data is made available in the public domain" ("API nie wymaga tokenu i nie posiada limitów
pobrań. Dane udostępniane są w domenie publicznej."). `api-krs.ms.gov.pl` publishes no robots.txt. This Actor makes
plain HTTPS requests to that API, one at a time, and does not use the court portal's web pages.

### Support

Found a KRS number that comes back wrong? Open an issue in the Issues tab with the number and what you expected.

# Actor input Schema

## `krsNumbers` (type: `array`):

One KRS number per line: 0000028860, 0000-028-860 and 28860 are the same entity (numbers are zero-padded to 10 digits before they are sent). A number with a decimal point (28860.0, which is what a spreadsheet or pandas writes for a numeric column) is refused with a reason and not charged, because dropping the point would look up 0000288600 - a different company that really exists. Only the KRS number can be looked up; the register's API has no search by name, NIP or REGON. A number given twice is looked up and charged once. Up to 2,000 per run (1,000 with fullExtract) so one run finishes within Apify's default one-hour limit; any beyond that are listed in an uncharged row as not looked up. If left empty, ORLEN S.A. (0000028860) is looked up as an example and the log says so.

## `registry` (type: `string`):

P = register of entrepreneurs (companies, cooperatives, foreign branches); S = register of associations, foundations, other social and professional organisations and public healthcare institutions. A number is in only one of them. If it is not found in the chosen register the other one is tried, and the registry column says where it was found.

## `fullExtract` (type: `boolean`):

When on, the full extract (odpis pełny) is read as well and each row also carries the list of all register entries (number, description, date, court, case number) and the previous names of the entity. This doubles the requests per number and halves the per-run limit to 1,000. The current data is the same either way. Entities struck off the register are always read from the full extract, whatever this setting.

## `maxResults` (type: `integer`):

Stop after this many KRS numbers have been looked up; the rest are listed in an uncharged row. Capped at 2,000 (1,000 with fullExtract); a value below 1 is treated as 1.

## Actor input object example

```json
{
  "krsNumbers": [
    "0000028860",
    "0000026438",
    "0000006865"
  ],
  "registry": "P",
  "fullExtract": false,
  "maxResults": 2000
}
```

# Actor output Schema

## `results` (type: `string`):

One row per KRS number: the entity's register data (current, or the last recorded values for a struck-off entity), or an uncharged row saying why there is none (invalid number, not in either register, register did not answer, repeated input, not looked up).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "krsNumbers": [
        "0000028860",
        "0000026438",
        "0000006865"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/poland-krs-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "krsNumbers": [
        "0000028860",
        "0000026438",
        "0000006865",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/poland-krs-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "krsNumbers": [
    "0000028860",
    "0000026438",
    "0000006865"
  ]
}' |
apify call neverempty/poland-krs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/poland-krs-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/lAwXCSZ5mT55EPemb/builds/fAL4S8olJXRVS3K4I/openapi.json
