REGON Validation in Python
The Polish statistical business registry number — two lengths, two different weight sets, and an optional GUS BIR lookup. Here's how to validate it correctly in Python.
In this guide
1. What is REGON?
REGON (Rejestr Gospodarki Narodowej) is the Polish National Business Registry Number assigned by GUS (Glowny Urzad Statystyczny — Central Statistical Office). Every legal entity and natural person conducting business activity in Poland receives a REGON number. It is used in:
- Tax filings and invoices
- Public procurement
- National Court Register (KRS) entries
- Social security (ZUS) registration
Two types exist:
- 9-digit REGON identifies the entire entity (company, NGO, sole proprietor)
- 14-digit REGON identifies a local unit (branch, office, factory) of an entity
| Entity | REGON | Type |
|---|---|---|
| Zaklad Ubezpieczen Spolecznych | 017344390 | 9-digit (entity) |
| ZUS Oddzial w Warszawie | 01734439000317 | 14-digit (local unit) |
2. Structure — 9-digit vs 14-digit
Both forms are purely numeric strings. Their internal structure determines how the check digit is computed:
| Form | Positions | Description |
|---|---|---|
| 9-digit | 1–8 | Base number |
| 9 | Check digit (computed over digits 1–8) | |
| 14-digit | 1–9 | Full 9-digit REGON of the parent entity |
| 10–13 | Local unit number | |
| 14 | Check digit (computed over digits 1–13) |
3. The MOD-11 checksum algorithm
REGON uses a weighted MOD-11 checksum. Each digit is multiplied by a fixed weight, the products are summed, and the remainder after dividing by 11 produces the check digit. There are two separate weight sets — one for the 9-digit form and another for the 14-digit form.
9-digit REGON
- Multiply each of the first 8 digits by its weight:
[8, 9, 2, 3, 4, 5, 6, 7] - Sum all products
- Calculate
sum mod 11 - If the remainder is 10, the check digit is 0; otherwise the check digit equals the remainder
- Compare the result with the 9th digit
14-digit REGON
- First, validate the embedded 9-digit prefix using the 9-digit algorithm above
- Then multiply each of the first 13 digits by weights:
[2, 4, 8, 5, 0, 9, 7, 3, 6, 1, 2, 4, 8] - Sum all products, compute
sum mod 11, same rule: if remainder is 10, check digit is 0 - Compare the result with the 14th digit
Here is a straightforward Python implementation of both variants using sum() and zip() for the weighted sum:
def validate_regon_9(regon: str) -> bool: digits = [int(c) for c in regon] weights = [8, 9, 2, 3, 4, 5, 6, 7] weighted_sum = sum(d * w for d, w in zip(digits, weights)) check = 0 if weighted_sum % 11 == 10 else weighted_sum % 11 return check == digits[8] def validate_regon_14(regon: str) -> bool: if not validate_regon_9(regon[:9]): return False digits = [int(c) for c in regon] weights = [2, 4, 8, 5, 0, 9, 7, 3, 6, 1, 2, 4, 8] weighted_sum = sum(d * w for d, w in zip(digits, weights)) check = 0 if weighted_sum % 11 == 10 else weighted_sum % 11 return check == digits[13]
?lookup=true parameter to check the GUS BIR registry.4. Worked example — step by step
Let's walk through the 9-digit REGON 123456785 to see the MOD-11 algorithm in action:
| Position | Digit | Weight | Product |
|---|---|---|---|
| 1 | 1 | 8 | 8 |
| 2 | 2 | 9 | 18 |
| 3 | 3 | 2 | 6 |
| 4 | 4 | 3 | 12 |
| 5 | 5 | 4 | 20 |
| 6 | 6 | 5 | 30 |
| 7 | 7 | 6 | 42 |
| 8 | 8 | 7 | 56 |
Sum = 8 + 18 + 6 + 12 + 20 + 30 + 42 + 56 = 192
192 mod 11 = 192 − 17 × 11 = 192 − 187 = 5
The remainder is 5, which is not 10, so the check digit is 5.
The 9th digit of 123456785 is 5 — it matches. The REGON is structurally valid.
Here is the same calculation expressed in Python:
regon = "123456785" digits = [int(c) for c in regon] weights = [8, 9, 2, 3, 4, 5, 6, 7] weighted_sum = sum(d * w for d, w in zip(digits, weights)) print(f"Sum: {weighted_sum}") # Sum: 192 remainder = weighted_sum % 11 check = 0 if remainder == 10 else remainder print(f"Check digit: {check}") # Check digit: 5 print(f"Valid: {check == digits[8]}") # Valid: True
5. Why format checks alone are not enough
A valid checksum does not mean the entity exists
Companies close, merge, or are deleted from the registry. The number remains mathematically valid — the MOD-11 checksum will still pass — but the entity behind it is gone. Relying solely on checksum validation gives you a false sense of confidence.
GUS BIR has the authoritative data
The Central Statistical Office maintains the official REGON registry through its BIR (Baza Internetowa REGON) system. Only a BIR lookup can confirm the entity's name, NIP, address, and whether business activity is still ongoing. The checksum tells you nothing about any of these.
14-digit REGONs add a second layer of complexity
A 14-digit REGON can have a valid checksum at both levels — the 9-digit prefix passes its own MOD-11 check, and the full 14-digit number passes its separate MOD-11 check — and still refer to a closed local unit. Double validation (structure + registry) is needed to be sure.
6. The right solution: one API call
The IsValid REGON API handles the full validation stack in a single GET request:
Format check
9 or 14 digits, all numeric
MOD-11 checksum
Correct weight set for 9-digit and 14-digit forms
GUS BIR lookup (optional)
Name, NIP, address, and activity status from the official registry
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: REGON Validation API docs →
7. Python code example
Using the requests library — the de facto standard for HTTP in Python. Install it with pip install requests.
# regon_validator.py import os import requests API_KEY = os.environ["ISVALID_API_KEY"] BASE_URL = "https://api.isvalid.dev" def validate_regon(regon: str, *, lookup: bool = False) -> dict: """Validate a Polish REGON number using the IsValid API. Args: regon: 9-digit or 14-digit REGON number. lookup: If True, query the GUS BIR registry for entity details. Returns: Validation result as a dictionary. Raises: requests.HTTPError: If the API returns a non-2xx status. """ params = {"value": regon} if lookup: params["lookup"] = "true" response = requests.get( f"{BASE_URL}/v0/pl/regon", params=params, headers={"Authorization": f"Bearer {API_KEY}"}, ) response.raise_for_status() return response.json() # ── Basic validation ──────────────────────────────────────────────────────── result = validate_regon("123456785") if not result["valid"]: print("Invalid REGON") else: print(f"Type: {result['type']}") # "entity" or "local-unit" # ── With GUS BIR lookup ───────────────────────────────────────────────────── detailed = validate_regon("123456785", lookup=True) if detailed["valid"] and detailed.get("regon", {}).get("found"): print(f"Name : {detailed['regon']['name']}") print(f"NIP : {detailed['regon']['nip']}") print(f"City : {detailed['regon']['city']}") print(f"Street : {detailed['regon']['street']}")
In a web application, you might use it like this with Flask:
# app.py (Flask) from flask import Flask, request, jsonify app = Flask(__name__) @app.post("/verify-company") def verify_company(): data = request.get_json() try: result = validate_regon(data["regon"], lookup=True) except requests.RequestException: return jsonify(error="REGON validation service unavailable"), 502 if not result["valid"]: return jsonify(error="Invalid REGON number"), 400 regon_data = result.get("regon", {}) if regon_data.get("found") and regon_data.get("activityEndDate"): return jsonify(error="Company is no longer active"), 400 return jsonify( valid=True, companyName=regon_data.get("name"), nip=regon_data.get("nip"), city=regon_data.get("city"), )
lookup parameter is optional. Use it when you need the entity's name, NIP, or address. Omit it for fast checksum-only validation.8. cURL examples
Basic 9-digit validation:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/pl/regon?value=123456785"
With GUS BIR lookup:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/pl/regon?value=123456785&lookup=true"
14-digit local unit:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/pl/regon?value=12345678512347"
9. Understanding the response
Valid REGON (basic)
{ "valid": true, "type": "entity" }
Valid REGON (with lookup)
{ "valid": true, "type": "entity", "regon": { "checked": true, "found": true, "name": "EXAMPLE COMPANY SP. Z O.O.", "nip": "1234567890", "voivodeship": "MAZOWIECKIE", "district": "m. st. Warszawa", "community": "Srodmiescie", "city": "Warszawa", "postalCode": "00-001", "street": "ul. Przykladowa", "houseNumber": "10", "flatNumber": "5", "activityEndDate": null } }
Invalid REGON
{ "valid": false }
| Field | Type | Description |
|---|---|---|
| valid | boolean | Whether the REGON passed format and checksum validation |
| type | string | "entity" (9-digit) or "local-unit" (14-digit). Only present when valid: true |
| regon | object | GUS BIR lookup result. Only present when lookup=true and valid: true |
| regon.checked | boolean | Whether the GUS BIR API was successfully queried |
| regon.found | boolean | Whether the entity was found in the registry. Only present when checked: true |
| regon.name | string | Full legal name of the entity |
| regon.nip | string | NIP (tax identification number) |
| regon.voivodeship | string | Voivodeship (province) |
| regon.city | string | City |
| regon.postalCode | string | Postal code |
| regon.street | string | Street name |
| regon.houseNumber | string | Building number |
| regon.flatNumber | string | null | Apartment/unit number |
| regon.activityEndDate | string | null | Date when business activity ended (null if still active) |
10. Edge cases to handle
GUS BIR unavailable
The GUS BIR SOAP API has occasional downtimes. When lookup=true but the API is unreachable, the response includes regon.checked: false with reason: "unavailable". Your code should handle this gracefully.
result = validate_regon("123456785", lookup=True) if result["valid"] and "regon" in result: regon_data = result["regon"] if not regon_data["checked"]: # GUS BIR was unreachable — checksum passed but no registry data print("GUS BIR unavailable, retry later") elif regon_data["found"]: print(f"Entity: {regon_data['name']}") else: print("Not found in the registry")
Dissolved entities
A REGON that passes checksum validation may belong to a dissolved company. The activityEndDate field in the lookup response tells you when the entity stopped operating. Always check this field for KYC/onboarding flows.
result = validate_regon("123456785", lookup=True) if result["valid"] and result.get("regon", {}).get("found"): if result["regon"]["activityEndDate"]: print(f"Entity dissolved on: {result['regon']['activityEndDate']}") # Reject for onboarding — this company is no longer active else: print("Entity is active")
14-digit with valid prefix but invalid suffix
A 14-digit REGON where the first 9 digits pass the 9-digit checksum but the full 14-digit checksum fails. The API returns { "valid": false } in this case — both checksums must pass for the number to be considered valid.
# 9-digit prefix is valid, but full 14-digit checksum fails result = validate_regon("12345678500000") print(result["valid"]) # False
Leading zeros
REGONs can start with 0 (e.g. 017344390). Do not store them as integers — use strings to preserve leading zeros. Casting to an integer would turn 017344390 into 17344390, which is only 8 digits and will fail validation.
# WRONG — loses the leading zero regon = int("017344390") # 17344390 — 8 digits, invalid # CORRECT — preserve as string regon = "017344390" # 9 digits, ready for validation
Summary
See also
Try REGON validation instantly
Free tier includes 100 API calls per day. No credit card required. Checksum validation and optional GUS BIR lookup included.