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
};
}
π‘ 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,
};
}
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,
};
}
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
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)