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.
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.
This guide covers the full stack: format validation, mod-97 checksum, and live entity lookup — with code you can use today.
What Is an LEI?
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.
7 L T W F Z Y I C N S X 8 D 6 2 1 K 8 6
├─────┤ ├─┤ ├─────────────────────┤ ├─┤
LOU Reserved Entity Check
prefix (2 chars) identifier digits
(4 chars) (12 chars) (2 chars)
The first 4 characters identify the Local Operating Unit (LOU) that issued the LEI. Characters 5–6 are reserved (always digits). Characters 7–18 are the entity-specific part. Characters 19–20 are check digits computed using the same mod-97 algorithm as IBANs.
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.
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 2^53), so we process it in chunks of 7 digits, carrying the remainder forward.
Step 3: The Hard Part — Entity Lookup
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.
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.
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;
}
Comparison: DIY vs. API
| DIY (regex + mod-97) | GLEIF API | IsValid | |
|---|---|---|---|
| Format check | ✅ | ✅ | ✅ |
| Mod-97 checksum | ✅ | ✅ | ✅ |
| Entity lookup | ❌ | ✅ | ✅ |
| Registration status | ❌ | ✅ | ✅ |
| Name search | ❌ | Limited | ✅ (trigram) |
| Rate limits | N/A | Yes | No* |
| Latency | <1ms | 200-500ms | <50ms |
* Within your plan's quota.
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.
50+ validators. IBAN, ISIN, LEI, VAT, BIC, email, phone, and more.
Try it free → isvalid.dev
Top comments (0)