CUSIP Validation in Python
Format checks and check digit algorithms catch typos, but they cannot tell you whether a CUSIP actually identifies a real security. Here's how to validate CUSIPs properly in Python with a single API call.
In this guide
1. What is a CUSIP?
A CUSIP (Committee on Uniform Securities Identification Procedures) is a 9-character alphanumeric code that uniquely identifies a financial instrument in North America — stocks, bonds, mutual funds, and other registered securities. CUSIPs are assigned by CUSIP Global Services, operated by FactSet under license from the American Bankers Association (ABA).
CUSIPs are the backbone of trade settlement, clearing, and custody in the United States and Canada. They are also embedded in ISINs: a US ISIN is simply US + the 9-character CUSIP + a Luhn check digit. If you work with North American securities, you will encounter CUSIPs everywhere — from DTCC settlement instructions to Bloomberg terminal lookups.
2. CUSIP anatomy
Every CUSIP is exactly 9 characters long and consists of three parts:
| Part | Positions | Description | Example |
|---|---|---|---|
| Issuer number | 1 – 6 | Identifies the issuer (company, municipality, or government agency) | 037833 |
| Issue number | 7 – 8 | Identifies the specific security issued by that issuer | 10 |
| Check digit | 9 | Single digit computed using a Luhn-variant algorithm | 0 |
Putting it together: 037833 + 10 + 0 = 037833100 (Apple Inc. common stock). The issuer number 037833 is assigned to Apple, and 10 refers to the common stock issue. Other issue numbers for the same issuer would represent different securities — preferred stock, bonds, warrants, etc.
3. The check digit algorithm
The CUSIP check digit uses a Luhn-variant algorithm that operates on the first 8 characters. Letters are converted to numeric values (A=10, B=11, ..., Z=35), and special characters like *, @, and # map to 36, 37, and 38 respectively. Characters at even positions (0-indexed) are doubled, then digit sums are computed.
Here is what a naive Python implementation looks like:
# cusip_check.py — naive implementation (format + checksum only) import re CUSIP_RE = re.compile(r'^[A-Z0-9*@#]{8}[0-9]$') SPECIAL_CHARS = {'*': 36, '@': 37, '#': 38} def char_value(ch: str) -> int: """Convert a CUSIP character to its numeric value.""" if ch.isdigit(): return int(ch) if ch.isalpha(): return ord(ch) - 55 # A=10, B=11, ..., Z=35 if ch in SPECIAL_CHARS: return SPECIAL_CHARS[ch] raise ValueError(f"Invalid CUSIP character: {ch}") def cusip_check_digit(cusip8: str) -> int: """Compute the CUSIP check digit for the first 8 characters.""" total = 0 for i, ch in enumerate(cusip8): value = char_value(ch) if i % 2 == 1: # odd position (0-indexed) → double value *= 2 # Sum the individual digits of the (possibly doubled) value total += value // 10 + value % 10 return (10 - (total % 10)) % 10 def validate_cusip_format(cusip: str) -> dict: cusip = cusip.replace(" ", "").replace("-", "").upper() if len(cusip) != 9: return {"valid": False, "reason": "length"} if not CUSIP_RE.match(cusip): return {"valid": False, "reason": "format"} expected = cusip_check_digit(cusip[:8]) actual = int(cusip[8]) if expected != actual: return {"valid": False, "reason": "check_digit"} return { "valid": True, "issuer_number": cusip[:6], "issue_number": cusip[6:8], "check_digit": cusip[8], } # ── Example ─────────────────────────────────────────────────────────────────── print(validate_cusip_format("037833100")) # valid — Apple Inc. print(validate_cusip_format("037833105")) # invalid check digit print(validate_cusip_format("03783310")) # invalid length
4. Why manual validation is not enough
Check digits only catch typos
The Luhn-variant algorithm detects single-character errors and most transpositions. But it says nothing about whether the CUSIP was ever assigned to a real instrument. You can generate infinitely many 9-character strings that pass the check digit — most of them correspond to no security at all.
Securities get delisted and bonds mature
A CUSIP that was valid last year might now refer to a delisted stock or a matured bond. The check digit still passes — the structural format never changes — but accepting it in a settlement system is an operational error. You need to verify the CUSIP against a live data source to know its current status.
You often need the component breakdown
Extracting the issuer number and issue number from a CUSIP is trivial string slicing. But confirming that 037833 is Apple Inc. requires a lookup against an authoritative source. Without this, your "validation" is just pattern matching — it tells you the format is correct but not what the identifier means.
CUSIP data is proprietary
Unlike ISINs (which can be looked up via ESMA FIRDS for EU instruments), CUSIP data is proprietary and licensed by CUSIP Global Services. Building your own lookup database requires a commercial license and ongoing data synchronization. An API abstracts this complexity away.
5. The right solution: one API call
Instead of implementing the check digit algorithm yourself and maintaining a CUSIP database, use the IsValid CUSIP API. A single GET request validates the format, verifies the check digit, and breaks down the CUSIP into its component parts — issuer number, issue number, and check digit.
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: CUSIP Validation API docs →
6. Python code example
Using the isvalid-sdk package or the requests library. Install with pip install isvalid-sdk or pip install requests.
# cusip_validator.py import os from isvalid_sdk import IsValidConfig, create_client iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"])) result = iv.cusip("037833100") # Apple Inc. if not result["valid"]: print("Invalid CUSIP: failed format or check digit") else: print(f"Valid : {result['valid']}") print(f"Issuer number: {result['issuerNumber']}") print(f"Issue number : {result['issueNumber']}") print(f"Check digit : {result['checkDigit']}")
Expected output for 037833100:
Valid : True Issuer number: 037833 Issue number : 10 Check digit : 0
037833100 and 0378 3310 0 are handled correctly.7. cURL example
Validate a CUSIP and get the component breakdown:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/cusip?value=037833100"
Invalid CUSIP — wrong check digit:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/cusip?value=037833105"
8. Understanding the response
Response for a valid CUSIP:
{ "valid": true, "issuerNumber": "037833", "issueNumber": "10", "checkDigit": "0" }
Response for an invalid CUSIP:
{ "valid": false }
| Field | Type | Description |
|---|---|---|
| valid | boolean | true if the CUSIP passes both format validation and check digit verification |
| issuerNumber | string | The first 6 characters — identifies the issuer (only present when valid: true) |
| issueNumber | string | Characters 7–8 — identifies the specific security within the issuer |
| checkDigit | string | The 9th character — Luhn-variant check digit computed from the first 8 characters |
issuerNumber, issueNumber, checkDigit) are only present when valid is true. When the CUSIP is invalid, the response contains only valid: false.9. Edge cases to handle
Wrong check digit
A single transposed or mistyped character will produce a different check digit. The API returns valid: false — no component breakdown is provided. This is the most common error when CUSIPs are entered manually or copied from unstructured text.
result = iv.cusip("037833105") # Wrong check digit if not result["valid"]: print("CUSIP is invalid — check digit mismatch") # Prompt the user to re-enter or verify the source
CUSIPs with letters and special characters
CUSIP characters are not limited to digits. Letters A–Z and special characters (*, @, #) are valid in the issuer and issue number positions. For example, government agency CUSIPs often contain letters. Make sure your input handling does not strip or reject alphabetic characters.
# CUSIPs with letters are perfectly valid result = iv.cusip("17275R102") # Cisco Systems print(result["valid"]) # True print(result["issuerNumber"]) # 17275R
CINS codes (international CUSIPs)
CINS (CUSIP International Numbering System) codes follow the same 9-character format but use a letter in the first position to indicate the country. They use the same check digit algorithm. The IsValid API handles CINS codes the same way as domestic CUSIPs — the validation and breakdown work identically.
# CINS code — letter in first position indicates country result = iv.cusip("D18190898") # International security if result["valid"]: print(f"Issuer: {result['issuerNumber']}") # D18190
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_cusip_safe(cusip: str) -> dict | None: """Validate a CUSIP with graceful error handling.""" try: return validate_cusip(cusip) except requests.Timeout: logger.warning("CUSIP API timed out for %s", cusip) return None except requests.HTTPError as exc: logger.error("CUSIP API error %s for %s", exc.response.status_code, cusip) return None
Batch validation
When validating multiple CUSIPs — for example, processing a CSV of positions — use concurrent requests to maximize throughput. The API is stateless, so every request is independent.
from concurrent.futures import ThreadPoolExecutor cusips = ["037833100", "17275R102", "594918104", "88160R101"] with ThreadPoolExecutor(max_workers=4) as pool: results = list(pool.map(iv.cusip, cusips)) for cusip, result in zip(cusips, results): status = "valid" if result["valid"] else "INVALID" print(f"{cusip}: {status}")
Summary
See also
Validate CUSIP codes instantly
Free tier includes 100 API calls per day. No credit card required. Format validation, check digit verification, and component breakdown in a single request.