Validate an IBAN in a JavaScript form

Check an IBAN's structure and checksum, show normalized bank details, and keep stale responses from validating an edited number.

JavaScript (browser)Last tested Get the complete example ↓
On this page

Working example

HTML + JavaScript

Before you start

Build an IBAN field with a Check IBAN button. A successful check shows the number in print format, its country, and the bank name and BIC when known. The example sends the entered IBAN to parseAPI for the check. It does not initiate a payment.

Create a Public key on the keys page. Allow localhost and your production hostname, then replace YOUR_PUBLIC_API_KEY in the complete HTML file with that key. Public keys start with parse_public_; secret keys stay on your server.

Save the downloaded example, then start a local server from the same directory:

Terminal
python3 -m http.server 8000

Open the example on localhost. Use the hostname allowed on your public key. Opening the file directly with file:// does not provide the website origin the key needs.

This browser check gives feedback while someone fills out a form. Validate the submitted value again on your server when integrating it into a real application; browser controls can be bypassed.

1. Treat the IBAN as text

Use a text input with a visible label. An IBAN includes letters and can contain leading zeroes, so a number input is unsuitable. Let the person paste spaces or type lowercase letters.

HTML
<form id="iban-form" aria-busy="false">
  <label for="iban">IBAN
    <input id="iban" name="iban" type="text" inputmode="text" autocomplete="off"
      autocapitalize="characters" spellcheck="false" maxlength="80" required
      aria-describedby="iban-hint status" aria-invalid="false" value="DE89 3704 0044 0532 0130 00">
  </label>
  <p id="iban-hint" class="hint">Include the country prefix. Spaces and lowercase letters are fine.</p>
  <button type="submit">Check IBAN</button>
  <p id="status" role="status" aria-live="polite" aria-atomic="true" data-error="false">Nothing is checked until you ask.</p>
  <dl id="details" hidden></dl>
</form>

The request starts only on submission. Before sending it, the example uppercases the letters and removes separators to obtain the electronic form. It leaves the original input visible so the person can compare and edit it.

This normalization is not validation. A string that contains only letters and digits can still have the wrong country length, an invalid national structure, or an incorrect checksum. The API performs those checks.

2. Request the current number

Call /iban/{iban} with the encoded electronic form. This example requires the country prefix in the input and uses the core response.

JavaScript
async function checkIban(value, signal) {
  if (!API_KEY.startsWith('parse_public_')) {
    throw new Error('Add your public API key and allow this hostname in its domain list.');
  }
  const url = new URL(`https://api.parseapi.com/iban/${encodeURIComponent(value)}`);
  url.search = new URLSearchParams({ key: API_KEY });
  const response = await fetch(url, { signal });
  if (!response.ok) {
    const messages = {
      400: 'The request could not be checked. Review the input and try again.',
      401: 'Check your public API key.',
      403: 'Check your public key, allowed hostnames, and account access.',
      429: 'The request limit was reached. Try again later.'
    };
    throw new Error(messages[response.status] || 'The IBAN check is unavailable. Try again later.');
  }
  return response.json();
}

Check the HTTP status before using the JSON. A bad IBAN normally returns 200 with valid: false. A rejected key, exhausted allowance, or unavailable service is a failed request; none establishes whether the number is valid.

The form has a ten-second timeout. Editing the input aborts the request and clears the previous result. A request revision check also discards a superseded response if it arrives after cancellation.

3. Read the verdict before the bank details

valid is true when the country length, national structure, and MOD-97 checksum match. Check it before presenting bank details.

JavaScript
function showResult(data, value) {
  if (!data || typeof data.valid !== 'boolean' || data.iban !== value) {
    throw new Error('The IBAN check returned an unexpected response. Try again.');
  }
  // Invalid numbers can still carry parsed bank fields. The verdict comes first.
  if (!data.valid) {
    input.setAttribute('aria-invalid', 'true');
    setStatus('The IBAN structure or checksum does not match. Check the number and try again.', true);
    return;
  }
  if (typeof data.formatted !== 'string' || typeof data.country !== 'string' ||
      ![data.bank_name, data.bic].every((value) => value === null || typeof value === 'string')) {
    throw new Error('The IBAN check returned incomplete details. Try again.');
  }
  const rows = [
    ['IBAN', data.formatted],
    ['Country', data.country],
    ['Bank', data.bank_name || 'Not identified'],
    ['BIC', data.bic || 'Not identified']
  ];
  details.replaceChildren(...rows.map(([label, value]) => {
    const row = document.createElement('div');
    const term = document.createElement('dt');
    const description = document.createElement('dd');
    term.textContent = label;
    description.textContent = value;
    row.append(term, description);
    return row;
  }));
  details.hidden = false;
  setStatus('The IBAN structure and checksum match. Review the number before using it.');
}

An invalid IBAN may still contain a recognizable bank code. Its response can therefore include bank_name or bic even when valid is false. Showing those details as a successful result would obscure the failed check. This example marks the field invalid and asks the person to correct it instead.

For a valid number, the form shows:

  • formatted: the IBAN grouped in fours for reading.
  • country: the country code from the IBAN prefix.
  • bank_name: the identified institution, or Not identified when null.
  • bic: the bank BIC, or Not identified when null.

A missing bank name or BIC does not undo a valid structure and checksum. Each field may be unknown independently. The IBAN reference describes the complete response, including the separate bank and branch identifiers.

4. Keep an edited number unchecked

Every input event clears the verdict, bank details, and invalid-field marker. The number remains editable while a check is running. The submit button becomes available again when editing cancels that check.

The response must echo the electronic form that was requested. An unexpected response is shown as a failed check. The example creates result elements with textContent, so bank names are displayed as text.

When adding a later form-submission step, bind its validation to the submitted IBAN value. A successful check for the old text must never approve a corrected number automatically.

Try a valid number and a mistake

Check the default DE89 3704 0044 0532 0130 00. It should pass the structure and checksum checks. Change its last digit to 1 and check again: the checksum fails, and bank details stay hidden.

Start a check and immediately edit the input. No late result should appear. Try the form offline or with a rejected key; it should report a request failure and let you retry, without labeling the IBAN invalid.

What a matching checksum establishes

A matching structure and checksum help catch entry errors. They do not prove that an account exists, belongs to the person completing the form, or can receive a particular payment. Bank identity is separate from those checks.

Keep any ownership verification and payment authorization in your application's appropriate flow. Use the IBAN API when you need to inspect another example response.

Complete example

One HTML file with the form, styles, and JavaScript. Replace YOUR_PUBLIC_API_KEY with your public key, then run it on a domain you have added to that key.

Download HTML
View and copy the complete source
validate-iban-javascript.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Validate an IBAN in a JavaScript form</title>
  <style>
    :root { color-scheme: light dark; --bg: #fff; --ink: #152029; --muted: #5e6b75; --line: #d9e2e7; --field: #f6f9fa; --accent: #007c91; --button: #c5f6ff; --error: #b42335; }
    @media (prefers-color-scheme: dark) {
      :root:not([data-theme="light"]) { --bg: #0b1015; --ink: #eff7fa; --muted: #94a6b2; --line: #2c3942; --field: #121b23; --accent: #67e8f9; --button: #67e8f9; --error: #fda4af; }
    }
    :root[data-theme="dark"] { color-scheme: dark; --bg: #0b1015; --ink: #eff7fa; --muted: #94a6b2; --line: #2c3942; --field: #121b23; --accent: #67e8f9; --button: #67e8f9; --error: #fda4af; }
    :root[data-theme="light"] { color-scheme: light; }
    * { box-sizing: border-box; }
    [hidden] { display: none !important; }
    body { margin: 0; padding: 24px; background: var(--bg); color: var(--ink); font: 15px/1.5 system-ui, sans-serif; }
    main { max-width: 560px; margin: 0 auto; }
    h1 { margin: 0 0 6px; font-size: clamp(23px, 5vw, 30px); font-weight: 600; letter-spacing: -.035em; }
    p { margin: 0; }
    .intro, .note, .hint { color: var(--muted); }
    form { margin-top: 24px; }
    label { display: block; font-size: 13px; font-weight: 550; }
    input, button { font: inherit; }
    input { width: 100%; min-width: 0; margin-top: 7px; padding: 11px 12px; border: 1px solid var(--line); border-radius: 7px; background: var(--field); color: var(--ink); }
    input[aria-invalid="true"] { border-color: var(--error); }
    :focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
    .hint { margin-top: 6px; font-size: 12px; }
    button { min-height: 46px; margin-top: 16px; padding: 11px 16px; border: 0; border-radius: 7px; background: var(--button); color: #07313c; cursor: pointer; font-size: 14px; font-weight: 650; }
    button:disabled { opacity: .55; cursor: wait; }
    #status { margin-top: 14px; min-height: 44px; color: var(--muted); font-size: 13px; }
    #status[data-error="true"] { color: var(--error); }
    dl { margin: 0 0 20px; border-top: 1px solid var(--line); font-size: 13px; }
    dl div { display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--line); }
    dt { color: var(--muted); }
    dd { margin: 0; overflow-wrap: anywhere; }
    .note { border-top: 1px solid var(--line); margin-top: 12px; padding-top: 16px; font-size: 12px; }
    @media (max-width: 380px) { body { padding: 18px; } button { width: 100%; } dl div { grid-template-columns: 70px minmax(0, 1fr); } }
  </style>
</head>
<body>
  <main>
    <h1>Check an IBAN.</h1>
    <p class="intro">Check its structure and checksum, then see the bank details we can identify.</p>
    <!-- example:form -->
    <form id="iban-form" aria-busy="false">
      <label for="iban">IBAN
        <input id="iban" name="iban" type="text" inputmode="text" autocomplete="off"
          autocapitalize="characters" spellcheck="false" maxlength="80" required
          aria-describedby="iban-hint status" aria-invalid="false" value="DE89 3704 0044 0532 0130 00">
      </label>
      <p id="iban-hint" class="hint">Include the country prefix. Spaces and lowercase letters are fine.</p>
      <button type="submit">Check IBAN</button>
      <p id="status" role="status" aria-live="polite" aria-atomic="true" data-error="false">Nothing is checked until you ask.</p>
      <dl id="details" hidden></dl>
    </form>
    <!-- /example:form -->
    <p class="note">Account existence, ownership and payment approval require separate checks. This example only validates the number.</p>
  </main>
  <script>
    // Restrict this public key to your development and production hostnames.
    const API_KEY = 'YOUR_PUBLIC_API_KEY';
    const form = document.querySelector('#iban-form');
    const input = document.querySelector('#iban');
    const button = form.querySelector('button[type="submit"]');
    const status = document.querySelector('#status');
    const details = document.querySelector('#details');
    let controller;
    let revision = 0;

    function setStatus(message, error = false) {
      status.textContent = message;
      status.dataset.error = String(error);
    }

    function cancelRequest() {
      revision += 1;
      controller?.abort();
      controller = undefined;
      button.disabled = false;
      form.setAttribute('aria-busy', 'false');
    }

    function clearResult() {
      details.hidden = true;
      details.replaceChildren();
      input.setAttribute('aria-invalid', 'false');
    }

    // example:request
    async function checkIban(value, signal) {
      if (!API_KEY.startsWith('parse_public_')) {
        throw new Error('Add your public API key and allow this hostname in its domain list.');
      }
      const url = new URL(`https://api.parseapi.com/iban/${encodeURIComponent(value)}`);
      url.search = new URLSearchParams({ key: API_KEY });
      const response = await fetch(url, { signal });
      if (!response.ok) {
        const messages = {
          400: 'The request could not be checked. Review the input and try again.',
          401: 'Check your public API key.',
          403: 'Check your public key, allowed hostnames, and account access.',
          429: 'The request limit was reached. Try again later.'
        };
        throw new Error(messages[response.status] || 'The IBAN check is unavailable. Try again later.');
      }
      return response.json();
    }
    // /example:request

    // example:result
    function showResult(data, value) {
      if (!data || typeof data.valid !== 'boolean' || data.iban !== value) {
        throw new Error('The IBAN check returned an unexpected response. Try again.');
      }
      // Invalid numbers can still carry parsed bank fields. The verdict comes first.
      if (!data.valid) {
        input.setAttribute('aria-invalid', 'true');
        setStatus('The IBAN structure or checksum does not match. Check the number and try again.', true);
        return;
      }
      if (typeof data.formatted !== 'string' || typeof data.country !== 'string' ||
          ![data.bank_name, data.bic].every((value) => value === null || typeof value === 'string')) {
        throw new Error('The IBAN check returned incomplete details. Try again.');
      }
      const rows = [
        ['IBAN', data.formatted],
        ['Country', data.country],
        ['Bank', data.bank_name || 'Not identified'],
        ['BIC', data.bic || 'Not identified']
      ];
      details.replaceChildren(...rows.map(([label, value]) => {
        const row = document.createElement('div');
        const term = document.createElement('dt');
        const description = document.createElement('dd');
        term.textContent = label;
        description.textContent = value;
        row.append(term, description);
        return row;
      }));
      details.hidden = false;
      setStatus('The IBAN structure and checksum match. Review the number before using it.');
    }
    // /example:result

    input.addEventListener('input', () => {
      cancelRequest();
      clearResult();
      setStatus('Check this IBAN to see its result.');
    });

    form.addEventListener('submit', async (event) => {
      event.preventDefault();
      cancelRequest();
      clearResult();
      if (!form.reportValidity()) return;
      // Match the API's electronic form without changing what the person typed.
      const value = input.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
      if (!value) {
        input.setAttribute('aria-invalid', 'true');
        setStatus('Enter an IBAN including its country prefix.', true);
        return;
      }
      const current = revision;
      controller = new AbortController();
      const request = controller;
      let timedOut = false;
      const timeout = setTimeout(() => { timedOut = true; request.abort(); }, 10000);
      button.disabled = true;
      form.setAttribute('aria-busy', 'true');
      setStatus('Checking IBAN...');
      try {
        const data = await checkIban(value, request.signal);
        if (current !== revision) return;
        if (request.signal.aborted) throw new Error('The request was cancelled.');
        showResult(data, value);
      } catch (error) {
        if (current !== revision) return;
        setStatus(timedOut ? 'The IBAN check timed out. Try again.'
          : error instanceof TypeError ? 'Could not reach parseAPI. Check your connection.'
          : error instanceof SyntaxError ? 'The response could not be read. Try again.'
          : error.message, true);
      } finally {
        clearTimeout(timeout);
        if (current === revision) cancelRequest();
      }
    });
  </script>
</body>
</html>

Need a key? Create a public API key. For every field and parameter, see IBAN API reference.