Guide · Python · REST API

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.

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:

PartLengthDescriptionExample
Country code2ISO 3166-1 alpha-2 issuing countryUS
NSIN9National Securities Identifying Number (country-specific)037833100
Check digit1Luhn-based check digit computed over the full code5

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)
⚠️This implementation catches format errors and invalid check digits — but it cannot tell you what the instrument is, whether it is still active, or what currency it trades in. A structurally valid ISIN might refer to a delisted bond, a terminated derivative, or simply an ISIN that was never issued.

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:

SourceCoverageData
ESMA FIRDSEU instruments (MiFID II regulated)Full name, FISN, CFI, currency, venue, issuer LEI, maturity, status
OpenFIGIGlobal (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.

2
Data sources
FIRDS + OpenFIGI, parallel
~700k
FIRDS instruments
EU MiFID II, daily sync
100/day
Free tier
no credit card

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)
The API strips whitespace and uppercases the input automatically — pass the raw user input without pre-processing. Both 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"
}
FieldSourceDescription
validFormat and Luhn check digit passed
foundtrue — instrument found; false — valid ISIN but unknown; null — data sources unavailable
dataSourcefirds, openfigi, or firds+openfigi
nameFIRDS / OpenFIGIFull instrument name
fisnFIRDSFinancial Instrument Short Name (ISO 18774) — e.g. PKN ORLEN SA/SHS
cfiCodeFIRDS6-character Classification of Financial Instruments code (ISO 10962)
currencyFIRDSISO 4217 notional currency code
tradingVenueFIRDSISO 10383 MIC code of the primary trading venue
issuerLeiFIRDSLEI of the instrument issuer
maturityDateFIRDSMaturity/expiry date in YYYY-MM-DD; null for equities
statusFIRDSACTV — active; TERM — terminated
ticker / exchCodeOpenFIGIExchange ticker and Bloomberg exchange code
figi / compositeFIGIOpenFIGIBloomberg 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

Do not rely on regex alone — ISIN format is not a single pattern
Do not treat a valid check digit as "instrument exists and is active"
Expand letters to digits before running the Luhn algorithm
Check status: TERM for terminated instruments in trading flows
Use FIRDS for EU instruments, OpenFIGI as global fallback
Handle found: null gracefully — do not reject on source unavailability

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.