LEI Validation in JavaScript — GLEIF Lookup Without Rate Limits
How to validate Legal Entity Identifiers, why mod-97 alone isn't enough, and how to enrich LEIs with entity data from a 2.3M record GLEIF database.
In this guide
1. What is an LEI?
If you work in finance, compliance, or regulatory reporting, you've seen LEIs everywhere. Every MiFID II transaction report, every EMIR derivative trade, every SEC filing — they all require a valid Legal Entity Identifier.
An LEI (ISO 17442) is a 20-character alphanumeric code that uniquely identifies legal entities participating in financial transactions. It was introduced after the 2008 crisis because regulators couldn't trace exposure across institutions — nobody had a universal ID for companies.
Yet most developers treat LEI validation as a simple format check. It's not. A syntactically valid LEI can belong to an entity that merged, dissolved, or had its registration lapsed years ago. If you're not checking against the GLEIF database, you're not really validating.
| Part | Positions | Description |
|---|---|---|
| LOU prefix | 1–4 | Identifies the Local Operating Unit that issued the LEI |
| Reserved | 5–6 | Always digits |
| Entity identifier | 7–18 | Entity-specific part (alphanumeric) |
| Check digits | 19–20 | MOD-97 checksum (same algorithm as IBAN) |
7 L T W F Z Y I C N S X 8 D 6 2 1 K 8 6 ├─────┤ ├─┤ ├─────────────────────┤ ├─┤ LOU Res Entity identifier CD
2. Step 1: Format validation
An LEI must be exactly 20 alphanumeric characters:
function isValidLEIFormat(lei) { return /^[A-Z0-9]{20}$/.test(lei.toUpperCase().trim()); }
This catches typos and garbage input, but tells you nothing about whether the LEI actually exists.
3. Step 2: Mod-97 checksum
The check digit algorithm is identical to IBAN's ISO 7064 mod-97:
Move the last two digits (check digits) to the front
Convert all letters to numbers (A=10, B=11, ..., Z=35)
Compute the remainder when divided by 97
If the remainder is 1, the checksum is valid
function verifyLEIChecksum(lei) { const upper = lei.toUpperCase().trim(); if (!/^[A-Z0-9]{20}$/.test(upper)) return false; // Move check digits (last 2) to front const rearranged = upper.slice(2) + upper.slice(0, 2); // Convert letters to numbers let numericString = ''; for (const char of rearranged) { const code = char.charCodeAt(0); if (code >= 65 && code <= 90) { numericString += (code - 55).toString(); } else { numericString += char; } } // Mod-97 using chunk method (avoids BigInt) let remainder = 0; for (let i = 0; i < numericString.length; i += 7) { const chunk = remainder + numericString.slice(i, i + 7); remainder = parseInt(chunk, 10) % 97; } return remainder === 1; } // Test with Deutsche Bank's LEI verifyLEIChecksum('7LTWFZYICNSX8D621K86'); // true verifyLEIChecksum('7LTWFZYICNSX8D621K87'); // false — one digit off
This is the same chunk-based approach used for IBAN validation. parseInt can't handle the full numeric string (it exceeds 253), so we process it in chunks of 7 digits, carrying the remainder forward.
4. Step 3: Entity lookup — the hard part
Here's where it gets interesting. A valid checksum doesn't mean the entity exists or is active. The Global LEI Foundation (GLEIF) maintains the authoritative database of all ~2.3 million LEIs worldwide.
Option A: GLEIF API directly
GLEIF provides a free public API:
async function lookupGLEIF(lei) { const res = await fetch( `https://api.gleif.org/api/v1/lei-records/${lei}` ); if (res.status === 404) return null; const data = await res.json(); return { legalName: data.data.attributes.entity.legalName.name, country: data.data.attributes.entity.legalAddress.country, status: data.data.attributes.entity.status, registrationStatus: data.data.attributes.registration.status, }; }
This works, but has practical limitations:
- Rate limits — the public API is rate-limited, which is a problem if you're validating batches
- Latency — each request is a round-trip to GLEIF's servers
- No search by name — you need the LEI upfront; you can't search "Deutsche Bank" and get back LEIs
- Response structure — deeply nested JSON that requires careful extraction
Option B: Use a local GLEIF database
IsValid maintains a local copy of the GLEIF Golden Copy database (~2.3 million entities), updated regularly. No rate limits, no external API calls from your perspective, and full-text name search built in.
// Validate + enrich in one call const res = await fetch( 'https://isvalid.dev/v0/lei?value=7LTWFZYICNSX8D621K86', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } } ); const data = await res.json();
{ "valid": true, "lei": "7LTWFZYICNSX8D621K86", "found": true, "dataSource": "gleif-db", "entity.legalName": "Deutsche Bank AG", "entity.country": "DE", "entity.entityStatus": "ACTIVE", "entity.registrationStatus": "ISSUED", "lou.name": "SWIFT" }
One request: format validation, mod-97 checksum, entity lookup, registration status, and LOU info.
With the SDK
npm install @isvalid-dev/sdk
import { IsValid } from '@isvalid-dev/sdk'; const client = new IsValid({ apiKey: 'YOUR_API_KEY' }); const result = await client.lei('7LTWFZYICNSX8D621K86'); console.log(result.entity.legalName); // "Deutsche Bank AG" console.log(result.entity.entityStatus); // "ACTIVE" console.log(result.entity.registrationStatus); // "ISSUED"
Search by name
Don't have the LEI? Search by entity name using trigram similarity:
const res = await fetch( 'https://isvalid.dev/v0/lei?search=deutsche%20bank', { headers: { 'Authorization': 'Bearer YOUR_API_KEY' } } );
This returns matching entities ranked by similarity — useful for onboarding flows where users type a company name and you need to resolve it to an LEI.
5. Why checksum-only validation isn't enough
Consider these real scenarios:
Merged entities
After an acquisition, the acquired company's LEI may still pass checksum validation but its status changes to MERGED. Using it in a regulatory filing would be incorrect.
Lapsed registrations
LEIs must be renewed annually. A lapsed LEI (registrationStatus: LAPSED) might indicate an entity that hasn't updated its reference data — a compliance red flag.
Duplicate detection
Some entities have multiple LEIs (they shouldn't, but it happens). Checking against the GLEIF database lets you flag this.
KYC/AML workflows
When onboarding a counterparty, validating the LEI format is step 1. Confirming the entity is active and the registration is current is step 2. Both are required for MiFID II, EMIR, and Dodd-Frank compliance.
registrationStatus from the API response.6. Production patterns
Validation middleware (Express)
import { IsValid } from '@isvalid-dev/sdk'; const isvalid = new IsValid({ apiKey: process.env.ISVALID_API_KEY }); async function validateLEI(req, res, next) { const { lei } = req.body; if (!lei) { return res.status(400).json({ error: 'LEI is required' }); } const result = await isvalid.lei(lei); if (!result.valid) { return res.status(400).json({ error: 'Invalid LEI format or checksum', }); } if (!result.found) { return res.status(400).json({ error: 'LEI not found in GLEIF database', }); } if (result.entity.registrationStatus !== 'ISSUED') { return res.status(400).json({ error: `LEI registration status: ${result.entity.registrationStatus}`, }); } req.leiData = result; next(); }
Batch validation
Processing a CSV of counterparties? Validate all LEIs in parallel:
async function validateBatch(leis) { const results = await Promise.all( leis.map(async (lei) => { const result = await isvalid.lei(lei); return { lei, valid: result.valid, found: result.found, name: result.entity?.legalName ?? null, status: result.entity?.registrationStatus ?? null, }; }) ); const invalid = results.filter( (r) => !r.valid || !r.found || r.status !== 'ISSUED' ); return { total: results.length, invalid: invalid.length, issues: invalid }; }
Caching strategy
LEI entity data doesn't change frequently. Cache lookups for 24 hours to reduce API calls:
const cache = new Map(); const TTL = 24 * 60 * 60 * 1000; // 24 hours async function cachedLEILookup(lei) { const cached = cache.get(lei); if (cached && Date.now() - cached.timestamp < TTL) { return cached.data; } const result = await isvalid.lei(lei); cache.set(lei, { data: result, timestamp: Date.now() }); return result; }
7. Comparison: DIY vs. API
| Feature | DIY (regex + mod-97) | GLEIF API | IsValid |
|---|---|---|---|
| Format check | Yes | Yes | Yes |
| Mod-97 checksum | Yes | Yes | Yes |
| Entity lookup | No | Yes | Yes |
| Registration status | No | Yes | Yes |
| Name search | No | Limited | Trigram |
| Rate limits | N/A | Yes | No* |
| Latency | <1ms | 200–500ms | <50ms |
* Within your plan's quota.
8. Wrapping up
LEI validation has three layers: format, checksum, and entity verification. Most implementations stop at the first two, which is fine for catching typos but insufficient for compliance workflows.
If you're building anything that touches regulatory reporting — MiFID II, EMIR, SFTR, Dodd-Frank — you need all three. The mod-97 code above handles format and checksum. For entity verification and name search across 2.3 million records, IsValid gives you that in a single API call.
See also
50+ validators. One API.
IBAN, ISIN, LEI, VAT, BIC, email, phone, and more. Free tier — 100 API calls per day, no credit card required.