ISIN Validation in Python
Format and checksum validation is only the beginning. Here's how to enrich ISINs with real instrument data — name, FISN, CFI code, currency, and more — using ESMA FIRDS and OpenFIGI.
In this guide
1. What is an ISIN?
An International Securities Identification Number (ISIN) is a 12-character alphanumeric code defined by ISO 6166 that uniquely identifies a financial instrument worldwide — equities, bonds, derivatives, ETFs, and more.
Every ISIN has three parts:
| Part | Length | Description | Example |
|---|---|---|---|
| Country code | 2 | ISO 3166-1 alpha-2 issuing country | US |
| NSIN | 9 | National Securities Identifying Number (country-specific) | 037833100 |
| Check digit | 1 | Luhn-based check digit computed over the full code | 5 |
Putting it together: US + 037833100 + 5 = US0378331005 (Apple Inc.). ISINs are assigned by National Numbering Agencies (NNAs) — for example, CUSIP Global Services in the US, Euroclear in Belgium (prefix XS), and GPW in Poland (prefix PL).
2. The Luhn check digit algorithm
The ISIN check digit uses a variant of the Luhn mod-10 algorithm. Because ISINs contain letters (A–Z), each character is first expanded to its numeric value: digits stay as-is, and letters are replaced by their position in the alphabet plus 9 (A=10, B=11, …, Z=35). The resulting digit string is then validated with the standard Luhn algorithm.
Here is what a naive Python implementation looks like:
# isin_check.py — naive implementation (format + checksum only) import re def expand_isin(isin: str) -> str: """Convert ISIN characters to digit string for Luhn check.""" digits = "" for ch in isin.upper(): if ch.isalpha(): digits += str(ord(ch) - 55) # A=10, B=11, ..., Z=35 else: digits += ch return digits def luhn_valid(digit_str: str) -> bool: """Standard Luhn mod-10 check.""" total = 0 should_double = False for ch in reversed(digit_str): d = int(ch) if should_double: d *= 2 if d > 9: d -= 9 total += d should_double = not should_double return total % 10 == 0 ISIN_RE = re.compile(r'^[A-Z]{2}[A-Z0-9]{9}[0-9]$') def validate_isin_format(isin: str) -> dict: isin = isin.replace(" ", "").upper() if not ISIN_RE.match(isin): return {"valid": False, "reason": "format"} if not luhn_valid(expand_isin(isin)): return {"valid": False, "reason": "check_digit"} return { "valid": True, "country_code": isin[:2], "nsin": isin[2:11], "check_digit": isin[11], } # ── Example ─────────────────────────────────────────────────────────────────── print(validate_isin_format("US0378331005")) # valid — Apple Inc. print(validate_isin_format("US0378331006")) # invalid check digit print(validate_isin_format("US037833100")) # invalid format (too short)
3. Why format validation is not enough
ISINs can be terminated
When a company delists, merges, or a bond matures, the ISIN is marked as terminated (TERM). The code remains structurally valid forever — the check digit still passes — but the instrument no longer trades. Accepting a terminated ISIN as "valid" in a trading or settlement system is a serious operational error.
You need instrument metadata
Most real-world use cases need more than a boolean. What is the instrument name? What currency does it trade in? Is it an equity or a bond? What is its CFI code under ISO 10962? What is the FISN (Financial Instrument Short Name) under ISO 18774? None of this is encoded in the ISIN itself.
Two authoritative data sources cover different markets
No single database covers all ISINs globally:
| Source | Coverage | Data |
|---|---|---|
| ESMA FIRDS | EU instruments (MiFID II regulated) | Full name, FISN, CFI, currency, venue, issuer LEI, maturity, status |
| OpenFIGI | Global (US equities and more) | Name, ticker, exchange code, security type, FIGI |
4. The right solution: one API call
Instead of building your own Luhn validator, maintaining FIRDS sync infrastructure, and integrating OpenFIGI separately, use the IsValid ISIN API. A single GET request handles format validation, check digit verification, FIRDS lookup, and OpenFIGI lookup — all results merged into a single response.
Get your free API key at isvalid.dev. The free tier includes 100 calls per day — enough for most development and low-volume production use.
Full parameter reference and response schema: ISIN Validation API docs →
5. Python code example
Using the requests library (install with pip install requests).
# isin_validator.py import os import requests API_KEY = os.environ["ISVALID_API_KEY"] BASE_URL = "https://api.isvalid.dev" def validate_isin(isin: str) -> dict: """ Validate an ISIN and enrich it with instrument data. Returns the full API response as a dict. Always check result["valid"] before accessing enrichment fields. """ response = requests.get( f"{BASE_URL}/v0/isin", params={"value": isin}, headers={"Authorization": f"Bearer {API_KEY}"}, timeout=10, ) response.raise_for_status() return response.json() # ── Example usage ───────────────────────────────────────────────────────────── result = validate_isin("PL0000503135") # PKN Orlen if not result["valid"]: print("Invalid ISIN: failed format or check digit") elif not result.get("found"): if result["found"] is None: print("ISIN is valid but data sources are unavailable") else: print("ISIN is valid but not found in any data source") else: print(f"Instrument : {result['name']}") print(f"FISN : {result['fisn']}") print(f"CFI code : {result['cfiCode']}") print(f"Currency : {result['currency']}") print(f"Status : {result['status']}") print(f"Data source: {result['dataSource']}") if result.get("ticker"): print(f"Ticker : {result['ticker']} ({result['exchCode']})")
Expected output for PL0000503135:
Instrument : PKN ORLEN SA FISN : PKN ORLEN SA/SHS CFI code : ESVUFR Currency : PLN Status : ACTV Data source: firds+openfigi Ticker : PKN (WSE)
pl 0000 503135 and PL0000503135 are handled correctly.6. cURL example
Validate an ISIN and get full instrument enrichment:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/isin?value=PL0000503135"
US equity — data from OpenFIGI (not in FIRDS):
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/isin?value=US0378331005"
7. Understanding the response
Response for a Polish equity found in both FIRDS and OpenFIGI:
{ "valid": true, "countryCode": "PL", "countryName": "Poland", "nsin": "000PKN0RH1", "checkDigit": "6", "found": true, "dataSource": "firds+openfigi", "name": "PKN ORLEN SA", "fisn": "PKN ORLEN SA/SHS", "cfiCode": "ESVUFR", "currency": "PLN", "tradingVenue": "XWAR", "issuerLei": "259400VMBIZKDL26YB25", "maturityDate": null, "status": "ACTV", "ticker": "PKN", "exchCode": "WSE", "securityType": "Common Stock", "marketSector": "Equity", "figi": "BBG000BW0VH6", "compositeFIGI": "BBG000BW0VH6" }
| Field | Source | Description |
|---|---|---|
| valid | — | Format and Luhn check digit passed |
| found | — | true — instrument found; false — valid ISIN but unknown; null — data sources unavailable |
| dataSource | — | firds, openfigi, or firds+openfigi |
| name | FIRDS / OpenFIGI | Full instrument name |
| fisn | FIRDS | Financial Instrument Short Name (ISO 18774) — e.g. PKN ORLEN SA/SHS |
| cfiCode | FIRDS | 6-character Classification of Financial Instruments code (ISO 10962) |
| currency | FIRDS | ISO 4217 notional currency code |
| tradingVenue | FIRDS | ISO 10383 MIC code of the primary trading venue |
| issuerLei | FIRDS | LEI of the instrument issuer |
| maturityDate | FIRDS | Maturity/expiry date in YYYY-MM-DD; null for equities |
| status | FIRDS | ACTV — active; TERM — terminated |
| ticker / exchCode | OpenFIGI | Exchange ticker and Bloomberg exchange code |
| figi / compositeFIGI | OpenFIGI | Bloomberg Financial Instrument Global Identifier |
8. Edge cases to handle
Terminated instruments
A bond that matured or a company that delisted will have status: "TERM". The ISIN is structurally valid, the check digit passes, and it may even appear in the database — but it no longer trades. Always check the status before accepting an ISIN in settlement or trading workflows.
result = validate_isin("DE000A0MR4U4") if result.get("found") and result.get("status") == "TERM": raise ValueError(f"Instrument {isin} is terminated and no longer trades")
Valid ISIN, not found in any source
Some ISINs are structurally valid and pass the check digit but are not listed in FIRDS or OpenFIGI. This can happen for private placements, unlisted instruments, or very recently issued securities that have not yet appeared in either database. The response returns found: false.
result = validate_isin("XS1234567890") if result["valid"] and result["found"] is False: # Structurally correct but unknown — log and flag for manual review print("ISIN passes checksum but has no instrument record")
Data sources temporarily unavailable
When both FIRDS and OpenFIGI are unreachable, the API returns found: null. This means the structural check succeeded but enrichment data could not be fetched. Treat this as "validation inconclusive", not as invalid.
if result["valid"] and result["found"] is None: # Sources unavailable — do not reject, retry or allow with a note print("ISIN format valid, enrichment unavailable — retry later")
Network failures in your code
Always wrap the API call with error handling. A network timeout should not crash your application — decide upfront whether to fail open or fail closed on API unavailability.
import requests def validate_isin_safe(isin: str) -> dict | None: try: return validate_isin(isin) except requests.Timeout: logger.warning("ISIN API timed out for %s", isin) return None except requests.HTTPError as exc: logger.error("ISIN API error %s for %s", exc.response.status_code, isin) return None
Summary
See also
Try ISIN validation instantly
Free tier includes 100 API calls per day. No credit card required. FIRDS and OpenFIGI enrichment included for every valid ISIN.