KYC Onboarding Validation Checklist
Three identifiers, three API calls, one onboarding flow. Here's how to validate a counterparty's IBAN, LEI, and VAT number before opening an account — and why skipping any of them is a compliance risk.
In this guide
1. What is KYC and why validation matters
KYC (Know Your Customer) is the process of verifying the identity of a counterparty before entering a business relationship. Regulations like AML4/AML5 (EU Anti-Money Laundering Directives), the Bank Secrecy Act (US), and MiFID II require financial institutions to verify key identifiers at onboarding — not as a formality, but as a legal obligation with real consequences for non-compliance.
Three identifiers form the backbone of entity KYC in Europe:
- IBAN — confirms the bank account is structurally valid and identifies the bank
- LEI — confirms the legal entity is registered and active in the GLEIF database
- VAT — confirms the tax registration is valid via the EU VIES system
Failing to validate these at onboarding leads to: rejected payments, regulatory fines, delayed settlements, and exposure to fraud. A single invalid identifier can block a wire transfer, trigger a compliance review, or — in the worst case — result in facilitating transactions with a sanctioned entity.
2. The three identifiers
Each identifier serves a different purpose in the KYC chain. Together, they verify the counterparty's bank account, legal existence, and tax status:
| Identifier | Standard | What it proves | API endpoint |
|---|---|---|---|
| IBAN | ISO 13616 | The bank account is structurally valid and the bank exists | GET /v0/iban |
| LEI | ISO 17442 | The legal entity is registered with GLEIF and its status is current | GET /v0/lei |
| VAT | EU VIES | The tax registration is active in the EU VIES database | GET /v0/vat |
3. Step 1: Validate the IBAN
The IBAN endpoint performs a multi-layer validation:
- MOD-97 checksum — the ISO 13616 algorithm that catches transcription errors
- Country code + BBAN extraction — validates the country-specific length and format
- Bank identification — looks up the bank code in the
iban_bank_codestable to returnbankCode,bankName, andbankBic - EU/SEPA membership flag — tells you whether the account is in a SEPA country
Example response
{ "valid": true, "countryCode": "DE", "countryName": "Germany", "bban": "210501700012345678", "isEU": true, "isSEPA": true, "bankCode": "21050170", "bankName": "Hamburger Sparkasse", "bankBic": "HASPDEHHXXX" }
What to check in your code
valid === true — the IBAN passes format, length, and mod-97 checksum
bankName exists — the bank code was identified in the national registry
countryCode matches expectations — the counterparty claims to be in Germany, and the IBAN confirms it
4. Step 2: Validate the LEI
The LEI endpoint performs a deep verification chain:
- ISO 17442 MOD-97 checksum — identical algorithm to IBAN, applied to the 20-character code
- Entity lookup in GLEIF database — 2.3 million records synced from the GLEIF Golden Copy, with live API fallback
- Registration status and entity status — distinguishes between ACTIVE/INACTIVE entities and ISSUED/LAPSED registrations
Example response
{ "valid": true, "lei": "7LTWFZYICNSX8D621K86", "louCode": "7LTW", "checkDigits": "86", "found": true, "dataSource": "gleif-db", "entity": { "legalName": "DEUTSCHE BANK AKTIENGESELLSCHAFT", "country": "DE", "entityStatus": "ACTIVE", "registrationStatus": "ISSUED", "category": null, "initialRegistrationDate": "2012-06-06", "lastUpdate": "2024-03-15", "nextRenewal": "2025-03-15", "managingLou": "EVK05KS7XY1DEII3R011" }, "lou": null }
What to check in your code
valid === true — the LEI passes format and MOD-97 checksum
found === true — the entity exists in the GLEIF database
entity.entityStatus === "ACTIVE" — the entity is currently active
entity.registrationStatus === "ISSUED" — the LEI is current, not LAPSED
registrationStatus.5. Step 3: Validate the VAT number
The VAT endpoint combines two layers of validation:
- Country-specific checksum validation — each EU country has its own format and check digit algorithm (Germany uses MOD-11, Poland uses weighted sum, etc.)
- VIES lookup — queries the EU VAT Information Exchange System to confirm the number is actively registered
Example response
{ "valid": true, "countryCode": "DE", "vatNumber": "123456789", "normalized": "DE123456789", "vies": { "checked": true, "valid": true, "name": "DEUTSCHE BANK AG", "address": "TAUNUSANLAGE 12, 60325 FRANKFURT AM MAIN" } }
What to check in your code
valid === true — the VAT number passes format and checksum validation
vies.checked === true — VIES was reachable and the query succeeded
vies.valid === true — the number is confirmed active in the EU VIES registry
6. Putting it all together — parallel validation
The following Node.js example validates all three identifiers in parallel using Promise.all. Using the native fetch API (Node 18+) — no dependencies required.
// kyc-validator.mjs const API_KEY = process.env.ISVALID_API_KEY; const BASE = 'https://api.isvalid.dev'; const headers = { Authorization: `Bearer ${API_KEY}` }; async function callApi(path, params) { const url = new URL(path, BASE); Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v)); const res = await fetch(url, { headers }); if (!res.ok) throw new Error(`${path}: ${res.status}`); return res.json(); } async function kycCheck({ iban, lei, vat }) { const [ibanResult, leiResult, vatResult] = await Promise.all([ callApi('/v0/iban', { value: iban }), callApi('/v0/lei', { value: lei }), callApi('/v0/vat', { value: vat }), ]); return { iban: { valid: ibanResult.valid, bank: ibanResult.bankName ?? null, country: ibanResult.countryCode ?? null, }, lei: { valid: leiResult.valid, found: leiResult.found ?? null, name: leiResult.entity?.legalName ?? null, status: leiResult.entity?.registrationStatus ?? null, }, vat: { valid: vatResult.valid, confirmed: vatResult.vies?.valid ?? null, name: vatResult.vies?.name ?? null, }, }; } // ── Example usage ────────────────────────────────────────────────────────── const result = await kycCheck({ iban: 'DE89370400440532013000', lei: '7LTWFZYICNSX8D621K86', vat: 'DE123456789', }); console.log('IBAN:', result.iban.valid ? `✓ ${result.iban.bank}` : '✗ invalid'); console.log('LEI :', result.lei.found ? `✓ ${result.lei.name}` : '✗ not found'); console.log('VAT :', result.vat.confirmed ? `✓ ${result.vat.name}` : '✗ not confirmed'); // All three must pass for KYC approval const approved = result.iban.valid && result.lei.found && result.lei.status === 'ISSUED' && result.vat.confirmed; console.log('KYC :', approved ? 'APPROVED' : 'REJECTED');
Promise.all. The total latency is the slowest single call, not the sum of all three.7. cURL examples
Validate an IBAN (German account):
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/iban?value=DE89370400440532013000"
Validate a LEI (Deutsche Bank):
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/lei?value=7LTWFZYICNSX8D621K86"
Validate a VAT number with VIES check:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/vat?value=DE123456789&checkVies=true"
8. Handling failures gracefully
Real-world KYC flows encounter partial failures, stale data, and edge cases. Here are the most common scenarios and how to handle them:
VIES unavailable
The EU VIES service has regular downtimes — each of the 27 member states runs its own node. When vies.checked is false, the VAT number passed local checksum validation but VIES confirmation is unavailable. Decide whether to accept provisionally and re-check later, or block the onboarding until VIES responds.
if (vatResult.valid && vatResult.vies && !vatResult.vies.checked) { // VIES is down — format is valid, but registration unconfirmed // Option A: accept provisionally, schedule re-check await scheduleRecheck(vatResult.normalized, '24h'); return { status: 'provisional', reason: 'vies_unavailable' }; }
LEI found but LAPSED
An entity whose LEI registration has lapsed still exists in GLEIF but is not current. For regulated transactions (EMIR, MiFID II), only "ISSUED" status is acceptable. A "LAPSED" LEI is a compliance red flag — the entity has not renewed, and regulators may reject reports that reference it.
if (leiResult.found && leiResult.entity?.registrationStatus !== 'ISSUED') { // LEI exists but is not current — reject for regulated transactions return { status: 'rejected', reason: `LEI registration is ${leiResult.entity.registrationStatus}`, }; }
IBAN valid but bank unknown
The mod-97 checksum passed but the bank code is not in the database. This happens with small banks, new entrants, or countries where the national registry is incomplete. The IBAN is still structurally valid — the bank just is not identified by name. This is not a rejection reason, but it reduces the information available for due diligence.
if (ibanResult.valid && !ibanResult.bankName) { // IBAN is structurally valid, but bank is not identified // Log for manual review — do not reject logger.warn(`IBAN valid but bank unknown: ${ibanResult.countryCode}`); }
Partial KYC — not all identifiers available
Not every entity has all three identifiers. Design a tiered approach: IBAN is always required for payment-related onboarding, LEI is required for regulated entities under MiFID II or EMIR, and VAT is required for EU B2B entities. Use a flexible config pattern to adapt requirements per counterparty type.
const REQUIREMENTS = { regulated_eu: { iban: true, lei: true, vat: true }, regulated_non_eu: { iban: true, lei: true, vat: false }, unregulated_eu: { iban: true, lei: false, vat: true }, unregulated_non_eu: { iban: true, lei: false, vat: false }, }; function getRequirements(counterpartyType) { return REQUIREMENTS[counterpartyType] ?? REQUIREMENTS.unregulated_non_eu; } // Only validate required identifiers const reqs = getRequirements('regulated_eu'); const calls = []; if (reqs.iban) calls.push(callApi('/v0/iban', { value: iban })); if (reqs.lei) calls.push(callApi('/v0/lei', { value: lei })); if (reqs.vat) calls.push(callApi('/v0/vat', { value: vat })); const results = await Promise.all(calls);
Summary checklist
See also
Start validating for KYC
Free tier includes 100 API calls per day. No credit card required. IBAN, LEI, and VAT validation all included.