DEV Community

Leszek Czajka
Leszek Czajka

Posted on • Originally published at isvalid.dev

LEI Validation in JavaScript — GLEIF Lookup Without Rate Limits

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)  
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Move the last two digits (check digits) to the front
  2. Convert all letters to numbers (A=10, B=11, ..., Z=35)
  3. Compute the remainder when divided by 97
  4. 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
Enter fullscreen mode Exit fullscreen mode

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,
  };
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode
{
  "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"
}
Enter fullscreen mode Exit fullscreen mode

One request: format validation, mod-97 checksum, entity lookup, registration status, and LOU info.

With the SDK

npm install @isvalid-dev/sdk
Enter fullscreen mode Exit fullscreen mode
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"
Enter fullscreen mode Exit fullscreen mode

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' } }
);
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)