Polish Business Verification
Four identifiers, four API calls, one verification flow. Here's how to validate a Polish company's PESEL, REGON, KRS, and VAT number — and how to cross-reference them to catch inconsistencies before onboarding.
In this guide
- 1. Why Polish business verification requires four identifiers
- 2. The four identifiers
- 3. Step 1: Validate the PESEL (owner identity)
- 4. Step 2: Validate the REGON (business statistics)
- 5. Step 3: Validate the KRS (court registry)
- 6. Step 4: Validate the VAT number
- 7. Putting it all together — parallel validation with cross-referencing
- 8. cURL examples
- 9. Handling edge cases
- 10. Summary checklist
1. Why Polish business verification requires four identifiers
Poland has one of the most layered business identification systems in the EU. Unlike countries where a single registry number identifies a company, Polish businesses are tracked across multiple independent systems:
- PESEL — the national identification number for individuals (used to verify the owner/director)
- REGON — the statistical identification number from the Central Statistical Office (GUS)
- KRS — the National Court Register number for legal entities (companies, foundations, associations)
- NIP/VAT — the tax identification number, also used as the EU VAT number with the PL prefix
Each identifier is issued by a different government body and serves a different purpose. A complete verification requires checking all four — and cross-referencing the results to ensure the data is consistent.
2. The four identifiers
Each identifier serves a different purpose in the Polish business verification chain:
| Identifier | Issuer | What it proves | API endpoint |
|---|---|---|---|
| PESEL | Ministry of Digital Affairs | The person (owner/director) has a valid national ID number | GET /v0/pl/pesel |
| REGON | Central Statistical Office (GUS) | The business is registered in the national statistics system | GET /v0/pl/regon |
| KRS | National Court Register | The legal entity is registered with the court and has a known legal form | GET /v0/pl/krs |
| VAT (NIP) | Ministry of Finance / EU VIES | The tax registration is active in Poland and/or the EU VIES system | GET /v0/vat |
3. Step 1: Validate the PESEL (owner identity)
The PESEL endpoint validates the Polish national identification number:
- Weighted checksum — the modulo-10 algorithm with weights 1-3-7-9-1-3-7-9-1-3
- Date of birth extraction — the first 6 digits encode the birth date (with century offset)
- Gender extraction — the 10th digit encodes gender (odd = male, even = female)
Example response
{ "valid": true, "pesel": "44051401458", "dateOfBirth": "1944-05-14", "gender": "male" }
What to check in your code
valid === true — the PESEL passes format and weighted checksum validation
dateOfBirth — matches the person's claimed date of birth (cross-reference with ID documents)
gender — matches the person's identity documents (additional consistency check)
4. Step 2: Validate the REGON (business statistics)
The REGON endpoint validates the Polish statistical identification number:
- Format validation — REGON is either 9 digits (entities) or 14 digits (local units)
- Weighted checksum — modulo-11 algorithm with specific weight sequences
- GUS registry lookup — queries the Central Statistical Office to confirm the business exists
Example response
{ "valid": true, "regon": "123456785", "type": "entity", "found": true, "entity": { "name": "EXAMPLE SP. Z O.O.", "regon": "123456785", "nip": "1234567890", "statusCode": "active" } }
What to check in your code
valid === true — the REGON passes format and checksum validation
found === true — the business exists in the GUS registry
entity.name — matches the company name provided by the counterparty
5. Step 3: Validate the KRS (court registry)
The KRS endpoint validates the National Court Register number:
- Format validation — KRS is a 10-digit number, zero-padded
- Court registry lookup — queries the KRS database for company details
- Legal form identification — returns the company type (sp. z o.o., S.A., etc.)
- REGON cross-reference — the KRS entry contains the company's REGON for verification
Example response
{ "valid": true, "krs": "0000012345", "found": true, "entity": { "name": "EXAMPLE SP. Z O.O.", "krs": "0000012345", "nip": "1234567890", "regon": "123456785", "legalForm": "SP. Z O.O.", "registrationDate": "2010-03-15", "lastEntryDate": "2024-11-20" } }
What to check in your code
found === true — the company exists in the National Court Register
entity.regon — cross-reference with the REGON you validated in step 2
entity.legalForm — confirm the legal form matches expectations (e.g., sp. z o.o. for limited liability)
krsResult.entity.regon === regonValue. A mismatch means the KRS and REGON belong to different entities — this is a serious red flag.6. Step 4: Validate the VAT number
The VAT endpoint validates the Polish NIP as an EU VAT number:
- Polish NIP checksum — weighted modulo-11 algorithm specific to Polish tax IDs
- VIES lookup — confirms the VAT number is active in the EU VAT Information Exchange System
- Company name and address — VIES returns the registered business details
Example response
{ "valid": true, "countryCode": "PL", "vatNumber": "1234567890", "normalized": "PL1234567890", "vies": { "checked": true, "valid": true, "name": "EXAMPLE SP. Z O.O.", "address": "UL. PRZYKLADOWA 10, 00-001 WARSZAWA" } }
What to check in your code
valid === true — the NIP passes Polish weighted checksum validation
vies.valid === true — the VAT registration is active in the EU VIES system
vies.name — cross-reference with the company name from KRS and REGON lookups
7. Putting it all together — parallel validation with cross-referencing
The following Python example validates all four identifiers in parallel using asyncio.gather (SDK) or ThreadPoolExecutor (requests). Install with pip install isvalid-sdk or pip install requests.
import asyncio import os from isvalid_sdk import IsValidConfig, create_client iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"])) async def verify_polish_business(pesel: str, regon: str, krs: str, vat: str) -> dict: pesel_result, regon_result, krs_result, vat_result = await asyncio.gather( iv.pl.pesel(pesel), iv.pl.regon(regon), iv.pl.krs(krs), iv.vat(vat, check_vies=True), ) # ── Cross-reference REGON from KRS lookup ────────────────────────────── regon_from_krs = (krs_result.get("entity") or {}).get("regon") regon_match = regon_from_krs == regon # ── Cross-reference company name across registries ───────────────────── names = [ n for n in [ (regon_result.get("entity") or {}).get("name"), (krs_result.get("entity") or {}).get("name"), (vat_result.get("vies") or {}).get("name"), ] if n ] names_consistent = all(n == names[0] for n in names) if len(names) > 1 else True return { "pesel": { "valid": pesel_result["valid"], "date_of_birth": pesel_result.get("dateOfBirth"), "gender": pesel_result.get("gender"), }, "regon": { "valid": regon_result["valid"], "found": regon_result.get("found"), "name": (regon_result.get("entity") or {}).get("name"), }, "krs": { "valid": krs_result["valid"], "found": krs_result.get("found"), "name": (krs_result.get("entity") or {}).get("name"), "legal_form": (krs_result.get("entity") or {}).get("legalForm"), "regon_from_krs": regon_from_krs, }, "vat": { "valid": vat_result["valid"], "confirmed": (vat_result.get("vies") or {}).get("valid"), "name": (vat_result.get("vies") or {}).get("name"), }, "cross_checks": { "regon_match": regon_match, "names_consistent": names_consistent, }, } # ── Example usage ────────────────────────────────────────────────────────── result = asyncio.run(verify_polish_business( pesel="44051401458", regon="123456785", krs="0000012345", vat="PL1234567890", )) print("PESEL:", f"✓ born {result['pesel']['date_of_birth']}" if result["pesel"]["valid"] else "✗ invalid") print("REGON:", f"✓ {result['regon']['name']}" if result["regon"]["found"] else "✗ not found") print("KRS :", f"✓ {result['krs']['name']}" if result["krs"]["found"] else "✗ not found") print("VAT :", f"✓ {result['vat']['name']}" if result["vat"]["confirmed"] else "✗ not confirmed") print("REGON match:", "✓" if result["cross_checks"]["regon_match"] else "✗ MISMATCH") print("Names consistent:", "✓" if result["cross_checks"]["names_consistent"] else "✗ MISMATCH") approved = ( result["pesel"]["valid"] and result["regon"]["found"] and result["krs"]["found"] and result["vat"]["confirmed"] and result["cross_checks"]["regon_match"] ) print("Verification:", "APPROVED" if approved else "REJECTED")
asyncio.gather (SDK) or ThreadPoolExecutor (requests). The cross-referencing (REGON from KRS, name consistency) happens after all results are in — it adds zero latency.8. cURL examples
Validate a PESEL number:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/pl/pesel?value=44051401458"
Validate a REGON number:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/pl/regon?value=123456785"
Validate a KRS number:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/pl/krs?value=0000012345"
Validate a Polish VAT number with VIES check:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/vat?value=PL1234567890&checkVies=true"
9. Handling edge cases
Polish business verification has unique challenges due to the multi-registry system. Here are the most common edge cases:
REGON from KRS does not match provided REGON
The KRS lookup returned a REGON that differs from the REGON provided by the counterparty. This is a serious red flag — it means the KRS and REGON belong to different entities. The counterparty may have provided incorrect identifiers, or this could indicate fraud.
entity = krs_result.get("entity") or {} if krs_result.get("found") and entity.get("regon") != regon: # KRS and REGON belong to different entities — reject return { "status": "rejected", "reason": f"REGON mismatch: KRS contains {entity['regon']}, but {regon} was provided", }
Sole proprietorship — no KRS
Sole proprietorships (jednoosobowa dzialalnosc gospodarcza) are registered in CEIDG, not KRS. If the counterparty is a sole proprietor, skip the KRS validation. You can still validate PESEL, REGON, and VAT.
is_company = bool(krs) # sole proprietors don't have KRS calls = [ pool.submit(call_api, "/v0/pl/pesel", {"value": pesel}), pool.submit(call_api, "/v0/pl/regon", {"value": regon}), pool.submit(call_api, "/v0/vat", {"value": vat, "checkVies": "true"}), ] # Only validate KRS for companies if is_company: calls.append(pool.submit(call_api, "/v0/pl/krs", {"value": krs})) results = [f.result() for f in calls]
VIES unavailable for Polish VAT
When vies.checked is false, the Polish VIES node is unavailable. The NIP passed local checksum validation, but VIES confirmation is pending. Accept provisionally and re-check — the Polish VIES node is generally reliable but has occasional maintenance windows.
vies = vat_result.get("vies") or {} if vat_result["valid"] and vies and not vies.get("checked"): # Polish VIES node is down — NIP format is valid schedule_recheck(vat_result["normalized"], delay="6h") return {"status": "provisional", "reason": "vies_unavailable"}
Company name differs across registries
The company name from REGON, KRS, and VIES may have minor differences — abbreviations, capitalization, or legal form suffixes. Do not require exact string matching. Instead, normalize and compare, or flag significant differences for manual review.
import re def normalize_company_name(name: str) -> str: name = name.upper() name = re.sub(r"SP\.?\s*Z\s*O\.?\s*O\.?", "SP. Z O.O.", name) name = re.sub(r"\s+", " ", name) return name.strip() regon_name = normalize_company_name((regon_result.get("entity") or {}).get("name", "")) krs_name = normalize_company_name((krs_result.get("entity") or {}).get("name", "")) if regon_name and krs_name and regon_name != krs_name: logger.warning(f'Company name differs: REGON="{regon_name}", KRS="{krs_name}"') # Flag for review but do not reject
10. Summary checklist
See also
Verify Polish businesses with one API
Free tier includes 100 API calls per day. No credit card required. PESEL, REGON, KRS, and VAT validation all included.