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 Node.js example validates all four identifiers in parallel using Promise.all, then cross-references the results. Use the @isvalid-dev/sdk package or the native fetch API (Node 18+).
import { createClient } from '@isvalid-dev/sdk'; const iv = createClient({ apiKey: process.env.ISVALID_API_KEY }); async function verifyPolishBusiness({ pesel, regon, krs, vat }) { const [peselResult, regonResult, krsResult, vatResult] = await Promise.all([ iv.pl.pesel(pesel), iv.pl.regon(regon), iv.pl.krs(krs), iv.vat(vat, { checkVies: true }), ]); // ── Cross-reference REGON from KRS lookup ────────────────────────────── const regonFromKrs = krsResult.entity?.regon ?? null; const regonMatch = regonFromKrs === regon; // ── Cross-reference company name across registries ───────────────────── const names = [ regonResult.entity?.name, krsResult.entity?.name, vatResult.vies?.name, ].filter(Boolean); const namesConsistent = names.length > 1 ? names.every(n => n === names[0]) : true; return { pesel: { valid: peselResult.valid, dateOfBirth: peselResult.dateOfBirth ?? null, gender: peselResult.gender ?? null, }, regon: { valid: regonResult.valid, found: regonResult.found ?? null, name: regonResult.entity?.name ?? null, }, krs: { valid: krsResult.valid, found: krsResult.found ?? null, name: krsResult.entity?.name ?? null, legalForm: krsResult.entity?.legalForm ?? null, regonFromKrs: regonFromKrs, }, vat: { valid: vatResult.valid, confirmed: vatResult.vies?.valid ?? null, name: vatResult.vies?.name ?? null, }, crossChecks: { regonMatch, namesConsistent, }, }; } // ── Example usage ────────────────────────────────────────────────────────── const result = await verifyPolishBusiness({ pesel: '44051401458', regon: '123456785', krs: '0000012345', vat: 'PL1234567890', }); console.log('PESEL:', result.pesel.valid ? `✓ born ${result.pesel.dateOfBirth}` : '✗ invalid'); console.log('REGON:', result.regon.found ? `✓ ${result.regon.name}` : '✗ not found'); console.log('KRS :', result.krs.found ? `✓ ${result.krs.name}` : '✗ not found'); console.log('VAT :', result.vat.confirmed ? `✓ ${result.vat.name}` : '✗ not confirmed'); console.log('REGON match:', result.crossChecks.regonMatch ? '✓' : '✗ MISMATCH'); console.log('Names consistent:', result.crossChecks.namesConsistent ? '✓' : '✗ MISMATCH'); const approved = result.pesel.valid && result.regon.found && result.krs.found && result.vat.confirmed && result.crossChecks.regonMatch; console.log('Verification:', approved ? 'APPROVED' : 'REJECTED');
Promise.all. 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.
if (krsResult.found && krsResult.entity?.regon !== regon) { // KRS and REGON belong to different entities — reject return { status: 'rejected', reason: `REGON mismatch: KRS contains ${krsResult.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.
const isCompany = !!krs; // sole proprietors don't have KRS const calls = [ iv.pl.pesel(pesel), iv.pl.regon(regon), iv.vat(vat, { checkVies: true }), ]; // Only validate KRS for companies if (isCompany) { calls.push(iv.pl.krs(krs)); } const results = await Promise.all(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.
if (vatResult.valid && vatResult.vies && !vatResult.vies.checked) { // Polish VIES node is down — NIP format is valid await scheduleRecheck(vatResult.normalized, '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.
function normalizeCompanyName(name) { return name .toUpperCase() .replace(/SP\.?\s*Z\s*O\.?\s*O\.?/g, 'SP. Z O.O.') .replace(/\s+/g, ' ') .trim(); } const regonName = normalizeCompanyName(regonResult.entity?.name ?? ''); const krsName = normalizeCompanyName(krsResult.entity?.name ?? ''); if (regonName && krsName && regonName !== krsName) { logger.warn(`Company name differs: REGON="${regonName}", KRS="${krsName}"`); // 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.