Guide · Node.js · Validation

BIC / SWIFT Validation in Node.js

BIC validation is more than a regex check. Learn the structure, the edge cases, and how to enrich a code with institution name and country in a single API call.

1. What is a BIC / SWIFT code?

A BIC (Bank Identifier Code), also called a SWIFT code, is a unique identifier for financial institutions used in international wire transfers. It is defined by ISO 9362 and maintained by SWIFT (Society for Worldwide Interbank Financial Telecommunication).

BICs appear in SEPA transfers, IBAN structures, correspondent banking, and payment system integrations. When a user enters a BIC in your app — whether during onboarding, a transfer form, or KYC — you need to verify it's a real, correctly formatted code before passing it downstream.

ℹ️The terms BIC and SWIFT code are used interchangeably. Technically, BIC is the ISO standard name; SWIFT is the organisation that issues and manages the codes. Both refer to the same thing.

2. BIC structure decoded

A BIC is either 8 characters (without branch) or 11 characters (with branch). Let's decode a real example:

Example: DEUTDEDBBER

DEUTInstitution
DECountry
DBLocation
BERBranch
PartLengthDescriptionExample
Institution code4 lettersIdentifies the bank or institutionDEUT
Country code2 lettersISO 3166-1 alpha-2 country codeDE
Location code2 charsAlphanumeric; passive participants end in 1DB
Branch code3 chars (opt.)Specific branch. XXX = primary office. Omitted in 8-char BICs.BER

DEUTDEDB (8 chars) and DEUTDEDBXXX (11 chars with XXX) both refer to the same primary office of Deutsche Bank in Germany. Both are equally valid.


3. The naive approach: regex (and what it misses)

The first impulse is to write a regex against the ISO 9362 format definition:

// bicValidator.js — regex-only approach
// Structurally correct — but misses real-world edge cases

const BIC_REGEX = /^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$/;

function validateBicNaive(bic) {
  return BIC_REGEX.test(bic.toUpperCase().trim());
}

console.log(validateBicNaive('DEUTDEDB'));      // true  — correct
console.log(validateBicNaive('DEUTDEDBXXX'));   // true  — correct
console.log(validateBicNaive('DEUTDEDBBER'));   // true  — correct
console.log(validateBicNaive('AAAABB11'));      // true  — structurally valid but not a real institution
console.log(validateBicNaive('DEUTDEDE'));      // true  — technically valid format, may not exist
console.log(validateBicNaive('DEUTDE'));        // false — too short
⚠️A regex can confirm the format — it cannot tell you whether the institution actually exists, what it's called, or where it's located. For form validation that prevents typos, regex is a reasonable client-side pre-filter. For anything consequential — payment routing, KYC, compliance — you need a lookup.

4. Why BIC validation is more than format

Institution lookup

The regex above accepts AAAABBCC— four random letters followed by a valid country and location. To confirm the code maps to a real institution, you need access to the SWIFT BIC directory (or a database derived from it). This is the most common source of user errors: a digit transposed, two letters swapped, or an old BIC that was retired.

8-char vs 11-char handling

Payment systems treat 8-char BICs and their XXX-suffixed equivalents as identical. But some systems expect exactly 11 characters and will reject an 8-char BIC. You often need to normalise the input — either stripping XXX or appending it — depending on your downstream system.

Passive SWIFT participants

If the second character of the location code is 1 (e.g. location code U1), the institution is a passive participant — it receives SWIFT messages but does not initiate them. Some payment workflows must not route outbound transfers to passive participants.

Country enrichment

The two-letter country segment lets you derive the country of the institution automatically — useful for displaying a flag, applying geo-specific fee rules, or SEPA zone checks. But EG could be Egypt, and you need a lookup table, not just a slice of the string.


5. The right solution: one API call

The IsValid BIC API validates format, resolves the institution name, city, and branch, and returns the full country name — all from a single GET request.

10k+
Institutions
banks and financial institutions
<30ms
Response time
local lookup, no external call
100/day
Free tier
no credit card required

Get your free API key at isvalid.dev — 100 calls per day, no credit card required.

Full parameter reference and response schema: BIC Validation API docs →


6. Node.js example with native fetch

Using the native fetch API (Node 18+). No dependencies required.

// bicValidator.js
const API_KEY = process.env.ISVALID_API_KEY;
const BASE_URL = 'https://api.isvalid.dev';

/**
 * Validate a BIC/SWIFT code using the IsValid API.
 *
 * Returns an object with keys: valid, bankCode, countryCode, countryName,
 * locationCode, branchCode, bankName, city, branch.
 *
 * @param {string} bic - BIC/SWIFT code (8 or 11 characters)
 * @returns {Promise<object>} Validation result
 */
async function validateBic(bic) {
  const params = new URLSearchParams({ value: bic });

  const response = await fetch(`${BASE_URL}/v0/bic?${params}`, {
    headers: { Authorization: `Bearer ${API_KEY}` },
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(`BIC API error ${response.status}: ${error.message ?? 'unknown'}`);
  }

  return response.json();
}

// ── Example usage ────────────────────────────────────────────────────────────

const result = await validateBic('DEUTDEDBBER');

if (!result.valid) {
  console.log('Invalid BIC');
} else {
  const bank = result.bankName ?? result.bankCode;
  const country = result.countryName ?? result.countryCode;
  const city = result.city ?? '—';
  const branch = result.branch ?? 'primary office';
  console.log(`${bank} · ${country} · ${city} · ${branch}`);
  // → Deutsche Bank · Germany · Berlin · Berlin
}

For batch validation — processing a list of BICs from a CSV import or bulk onboarding:

// bicBatch.js — validate multiple BICs sequentially
async function validateBics(bicList) {
  const results = [];

  for (const bic of bicList) {
    try {
      const data = await validateBic(bic);
      results.push({ bic, ...data });
    } catch (err) {
      results.push({ bic, valid: false, error: err.message });
    }
  }

  return results;
}

// ── Example ──────────────────────────────────────────────────────────────────

const codes = ['DEUTDEDBBER', 'BNPAFRPPXXX', 'CHASUS33', 'INVALID123'];
const results = await validateBics(codes);

for (const r of results) {
  const status = r.valid ? '✓' : '✗';
  const name = r.bankName ?? 'unknown';
  console.log(`  ${status}  ${r.bic.padEnd(15)}  ${name}`);
}
For high-volume use, cache results by BIC. Institution data changes rarely — a TTL of 24 hours is a reasonable default. Store the full response so you can serve name and country lookups without re-calling the API.

7. Express.js integration

In a payment form or onboarding flow, wrap the API call in an Express route handler with proper error handling:

// routes/bic.js (Express)
app.get('/api/validate-bic', async (req, res) => {
  const bic = (req.query.bic ?? '').trim();

  if (!bic) {
    return res.status(400).json({ error: 'bic parameter required' });
  }

  let result;
  try {
    result = await validateBic(bic);
  } catch {
    return res.status(502).json({ error: 'BIC validation service unavailable' });
  }

  if (!result.valid) {
    return res.status(422).json({ error: 'Invalid BIC code' });
  }

  res.json({
    bic: bic.toUpperCase(),
    institution: result.bankName,
    country: result.countryName,
    city: result.city,
  });
});

For middleware-style validation that rejects invalid BICs before reaching your business logic:

// middleware/validateBicParam.js
async function validateBicParam(req, res, next) {
  const bic = req.body.bic ?? req.query.bic;

  if (!bic) {
    return res.status(400).json({ error: 'bic is required' });
  }

  try {
    const result = await validateBic(bic);
    if (!result.valid) {
      return res.status(422).json({ error: 'Invalid BIC code' });
    }
    // Attach enriched data for downstream handlers
    req.bicData = result;
    next();
  } catch {
    return res.status(502).json({ error: 'Validation service error' });
  }
}

// Usage: app.post('/transfer', validateBicParam, transferHandler);

8. cURL example

Deutsche Bank, Berlin branch (11-char BIC):

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/bic?value=DEUTDEDBBER"

Same institution, primary office shorthand (8-char BIC):

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/bic?value=DEUTDEDB"

Invalid BIC (wrong format):

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/bic?value=TOOSHORT"

9. Understanding the response

Response for XASXAU2SRTG (ASX Operations, Sydney):

{
  "valid": true,
  "bankCode": "XASX",
  "countryCode": "AU",
  "countryName": "Australia",
  "locationCode": "2S",
  "branchCode": "RTG",
  "bankName": "ASX Operations Pty Limited",
  "city": "Sydney",
  "branch": "RTGS Settlement"
}

Response for an 8-char BIC with no branch code:

{
  "valid": true,
  "bankCode": "BNPA",
  "countryCode": "FR",
  "countryName": "France",
  "locationCode": "PP",
  "branchCode": null,
  "bankName": "BNP Paribas",
  "city": "Paris",
  "branch": null
}

Response for an invalid code:

{
  "valid": false,
  "bankCode": "TOOS",
  "countryCode": "HO",
  "countryName": null,
  "locationCode": "RT",
  "branchCode": null,
  "bankName": null,
  "city": null,
  "branch": null
}
FieldTypeDescription
validbooleanFormat is correct and the code is recognised
bankCodestring4-letter institution identifier
countryCodestringISO 3166-1 alpha-2 code (chars 5-6 of BIC)
countryNamestring | nullFull English country name, or null if unrecognised
locationCodestring2-char location code; second char 1 = passive participant
branchCodestring | nullXXX = primary office; null for 8-char BICs
bankNamestring | nullInstitution name if in directory, else null
citystring | nullCity of the branch or head office
branchstring | nullBranch or department name if available

10. Edge cases to handle

Normalise 8- and 11-char BICs

Some downstream systems require exactly 11 characters. If your system needs this, append XXX to 8-char BICs after validation. Never pad before validation — let the API accept both forms.

/**
 * Normalise a BIC after successful validation.
 * @param {string} bic - Validated BIC code
 * @param {{ to11?: boolean }} options
 */
function normaliseBic(bic, { to11 = false } = {}) {
  bic = bic.toUpperCase().trim();
  if (to11 && bic.length === 8) {
    return bic + 'XXX';
  }
  if (!to11 && bic.endsWith('XXX')) {
    return bic.slice(0, -3);
  }
  return bic;
}

Check for passive participants

If you route outbound transfers, reject passive participants before sending.

/**
 * Passive SWIFT participants have '1' as the second char of locationCode.
 */
function isPassiveParticipant(result) {
  const location = result.locationCode ?? '';
  return location.length === 2 && location[1] === '1';
}

const result = await validateBic('DEUTDEDB');
if (isPassiveParticipant(result)) {
  throw new Error('Cannot send outbound transfer to a passive SWIFT participant');
}

Cache results to reduce API calls

BIC directory data is stable. Cache validated results for 24 hours to avoid redundant calls during high-throughput batch processing.

// Simple in-process TTL cache (use Redis for multi-process deployments)
const cache = new Map();
const TTL = 24 * 60 * 60 * 1000; // 24 hours in ms

async function validateBicCached(bic) {
  const key = bic.toUpperCase().trim();
  const cached = cache.get(key);

  if (cached && Date.now() - cached.ts < TTL) {
    return cached.data;
  }

  const data = await validateBic(key);
  cache.set(key, { data, ts: Date.now() });
  return data;
}

Handle unknown institutions gracefully

A BIC can be structurally valid (passes format check) but not found in the institution directory — the API returns valid: true but bankName: null. This can happen with newly issued BICs not yet in the built-in directory. Always fall back to displaying the bankCode if bankName is null.


Summary

Do not use regex alone — it cannot verify the institution exists
Do not assume 8 and 11 char BICs are always interchangeable downstream
Use GET with the value as a query param (BICs are not sensitive)
Check locationCode[1] === "1" to detect passive participants
Fall back to bankCode when bankName is null
Cache results — BIC data changes rarely

See also

Try BIC validation instantly

Free tier includes 100 API calls per day. No credit card required. Institution name, city, and branch lookup included in every response.