Guide · Node.js · Compliance

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.

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.

ℹ️KYC is not a one-time check. Identifiers can change — IBANs get closed, LEIs lapse, VAT registrations get revoked. Periodic re-validation is part of ongoing due diligence.

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:

IdentifierStandardWhat it provesAPI endpoint
IBANISO 13616The bank account is structurally valid and the bank existsGET /v0/iban
LEIISO 17442The legal entity is registered with GLEIF and its status is currentGET /v0/lei
VATEU VIESThe tax registration is active in the EU VIES databaseGET /v0/vat
⚠️Not every counterparty has all three. Non-EU entities won't have an EU VAT number. Non-financial entities may not have a LEI. Design your flow to require only the identifiers relevant to your use case.

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_codes table to return bankCode, bankName, and bankBic
  • 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

1

valid === true — the IBAN passes format, length, and mod-97 checksum

2

bankName exists — the bank code was identified in the national registry

3

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

1

valid === true — the LEI passes format and MOD-97 checksum

2

found === true — the entity exists in the GLEIF database

3

entity.entityStatus === "ACTIVE" — the entity is currently active

4

entity.registrationStatus === "ISSUED" — the LEI is current, not LAPSED

⚠️A LAPSED LEI means the entity failed to renew. Regulators may reject transactions with lapsed LEIs. Always check 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

1

valid === true — the VAT number passes format and checksum validation

2

vies.checked === true — VIES was reachable and the query succeeded

3

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');
All three API calls run in parallel via 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

Do not skip LEI registration status — a LAPSED LEI is a compliance red flag
Do not treat VIES downtime as a VAT rejection — re-check later
Do not assume all counterparties have all three identifiers — tier your requirements
Run all validations in parallel — reduces onboarding latency
Check registrationStatus for LEI and vies.valid for VAT — not just the checksum
Log and store validation results — auditors need the paper trail

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.