DEV Community

Thanasis K
Thanasis K

Posted on

How I built a LEI checker for KYC compliance, from scratch!

When you work in fintech or deal with corporate clients, KYC (Know Your Customer) isn't optional - it's a legal requirement. One piece of that puzzle is the LEI (Legal Entity Identifier): a 20-character ISO standard code assigned to every legal entity that participates in financial transactions.

The problem? Most developers don't know what LEI is, the official lookup tools are clunky, and integrating validation into your own stack requires stitching together a few public APIs with zero hand-holding. This is the build log of how I did it.

What is an LEI?
An LEI (ISO 17442) is a 20-character alphanumeric code that uniquely identifies legal entities in global financial markets. It's mandatory for reporting to ESMA, MiFID II, EMIR, and many other regulatory frameworks.


Why I built this

I was integrating a B2B onboarding flow where corporate clients had to submit their company details. Part of the compliance check required verifying their LEI against the GLEIF (Global Legal Entity Identifier Foundation) database. The existing options were:

  • Manual lookup on search.gleif.org, not scalable
  • Pay for a third-party KYC provider, expensive for early stage
  • Build a thin wrapper around the free GLEIF API β€” this is what I did

The GLEIF API

GLEIF provides a free, public REST API at api.gleif.org/api/v1. No auth required for basic lookups. Here are the two endpoints I used:

Endpoint Purpose
GET /lei-records/{lei} Fetch full entity data for a known LEI
GET /lei-records?filter[entity.legalName]={name} Search by legal name
GET /lei-records?filter[entity.status]=ACTIVE Filter by registration status

Step 1 β€” Validate the LEI format

Before hitting the API, validate the format locally. An LEI is exactly 20 characters: 18 alphanumeric chars + 2 numeric check digits (ISO 17442 checksum).

function validateLEIFormat(lei) {
  // Must be exactly 20 alphanumeric characters
  const LEI_REGEX = /^[A-Z0-9]{18}[0-9]{2}$/;

  if (!LEI_REGEX.test(lei.toUpperCase())) {
    return { valid: false, error: 'Invalid LEI format' };
  }

  // ISO 17442 checksum (mod 97)
  const padded = lei.toUpperCase().split('')
    .map(c => c >= 'A' ? c.charCodeAt(0) - 55 : c)
    .join('');

  const checksum = BigInt(padded) % 97n;

  return {
    valid: checksum === 1n,
    error: checksum !== 1n ? 'Checksum failed' : null
  };
}
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Pro tip: Always validate locally first. You save API calls and give users instant feedback without a round-trip.


Step 2 β€” Fetch entity data from GLEIF

async function fetchLEI(lei) {
  const BASE = 'https://api.gleif.org/api/v1';

  const res = await fetch(`${BASE}/lei-records/${lei}`, {
    headers: { 'Accept': 'application/vnd.api+json' }
  });

  if (res.status === 404) throw new Error('LEI not found');
  if (!res.ok) throw new Error(`GLEIF API error: ${res.status}`);

  const { data } = await res.json();
  const attrs = data.attributes;

  return {
    lei:         data.id,
    legalName:   attrs.entity.legalName.name,
    status:      attrs.entity.status,        // ACTIVE | INACTIVE
    leiStatus:   attrs.registration.status,  // ISSUED | LAPSED | RETIRED
    country:     attrs.entity.legalAddress.country,
    nextRenewal: attrs.registration.nextRenewalDate,
    managedBy:   attrs.registration.managingLou,
  };
}
Enter fullscreen mode Exit fullscreen mode

Step 3 β€” The KYC decision logic

Not all valid LEIs are "good" from a compliance perspective. Here's the rule set I implemented based on ESMA guidance:

function assessKYCRisk(record) {
  const issues = [];

  if (record.status !== 'ACTIVE')
    issues.push({ level: 'block', msg: 'Entity is not ACTIVE' });

  if (record.leiStatus !== 'ISSUED')
    issues.push({ level: 'block', msg: `LEI registration: ${record.leiStatus}` });

  const daysToExpiry = (new Date(record.nextRenewal) - new Date()) / 86400000;
  if (daysToExpiry < 30)
    issues.push({ level: 'warn', msg: `LEI expires in ${Math.round(daysToExpiry)} days` });

  return {
    pass:   issues.filter(i => i.level === 'block').length === 0,
    issues: issues,
  };
}
Enter fullscreen mode Exit fullscreen mode

Step 4 β€” Putting it all together

async function checkLEI(lei) {
  // 1. Format + checksum
  const fmt = validateLEIFormat(lei);
  if (!fmt.valid) return { ok: false, error: fmt.error };

  // 2. Live GLEIF lookup
  const record = await fetchLEI(lei);

  // 3. KYC assessment
  const kyc = assessKYCRisk(record);

  return {
    ok:     kyc.pass,
    record: record,
    issues: kyc.issues,
  };
}

// Usage
const result = await checkLEI('7LTWFZYICNSX8D621K86');
console.log(result.record.legalName); // Deutsche Bank AG
console.log(result.ok);               // true
Enter fullscreen mode Exit fullscreen mode

Production considerations

Before shipping this to prod, a few things worth adding:

  • Caching β€” LEI records don't change hourly. I cache results in Redis with a 24h TTL, keyed by LEI string.
  • Rate limits β€” GLEIF's free tier is generous but not unlimited. Add a simple token bucket or exponential backoff.
  • Audit log β€” In regulated environments, you need a paper trail. Store every check with a timestamp, result, and the raw API response.
  • Sanctions screening β€” LEI validation alone is not full KYC. You still need to cross-reference against OFAC, EU sanctions, and UN lists. I used the OpenSanctions API for this.

⚠️ Regulatory note: This is a technical implementation guide, not legal advice. Whether LEI validation satisfies your specific KYC obligations depends on your jurisdiction and regulator. Always consult a compliance specialist.


What I'd do differently

  • Move the checksum logic to a Web Worker if validating bulk files, it can block the main thread on large batches
  • Use GLEIF's bulk data download (they publish daily snapshots) for offline validation if API latency is a concern
  • Expose this as a small internal npm package so other teams in the org can reuse it without copy-pasting

Conclusion

The GLEIF API is surprisingly well-designed and free. A basic LEI checker takes about 50 lines of JS, and you can extend it with proper KYC logic, caching, and audit trails within a day. If you're building anything B2B in the EU financial space, this is a tool you're likely going to need sooner or later.

The full working code is on my GitHub. Questions or corrections? Drop them in the comments.


Next up: Screening against OFAC + EU sanctions lists with OpenSanctions API

Top comments (0)