Guide · Python · SDK · Compliance

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.

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.

ℹ️The KRS entry contains the company's REGON number. After validating both, you can cross-reference: the REGON from the KRS lookup should match the REGON you validated independently. A mismatch is a red flag.

2. The four identifiers

Each identifier serves a different purpose in the Polish business verification chain:

IdentifierIssuerWhat it provesAPI endpoint
PESELMinistry of Digital AffairsThe person (owner/director) has a valid national ID numberGET /v0/pl/pesel
REGONCentral Statistical Office (GUS)The business is registered in the national statistics systemGET /v0/pl/regon
KRSNational Court RegisterThe legal entity is registered with the court and has a known legal formGET /v0/pl/krs
VAT (NIP)Ministry of Finance / EU VIESThe tax registration is active in Poland and/or the EU VIES systemGET /v0/vat
⚠️Sole proprietorships (jednoosobowa dzialalnosc gospodarcza) do not have a KRS number — they are registered in CEIDG instead. Only validate KRS for companies (sp. z o.o., S.A., etc.) and other legal entities.

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

1

valid === true — the PESEL passes format and weighted checksum validation

2

dateOfBirth — matches the person's claimed date of birth (cross-reference with ID documents)

3

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

1

valid === true — the REGON passes format and checksum validation

2

found === true — the business exists in the GUS registry

3

entity.name — matches the company name provided by the counterparty

The REGON lookup also returns the company's NIP (tax ID). You can cross-reference this with the VAT number to ensure consistency across registries.

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

1

found === true — the company exists in the National Court Register

2

entity.regon — cross-reference with the REGON you validated in step 2

3

entity.legalForm — confirm the legal form matches expectations (e.g., sp. z o.o. for limited liability)

⚠️The KRS entry includes the company's REGON. Always cross-reference: 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

1

valid === true — the NIP passes Polish weighted checksum validation

2

vies.valid === true — the VAT registration is active in the EU VIES system

3

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")
All four API calls run in parallel via 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

Validate PESEL to confirm the owner's identity — checksum + date of birth extraction
Cross-reference REGON from KRS lookup — a mismatch is a red flag
Run all four validations in parallel — reduces total verification latency
Confirm VAT via VIES — checksum alone does not prove active registration
Do not require KRS for sole proprietorships — they use CEIDG, not KRS
Do not require exact name matching across registries — normalize before comparing

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.