CEIDG Validation in Python
The Polish registry for sole proprietors and civil partnerships — validated by NIP checksum, confirmed by a live CEIDG lookup returning owner name, address, status, and PKD activity codes.
In this guide
1. What is CEIDG?
CEIDG (Centralna Ewidencja i Informacja o Działalności Gospodarczej — Central Registration and Information on Business Activity) is the official Polish government registry for:
- Sole proprietors (JDG — jednoosobowa działalność gospodarcza)
- Civil partnerships (spółki cywilne) — each partner registers separately
CEIDG does not cover companies incorporated under commercial law (sp. z o.o., S.A., sp. k., etc.) — those are registered in the KRS (National Court Register). If you receive a NIP and do not know the entity type, start with CEIDG; if not found, fall back to KRS.
CEIDG is maintained by the Ministry of Economic Development and is publicly accessible. It stores:
- Owner's first and last name
- Business name (if different from the owner's name)
- Registration status (active / suspended / deleted)
- Registered business address
- PKD activity codes (Polish Classification of Activities, based on NACE Rev. 2)
- REGON statistical number and NIP tax number
- Business commencement date
2. NIP — the key identifier
CEIDG entries are identified by NIP (Numer Identyfikacji Podatkowej — Tax Identification Number), the Polish equivalent of a VAT/tax ID. A NIP is:
- Exactly 10 digits, no letters
- Written with hyphens as
NNN-NNN-NN-NN(e.g.526-104-08-28) - Protected by a MOD-11 weighted checksum on the last digit
3. The NIP MOD-11 checksum algorithm
The NIP checksum uses a weighted MOD-11 algorithm. The first 9 digits are each multiplied by a fixed weight, the products are summed, and the result modulo 11 must equal the 10th (check) digit. If the remainder is 10, the number is always invalid.
| Position | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| Weight | 6 | 5 | 7 | 2 | 3 | 4 | 5 | 6 | 7 | check |
import re def nip_valid(nip: str) -> bool: digits = [int(c) for c in re.sub(r"[\-\s]", "", nip)] if len(digits) != 10: return False weights = [6, 5, 7, 2, 3, 4, 5, 6, 7] total = sum(d * w for d, w in zip(digits, weights)) check = total % 11 return check != 10 and check == digits[9]
total % 11 == 10, the NIP is always structurally invalid — no valid check digit exists for that combination. This is by design in the Polish NIP specification.4. Worked example — step by step
Let's verify NIP 526-104-08-28 (digits: 5261040828):
| Position | Digit | Weight | Product |
|---|---|---|---|
| 1 | 5 | 6 | 30 |
| 2 | 2 | 5 | 10 |
| 3 | 6 | 7 | 42 |
| 4 | 1 | 2 | 2 |
| 5 | 0 | 3 | 0 |
| 6 | 4 | 4 | 16 |
| 7 | 0 | 5 | 0 |
| 8 | 8 | 6 | 48 |
| 9 | 2 | 7 | 14 |
Sum = 30 + 10 + 42 + 2 + 0 + 16 + 0 + 48 + 14 = 162
162 mod 11 = 162 − 14 × 11 = 162 − 154 = 8
The remainder is 8 (not 10), so the check digit must be 8.
The 10th digit of 5261040828 is 8 — it matches. The NIP is valid.
5. Why format checks alone are not enough
A valid NIP checksum does not mean the business is active
Sole proprietors can suspend or close their business. The NIP remains valid and the MOD-11 checksum still passes — but the CEIDG status will be suspended or deleted. For any onboarding or compliance use case, you need the live registry status.
You need the owner's name and address for KYC
The NIP alone identifies a tax entity but tells you nothing about the person behind it. A CEIDG lookup returns the owner's first and last name, business address, and registration date — all required for customer due diligence (CDD) and Know Your Customer (KYC) flows.
PKD codes define what the business is allowed to do
Polish sole proprietors register the specific PKD activity codes under which they operate. For regulated industries — financial services, healthcare, transport — you may need to verify that the supplier or partner holds the correct PKD codes before contracting with them.
6. The right solution: one API call
The IsValid CEIDG API handles the full validation and lookup stack in a single GET request:
Format check
Exactly 10 digits, strips hyphens and spaces
MOD-11 checksum
Weights [6,5,7,2,3,4,5,6,7], mod 11 must equal the 10th digit
CEIDG registry lookup (optional)
Owner name, business name, address, status, REGON, start date, and PKD codes from the official government API
Get your free API key at isvalid.dev. The free tier includes 100 calls per day with no credit card required.
Full parameter reference and response schema: CEIDG API docs →
7. Python code example
Using the requests library or the standard urllib.request:
# ceidg_validator.py import os import requests API_KEY = os.environ["ISVALID_API_KEY"] BASE_URL = "https://api.isvalid.dev" def validate_ceidg(nip: str, lookup: bool = False) -> dict: params = {"value": nip} if lookup: params["lookup"] = "true" response = requests.get( f"{BASE_URL}/v0/pl/ceidg", params=params, headers={"Authorization": f"Bearer {API_KEY}"}, timeout=10, ) response.raise_for_status() return response.json() # ── NIP checksum validation only ──────────────────────────────────────────── result = validate_ceidg("9876543210") if not result["valid"]: print("Invalid NIP") else: print("NIP:", result["nip"]) # "987-654-32-10" # ── With CEIDG registry lookup ─────────────────────────────────────────────── detailed = validate_ceidg("9876543210", lookup=True) if detailed["valid"] and detailed.get("ceidg", {}).get("found"): c = detailed["ceidg"] print("Owner :", c["firstName"], c["lastName"]) print("Business :", c["businessName"]) print("Status :", c["status"]) # "active" | "suspended" | "deleted" print("City :", c["city"]) print("PKD :", c["primaryPkd"]) # e.g. "70.22.Z" print("All PKD :", c["pkd"]) # ["70.22.Z", "74.90.Z", ...]
lookup parameter is optional. Omit it for fast checksum-only validation (e.g. validating a form field). Add it when you need to confirm the entity is active and retrieve owner details.8. cURL examples
NIP checksum validation only:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/pl/ceidg?value=9876543210"
With CEIDG registry lookup:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/pl/ceidg?value=9876543210&lookup=true"
Hyphens are accepted and stripped automatically:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/pl/ceidg?value=987-654-32-10"
9. Understanding the response
Valid NIP (checksum only)
{ "valid": true, "nip": "987-654-32-10" }
Valid NIP with CEIDG lookup
{ "valid": true, "nip": "987-654-32-10", "ceidg": { "checked": true, "found": true, "status": "active", "firstName": "JAN", "lastName": "KOWALSKI", "businessName": "JK CONSULTING JAN KOWALSKI", "regon": "146123456", "city": "Warszawa", "postalCode": "00-001", "street": "Marszałkowska", "houseNumber": "1", "flatNumber": null, "startDate": "2015-03-01", "pkd": ["70.22.Z", "74.90.Z", "63.11.Z"], "primaryPkd": "70.22.Z" } }
NIP not found in CEIDG
{ "valid": true, "nip": "526-104-08-28", "ceidg": { "checked": true, "found": false } }
Invalid NIP
{ "valid": false }
10. Edge cases to handle
Entity found but status is suspended or deleted
The NIP is valid and the entity exists in CEIDG, but the business is no longer active. Always check ceidg["status"] before onboarding.
result = validate_ceidg(nip, lookup=True) ceidg = result.get("ceidg") or {} if result["valid"] and ceidg.get("found"): if ceidg["status"] != "active": # Business is suspended or deleted — do not onboard raise ValueError(f"CEIDG status: {ceidg['status']}") # Proceed with onboarding
NIP not found in CEIDG — try KRS
A valid NIP that is not in CEIDG likely belongs to a KRS company (sp. z o.o., S.A., etc.) or a non-business entity. Fall back to the VAT or KRS endpoints.
ceidg = validate_ceidg(nip, lookup=True) ceidg_data = ceidg.get("ceidg") or {} if ceidg["valid"] and ceidg_data.get("checked") and not ceidg_data.get("found"): # Not a sole proprietor — try VAT/VIES for companies vat = validate_vat(f"PL{nip}", check_vies=True) if vat["valid"] and (vat.get("vies") or {}).get("valid"): print("KRS company confirmed via VIES:", vat["vies"]["name"])
CEIDG API unavailable
When ceidg["checked"] is False, the CEIDG government API could not be reached. Handle this gracefully — accept provisionally and re-check.
result = validate_ceidg(nip, lookup=True) ceidg = result.get("ceidg") or {} if result["valid"] and ceidg and not ceidg.get("checked"): # CEIDG API temporarily unavailable schedule_recheck(nip, delay="1h") return {"status": "provisional", "reason": "ceidg_unavailable"}
Verify required PKD codes
For regulated industries, check that the entity holds the required PKD activity code before contracting.
REQUIRED_PKD = {"64", "65", "66"} # financial services divisions result = validate_ceidg(nip, lookup=True) ceidg = result.get("ceidg") or {} if ceidg.get("found"): pkd_divisions = {code[:2] for code in ceidg.get("pkd", [])} if not REQUIRED_PKD & pkd_divisions: raise ValueError( f"Entity does not hold required financial services PKD codes. " f"Has: {pkd_divisions}" )
Summary
See also
Try CEIDG validation instantly
Free tier includes 100 API calls per day. No credit card required. NIP checksum validation and optional CEIDG registry lookup included.