CUSIP Validation in Node.js
A 9-character identifier underpins every trade in the US capital markets. Here's how to validate CUSIPs correctly — check digit algorithm, edge cases, and a single API call that does it all.
In this guide
1. What is a CUSIP?
A CUSIP (Committee on Uniform Securities Identification Procedures) is a 9-character alphanumeric identifier assigned to financial instruments traded in the United States and Canada. Introduced in 1964 and maintained by CUSIP Global Services (a subsidiary of the American Bankers Association, operated by FactSet), CUSIPs serve as the backbone of securities identification across North American capital markets.
CUSIPs appear everywhere in the US financial ecosystem. The Depository Trust Company (DTC) — the central securities depository for US equities and bonds — uses CUSIPs as the primary key for settlement and custody. When a broker executes a trade on the NYSE or NASDAQ, the CUSIP is the identifier that flows through the National Securities Clearing Corporation (NSCC) for clearance and on to the DTC for final settlement.
The SEC requires CUSIPs in many regulatory filings. Form 13F (institutional investment manager holdings), Form N-PORT (mutual fund portfolio holdings), and TRACE (Trade Reporting and Compliance Engine for fixed-income) all reference securities by CUSIP. Without a valid CUSIP, an instrument effectively does not exist in the US regulatory reporting infrastructure.
CUSIPs cover a broad range of instruments: common and preferred stock, corporate and government bonds, commercial paper, municipal securities, certificates of deposit, and syndicated loans. US Treasury securities use a special CUSIP range managed by the Bureau of the Fiscal Service.
2. CUSIP anatomy
Every CUSIP is exactly 9 characters long, divided into three parts:
| Part | Positions | Length | Description |
|---|---|---|---|
| Issuer number | 1 – 6 | 6 | Identifies the issuer (company, government entity, municipality) |
| Issue number | 7 – 8 | 2 | Distinguishes multiple securities from the same issuer (e.g. common stock vs. preferred) |
| Check digit | 9 | 1 | Single digit computed using the modified Luhn algorithm |
Characters in positions 1 – 8 can be digits (0 – 9), uppercase letters (A – Z), or the special characters *, @, and #. The check digit in position 9 is always a single digit (0 – 9).
Here are some well-known examples:
| CUSIP | Issuer (1–6) | Issue (7–8) | Check (9) | Security |
|---|---|---|---|---|
| 037833100 | 037833 | 10 | 0 | Apple Inc. (Common Stock) |
| 594918104 | 594918 | 10 | 4 | Microsoft Corp. (Common Stock) |
| 88160R101 | 88160R | 10 | 1 | Tesla Inc. (Common Stock) |
| 912828Z87 | 912828 | Z8 | 7 | US Treasury Bond |
037833 belongs to Apple. Once assigned, this 6-character prefix identifies the issuer across all of its securities. The issue number 10 typically denotes common stock; other issue numbers distinguish preferred shares, bonds, warrants, and other instrument types issued by the same company.3. The check digit algorithm
The CUSIP check digit uses a modified Luhn algorithm. Unlike the standard Luhn used for credit card numbers (which operates only on digits), the CUSIP variant must handle an extended alphanumeric character set. The mapping works as follows:
| Character | Numeric value |
|---|---|
| 0 – 9 | 0 – 9 (face value) |
| A – Z | 10 – 35 (A=10, B=11, …, Z=35) |
| * | 36 |
| @ | 37 |
| # | 38 |
The algorithm processes the first 8 characters (positions 1 – 8) and produces the 9th character (the check digit). Here is the step-by-step procedure:
- For each of the first 8 characters, convert it to its numeric value using the mapping above.
- If the character is in an even position (2nd, 4th, 6th, 8th — using 1-based indexing), multiply its value by 2.
- For each resulting value, compute the digit sum: divide by 10, add quotient and remainder (equivalent to summing the digits of a two-digit number).
- Sum all 8 digit-sum values to get
S. - The check digit is
(10 - (S % 10)) % 10.
Here is what a naive Node.js implementation looks like:
// cusipCheck.js — naive implementation (format + check digit only) /** * Map a CUSIP character to its numeric value. * 0-9 = 0-9, A-Z = 10-35, * = 36, @ = 37, # = 38 */ function charValue(ch) { const code = ch.charCodeAt(0); if (code >= 48 && code <= 57) return code - 48; // '0'-'9' if (code >= 65 && code <= 90) return code - 65 + 10; // 'A'-'Z' if (ch === '*') return 36; if (ch === '@') return 37; if (ch === '#') return 38; throw new Error(`Invalid CUSIP character: ${ch}`); } /** * Compute the CUSIP check digit using the modified Luhn algorithm. * Input: the first 8 characters of a CUSIP. * Returns: the expected check digit (0-9). */ function computeCheckDigit(cusip8) { let sum = 0; for (let i = 0; i < 8; i++) { let v = charValue(cusip8[i]); // Double values at even positions (0-based: positions 1, 3, 5, 7) if (i % 2 === 1) { v *= 2; } // Digit sum: add tens digit and units digit sum += Math.floor(v / 10) + (v % 10); } return (10 - (sum % 10)) % 10; } const CUSIP_RE = /^[A-Z0-9*@#]{8}[0-9]$/; function validateCusipFormat(cusip) { cusip = cusip.replace(/\s/g, '').toUpperCase(); if (cusip.length !== 9 || !CUSIP_RE.test(cusip)) { return { valid: false, reason: 'format' }; } const expected = computeCheckDigit(cusip.slice(0, 8)); const actual = Number(cusip[8]); if (expected !== actual) { return { valid: false, reason: 'check_digit' }; } return { valid: true, issuerNumber: cusip.slice(0, 6), issueNumber: cusip.slice(6, 8), checkDigit: cusip[8], }; } // ── Examples ────────────────────────────────────────────────────────────────── console.log(validateCusipFormat('037833100')); // valid — Apple Inc. console.log(validateCusipFormat('594918104')); // valid — Microsoft Corp. console.log(validateCusipFormat('037833109')); // invalid check digit console.log(validateCusipFormat('03783310')); // invalid format (too short)
4. Why manual validation isn't enough
Alphanumeric mapping is non-standard
The CUSIP character mapping (A=10 through Z=35, plus three special characters) is unique to CUSIPs. It is not the same as the ISIN Luhn expansion, and it is not the same as standard Luhn for credit cards. Developers who port a generic Luhn implementation frequently get it wrong because they forget the doubling rule applies to even-indexed positions (1-based) and that the special characters *, @, and # must be handled.
CUSIP and ISIN are related but different
For US and Canadian securities, the ISIN is built directly from the CUSIP:
For example, Apple's CUSIP 037833100 becomes ISIN US0378331005. The CUSIP check digit (0) and the ISIN check digit (5) are computed with different algorithms operating on different character sets. Validating one does not validate the other. Systems that accept both formats need to handle each algorithm independently.
Private placements use a different scheme
CUSIPs for Rule 144A private placements and other restricted securities follow a different numbering convention. These identifiers may use character ranges that are rare in public CUSIPs, and they are not always listed in the same databases. A validation system that only handles the "happy path" of publicly traded equities will silently reject valid private placement CUSIPs.
Corporate actions change CUSIPs
When a company undergoes a merger, spinoff, stock split with a symbol change, or other corporate action, a new CUSIP may be assigned. The old CUSIP remains structurally valid but no longer maps to an active security. DTC settlement systems reject trades submitted with stale CUSIPs, causing costly failed settlements. You need a data source — not just an algorithm — to know whether a CUSIP is current.
5. The right solution: one API call
Instead of implementing the modified Luhn algorithm yourself and maintaining a reference database of active CUSIPs, use the IsValid CUSIP API. A single GET /v0/cusip request handles format validation, check digit verification, and structural decomposition — all in one call.
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. Node.js code example
Using the @isvalid-dev/sdk package or the native fetch API (Node 18+).
// cusipValidator.js import { createClient } from '@isvalid-dev/sdk'; const iv = createClient({ apiKey: process.env.ISVALID_API_KEY }); // ── Example usage ───────────────────────────────────────────────────────────── const result = await iv.cusip('037833100'); // Apple Inc. if (!result.valid) { console.log('Invalid CUSIP: failed format or check digit'); } else { console.log(`Valid CUSIP : ${result.valid}`); console.log(`Issuer number : ${result.issuerNumber}`); console.log(`Issue number : ${result.issueNumber}`); console.log(`Check digit : ${result.checkDigit}`); }
Expected output for 037833100:
Valid CUSIP : true Issuer number : 037833 Issue number : 10 Check digit : 0
In an Express.js route handler, you might use it like this:
// routes/securities.js (Express) app.get('/securities/validate-cusip', async (req, res) => { const { cusip } = req.query; if (!cusip) { return res.status(400).json({ error: 'Missing cusip parameter' }); } let result; try { result = await validateCusip(cusip); } catch { return res.status(502).json({ error: 'CUSIP validation service unavailable' }); } if (!result.valid) { return res.status(400).json({ error: 'Invalid CUSIP', cusip, }); } res.json({ cusip, issuerNumber: result.issuerNumber, issueNumber: result.issueNumber, checkDigit: result.checkDigit, }); });
037 833 100 and 037833100 are handled correctly.7. cURL example
Validate a CUSIP from the command line:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/cusip?value=037833100"
Microsoft CUSIP:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/cusip?value=594918104"
Invalid check digit — the API returns valid: false:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/cusip?value=037833109"
8. Understanding the response
Response for a valid CUSIP:
{ "valid": true, "issuerNumber": "037833", "issueNumber": "10", "checkDigit": "0" }
Response for an invalid CUSIP (bad check digit):
{ "valid": false }
| Field | Type | Description |
|---|---|---|
| valid | boolean | true if the CUSIP passes format validation and the modified Luhn check digit is correct |
| issuerNumber | string | The 6-character issuer number (positions 1 – 6). Only present when valid is true |
| issueNumber | string | The 2-character issue number (positions 7 – 8). Distinguishes different securities from the same issuer. Only present when valid |
| checkDigit | string | The single check digit (position 9), computed via modified Luhn. Only present when valid |
9. Edge cases to handle
(a) CINS numbers — international securities
The CUSIP International Numbering System (CINS) extends CUSIP to international securities. A CINS number has the same 9-character structure, but the first character is always a letter (A – Z) that indicates the country or region of the issuer. For example, a Bermuda-domiciled company starts with G.
CINS numbers use the same modified Luhn check digit algorithm as domestic CUSIPs. Your validation logic does not need to change — but if your system is intended exclusively for US-domiciled securities, you may want to flag CINS identifiers (first character is a letter) separately.
const result = await iv.cusip('G0250X107'); // CINS — international security if (result.valid) { const isCins = /^[A-Z]/.test(result.issuerNumber); if (isCins) { console.log('Valid CINS (international CUSIP)'); } else { console.log('Valid domestic CUSIP'); } }
(b) Private placement identifiers
Private placement CUSIPs (PP CUSIPs) are assigned to securities offered under SEC Rule 144A and Regulation S. These identifiers follow the same structural rules but are drawn from reserved issuer number ranges. They are less widely distributed and may not appear in standard market data feeds.
The check digit algorithm works identically for private placements. The challenge is not validation — it is recognizing that a structurally valid CUSIP may refer to a restricted security that requires different handling in your workflow (e.g., verifying the holder's qualified institutional buyer status).
// Private placement CUSIPs are structurally identical // — validate them the same way, but handle downstream differently const result = await iv.cusip('123456AB9'); if (result.valid) { // Check your internal rules for private placement eligibility console.log('Valid CUSIP — check if restricted security'); }
(c) CUSIP-to-ISIN mapping
A US ISIN is constructed by prepending the country code US (or CA for Canadian securities) to the 9-character CUSIP, then computing a new ISIN check digit using the standard Luhn algorithm on the expanded character sequence. The two check digits (CUSIP's and ISIN's) are computed with different algorithms.
If your system receives ISINs that embed CUSIPs, validate both layers separately: first confirm the ISIN check digit, then extract the 9-character CUSIP substring and validate its check digit independently.
// Validate a US ISIN and its embedded CUSIP separately const isinResult = await iv.isin('US0378331005'); // Full ISIN const cusipResult = await iv.cusip('037833100'); // Embedded CUSIP if (isinResult.valid && cusipResult.valid) { console.log('Both ISIN and embedded CUSIP are valid'); } // The CUSIP is characters 3-11 of a US/CA ISIN const embeddedCusip = 'US0378331005'.slice(2, 11); // '037833100' console.log(`Embedded CUSIP: ${embeddedCusip}`);
(d) 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.
/** * Safe wrapper that returns null on network or API errors * instead of throwing. */ async function validateCusipSafe(cusip) { try { return await validateCusip(cusip); } catch (err) { if (err.cause?.code === 'UND_ERR_CONNECT_TIMEOUT') { console.warn(`CUSIP API timed out for ${cusip}`); } else { console.error(`CUSIP API error for ${cusip}:`, err.message); } return null; } }
10. Summary
See also
Validate CUSIP codes instantly
Free tier includes 100 API calls per day. No credit card required. Format validation, modified Luhn check digit verification, and structural decomposition in a single call.