Guide · Node.js · SDK · REST API

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.

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.

PartPositionsDescription
LOU prefix1–4Identifies the Local Operating Unit that issued the LEI
Reserved5–6Always digits
Entity identifier7–18Entity-specific part (alphanumeric)
Check digits19–20MOD-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:

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

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. Use the SDK or call the REST API directly with fetch.

import { createClient } from '@isvalid-dev/sdk';

const iv = createClient({ apiKey: process.env.ISVALID_API_KEY });

// Validate + enrich in one call
const data = await iv.lei('7LTWFZYICNSX8D621K86');
{
  "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 with the SDK or fetch:

const results = await iv.lei.search('deutsche bank');

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.

⚠️A valid checksum does not mean the entity exists or is currently active. LEIs expire annually — an entity that did not renew its LEI will still pass the checksum but have a LAPSED registration status. Always check 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

FeatureDIY (regex + mod-97)GLEIF APIIsValid
Format checkYesYesYes
Mod-97 checksumYesYesYes
Entity lookupNoYesYes
Registration statusNoYesYes
Name searchNoLimitedTrigram
Rate limitsN/AYesNo*
Latency<1ms200–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.

Use format + checksum + entity lookup for compliance-grade validation
Check registrationStatus — only "ISSUED" means the LEI is current
Do not rely on checksum alone — always verify entity existence and status
Do not assume a valid LEI is currently active — LEIs expire annually

Node.js integration notes

When handling LEI in a TypeScript codebase, define a branded type to prevent accidental mixing of financial identifier strings at compile time:type Lei = string & { readonly __brand: 'Lei' }. The IsValid SDK ships with full TypeScript definitions covering all response fields, including country-specific and instrument-specific data, so your editor provides autocomplete on the parsed result without manual type declarations.

In financial data pipelines — payment processors, reconciliation engines, or KYC workflows — LEI validation sits at the ingestion boundary. Pair the IsValid SDK with decimal.js orbig.js for any monetary amounts tied to the identifier, and usepino for structured logging that attaches the validation result to the transaction reference in every log line, making audit trails straightforward.

Express.js and Fastify middleware

Centralise LEI validation in a request middleware rather than repeating it in every route handler. The middleware calls the IsValid API, attaches the parsed result toreq.validated, and callsnext() on success. Layer in a Redis cache keyed by the normalised identifier with a 24-hour TTL to avoid redundant API calls for the same value across multiple requests in the same session.

Error handling should distinguish between a 422 response from IsValid (the LEI is structurally invalid — return this to the caller immediately) and 5xx or network errors (transient failures — retry once after a short delay before surfacing a service-unavailable error). Never swallow validation failures silently; they indicate bad data that could propagate into financial records downstream.

  • Assert process.env.ISVALID_API_KEY is present at server startup, not lazily at first request
  • Use Promise.allSettled() for batch validation — it collects all results without aborting on the first failure
  • Mock the IsValid client with jest.mock() in unit tests; keep CI pipelines free of real API calls
  • Store the full parsed API response alongside the raw LEI in your database — country code, institution data, and check-digit status are useful for downstream logic

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

50+ validators. One API.

IBAN, ISIN, LEI, VAT, BIC, email, phone, and more. Free tier — 100 API calls per day, no credit card required.