Guide · Node.js · SDK · REST API

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.

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:

PartPositionsLengthDescription
Issuer number1 – 66Identifies the issuer (company, government entity, municipality)
Issue number7 – 82Distinguishes multiple securities from the same issuer (e.g. common stock vs. preferred)
Check digit91Single 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:

CUSIPIssuer (1–6)Issue (7–8)Check (9)Security
037833100037833100Apple Inc. (Common Stock)
594918104594918104Microsoft Corp. (Common Stock)
88160R10188160R101Tesla Inc. (Common Stock)
912828Z87912828Z87US Treasury Bond
ℹ️The issuer number 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:

CharacterNumeric value
0 – 90 – 9 (face value)
A – Z10 – 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:

  1. For each of the first 8 characters, convert it to its numeric value using the mapping above.
  2. If the character is in an even position (2nd, 4th, 6th, 8th — using 1-based indexing), multiply its value by 2.
  3. 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).
  4. Sum all 8 digit-sum values to get S.
  5. 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)
⚠️This implementation catches format errors and invalid check digits — but it tells you nothing about the underlying security. A structurally valid CUSIP might refer to a delisted equity, a matured bond, or a CUSIP that was reassigned after a corporate action. The check digit algorithm alone cannot distinguish a live Apple share from a defunct instrument.

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:

US ISIN = "US" + CUSIP (9 chars) + ISIN check digit

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.

Full
Validation
Format + modified Luhn check digit
Parsed
Decomposition
Issuer, issue number, check digit
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. 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,
  });
});
The API strips whitespace and uppercases the input automatically — pass the raw user input without pre-processing. Both 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
}
FieldTypeDescription
validbooleantrue if the CUSIP passes format validation and the modified Luhn check digit is correct
issuerNumberstringThe 6-character issuer number (positions 1 – 6). Only present when valid is true
issueNumberstringThe 2-character issue number (positions 7 – 8). Distinguishes different securities from the same issuer. Only present when valid
checkDigitstringThe 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

Do not rely on regex alone — CUSIPs contain letters and special characters that need the modified Luhn algorithm
Do not assume a valid check digit means the CUSIP is active — corporate actions can retire CUSIPs
Use the correct character mapping: 0-9, A-Z (10-35), * (36), @ (37), # (38)
Remember that CUSIP and ISIN check digits use different algorithms — validate each independently
Handle CINS (international CUSIPs) with the same algorithm but flag them if your system is US-only
Wrap API calls with error handling — decide whether to fail open or fail closed

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.