Guide · Python · SDK · REST API

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.

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:

PartPositionsDescriptionExample
Issuer number1 – 6Identifies the issuer (company, municipality, or government agency)037833
Issue number7 – 8Identifies the specific security issued by that issuer10
Check digit9Single digit computed using a Luhn-variant algorithm0

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
⚠️This implementation catches typos and transposition errors — but it cannot tell you whether the CUSIP actually identifies a real, active security. A structurally valid CUSIP might refer to a delisted stock, a matured bond, or a code that was never assigned.

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.

Full
Validation
Format + check digit
<50ms
Response time
Lightweight & fast
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: 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
The API strips whitespace and handles formatting automatically — pass the raw user input without pre-processing. Both 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
}
FieldTypeDescription
validbooleantrue if the CUSIP passes both format validation and check digit verification
issuerNumberstringThe first 6 characters — identifies the issuer (only present when valid: true)
issueNumberstringCharacters 7–8 — identifies the specific security within the issuer
checkDigitstringThe 9th character — Luhn-variant check digit computed from the first 8 characters
ℹ️Component fields (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

Do not rely on regex alone — CUSIPs contain letters and special characters
Do not treat a valid check digit as proof the CUSIP was ever assigned
Map letters to 10–35 and special characters to 36–38 before computing the check digit
Use the API to get issuer number, issue number, and check digit breakdown
Handle CINS codes the same way — same algorithm, letter in first position
Wrap API calls in try/except to handle network errors gracefully

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.