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.
In this guide
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:
| Part | Length | Description | Example |
|---|---|---|---|
| Country code | 2 | ISO 3166-1 alpha-2 issuing country | US |
| NSIN | 9 | National Securities Identifying Number (country-specific) | 037833100 |
| Check digit | 1 | Luhn-based check digit computed over the full code | 5 |
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)
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:
| Source | Coverage | Data |
|---|---|---|
| ESMA FIRDS | EU instruments (MiFID II regulated) | Full name, FISN, CFI, currency, venue, issuer LEI, maturity, status |
| OpenFIGI | Global (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.
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 @isvalid-dev/sdk package or the native fetch API (Node 18+).
// isinValidator.js import { createClient } from '@isvalid-dev/sdk'; const iv = createClient({ apiKey: process.env.ISVALID_API_KEY }); // ── Example usage ───────────────────────────────────────────────────────────── const result = await iv.isin('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, }); });
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" }
| Field | Source | Description |
|---|---|---|
| valid | — | Format and Luhn check digit passed |
| found | — | true — instrument found; false — valid ISIN but unknown; null — data sources unavailable |
| dataSource | — | firds, openfigi, or firds+openfigi |
| name | FIRDS / OpenFIGI | Full instrument name |
| fisn | FIRDS | Financial Instrument Short Name (ISO 18774) — e.g. PKN ORLEN SA/SHS |
| cfiCode | FIRDS | 6-character Classification of Financial Instruments code (ISO 10962) |
| currency | FIRDS | ISO 4217 notional currency code |
| tradingVenue | FIRDS | ISO 10383 MIC code of the primary trading venue |
| issuerLei | FIRDS | LEI of the instrument issuer |
| maturityDate | FIRDS | Maturity/expiry date in YYYY-MM-DD; null for equities |
| status | FIRDS | ACTV — active; TERM — terminated |
| ticker / exchCode | OpenFIGI | Exchange ticker and Bloomberg exchange code |
| figi / compositeFIGI | OpenFIGI | Bloomberg 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
Node.js integration notes
In a TypeScript project, represent a validated ISIN as a branded type so the compiler enforces that only checked values flow through your pipeline: type Isin = string & { readonly __brand: 'Isin' }. The IsValid SDK ships with complete TypeScript definitions for all response fields, which means your editor provides autocomplete on the parsed result — country codes, category names, registry data — without writing manual type declarations.
ISIN validation often appears inside data ingestion pipelines: EDI feeds, supply-chain APIs, catalog imports, or financial data streams. In these contexts, validation happens at the boundary where external data enters your system. Model this as a transformation step: raw string in, validated branded type out. Use Promise.allSettled() to process batches and collect both valid and invalid results — invalid items can be quarantined for manual review without blocking the rest of the batch.
Express.js and Fastify middleware
For APIs that accept ISIN as a path or query parameter, create a route middleware that validates the value before it reaches the handler. On success, attach the full parsed API response to req.validatedData so handlers can access enrichment fields (description, category, country) without making a second API call. Cache the parsed result in Redis keyed by the normalised identifier to avoid repeat API calls for the same value within a TTL window.
When ISIN values arrive from user input or partner systems, normalise them before validation: trim surrounding whitespace, remove optional hyphens or spaces that some sources include for readability, and convert to the canonical case used by the relevant standard. Apply the same normalisation logic consistently across your codebase to prevent cache misses caused by formatting differences rather than value differences.
- Read
process.env.ISVALID_API_KEYonce at startup and store it in a module-level constant - Use
jest.mock()to stub the IsValid client in unit tests; CI pipelines should never make real API calls - Log the full validation response at
debuglevel — it often contains fields useful for debugging data quality issues - For very high throughput, consider a local pre-filter that checks format with a regex before calling the API, reducing calls for obviously malformed inputs
When making HTTP calls to the IsValid API directly (without the SDK), the choice between fetch and axios is largely a matter of preference. The native fetch API is available in Node.js 18+ without any additional dependency and is sufficient for simple request/response flows. axios adds automatic JSON parsing, request/response interceptors, and a cleaner timeout API (axios.create({ timeout: 5000 })), which makes it easier to centralise the Authorization header and retry logic in one place. For high-throughput services that make many concurrent API calls, consider undici — the HTTP client underlying Node.js fetch — used directly for its connection pooling and lower overhead.
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.