Guide · Node.js · Validation

ISIN Validation in Node.js

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 Node.js implementation looks like:

// isinCheck.js — naive implementation (format + checksum only)

/**
 * Convert ISIN characters to a digit string for the Luhn check.
 * Letters become their numeric value: A=10, B=11, ..., Z=35.
 */
function expandIsin(isin) {
  return isin
    .toUpperCase()
    .split('')
    .map(ch => {
      const code = ch.charCodeAt(0);
      // A-Z → 10-35
      if (code >= 65 && code <= 90) return String(code - 55);
      return ch;
    })
    .join('');
}

/**
 * Standard Luhn mod-10 check on a numeric string.
 */
function luhnValid(digitStr) {
  let total = 0;
  let shouldDouble = false;

  for (let i = digitStr.length - 1; i >= 0; i--) {
    let d = Number(digitStr[i]);
    if (shouldDouble) {
      d *= 2;
      if (d > 9) d -= 9;
    }
    total += d;
    shouldDouble = !shouldDouble;
  }

  return total % 10 === 0;
}

const ISIN_RE = /^[A-Z]{2}[A-Z0-9]{9}[0-9]$/;

function validateIsinFormat(isin) {
  isin = isin.replace(/\s/g, '').toUpperCase();

  if (!ISIN_RE.test(isin)) {
    return { valid: false, reason: 'format' };
  }

  if (!luhnValid(expandIsin(isin))) {
    return { valid: false, reason: 'check_digit' };
  }

  return {
    valid: true,
    countryCode: isin.slice(0, 2),
    nsin: isin.slice(2, 11),
    checkDigit: isin[11],
  };
}

// ── Example ───────────────────────────────────────────────────────────────────
console.log(validateIsinFormat('US0378331005'));   // valid — Apple Inc.
console.log(validateIsinFormat('US0378331006'));   // invalid check digit
console.log(validateIsinFormat('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. Node.js code example

Using the native fetch API (Node 18+). No dependencies required.

// isinValidator.js
const API_KEY = process.env.ISVALID_API_KEY;
const BASE_URL = 'https://api.isvalid.dev';

/**
 * Validate an ISIN and enrich it with instrument data.
 *
 * @param {string} isin - ISIN code in any format (spaces or no spaces)
 * @returns {Promise<object>} Full API response  always check result.valid first
 */
async function validateIsin(isin) {
  const params = new URLSearchParams({ value: isin });

  const response = await fetch(`${BASE_URL}/v0/isin?${params}`, {
    headers: { Authorization: `Bearer ${API_KEY}` },
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(`ISIN API error ${response.status}: ${error.message ?? 'unknown'}`);
  }

  return response.json();
}

// ── Example usage ─────────────────────────────────────────────────────────────

const result = await validateIsin('PL0000503135');  // PKN Orlen

if (!result.valid) {
  console.log('Invalid ISIN: failed format or check digit');
} else if (result.found === null) {
  console.log('ISIN is valid but data sources are unavailable');
} else if (!result.found) {
  console.log('ISIN is valid but not found in any data source');
} else {
  console.log(`Instrument : ${result.name}`);
  console.log(`FISN       : ${result.fisn}`);
  console.log(`CFI code   : ${result.cfiCode}`);
  console.log(`Currency   : ${result.currency}`);
  console.log(`Status     : ${result.status}`);
  console.log(`Data source: ${result.dataSource}`);
  if (result.ticker) {
    console.log(`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)

In an Express.js route handler, you might use it like this:

// routes/instruments.js (Express)
app.get('/instruments/check', async (req, res) => {
  const { isin } = req.query;

  if (!isin) {
    return res.status(400).json({ error: 'Missing isin parameter' });
  }

  let result;
  try {
    result = await validateIsin(isin);
  } catch {
    return res.status(502).json({ error: 'ISIN validation service unavailable' });
  }

  if (!result.valid) {
    return res.status(400).json({ error: 'Invalid ISIN' });
  }

  if (result.found && result.status === 'TERM') {
    return res.status(422).json({
      error: 'Instrument is terminated',
      name: result.name,
    });
  }

  res.json({
    isin: result.normalized,
    name: result.name,
    currency: result.currency,
    dataSource: result.dataSource,
  });
});
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.

const result = await validateIsin('DE000A0MR4U4');

if (result.found && result.status === 'TERM') {
  throw new Error(`Instrument ${result.normalized} 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.

const result = await validateIsin('XS1234567890');

if (result.valid && result.found === false) {
  // Structurally correct but unknown — log and flag for manual review
  console.log('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 && result.found === null) {
  // Sources unavailable — do not reject, retry or allow with a note
  console.log('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.

/**
 * Safe wrapper that returns null on network or API errors
 * instead of throwing.
 */
async function validateIsinSafe(isin) {
  try {
    return await validateIsin(isin);
  } catch (err) {
    if (err.cause?.code === 'UND_ERR_CONNECT_TIMEOUT') {
      console.warn(`ISIN API timed out for ${isin}`);
    } else {
      console.error(`ISIN API error for ${isin}:`, err.message);
    }
    return null;
  }
}

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.