Skip to content
IBANforge

Validate IBAN

Validate a single IBAN with full checksum verification, country-specific BBAN structure parsing, automatic BIC/institution lookup, SEPA compliance data, issuer classification (bank vs. EMI/neobank), and risk indicators for compliance agents. The length and layout of every country, with the official example and the register we check it against, is on IBAN by country.

Endpoint

POST https://api.ibanforge.com/v1/iban/validate

Cost: $0.005 USDC per request with x402, or one request from your key's allowance or credits.

No key at all: up to 25 validations a week per source address (IPv6 counted per /64), on this route only, to try it out. The week is the ISO week in UTC and resets on Monday at 00:00 UTC. Send a real iban with no credential: the answer is complete and carries a trial block with the count left this week and the reset instant. Past that, the route answers 402 with cause.reason: "trial_exhausted" until Monday. For every other endpoint, take a key that needs no e-mail.

Request

Headers

HeaderValueRequired
Content-Typeapplication/jsonYes
AuthorizationBearer ifk_... (API key, no e-mail needed)No: without it, the keyless trial serves the call while it lasts
PAYMENT-SIGNATURE (x402 v2) or X-PAYMENT (v1)x402 payment signatureNo: pays for this one call instead of a key

Body

{
  "iban": "CH10 0023 0000 0000 1234 5"
}
FieldTypeDescription
ibanstringThe IBAN to validate. Spaces and hyphens are stripped automatically. Case-insensitive.

Response

Success (200)

{
  "iban": "CH1000230000000012345",
  "valid": true,
  "country": {
    "code": "CH",
    "name": "Switzerland"
  },
  "check_digits": "10",
  "bban": {
    "bank_code": "00230",
    "account_number": "000000012345"
  },
  "bic": {
    "code": "UBSWCHZH80A",
    "bic8": "UBSWCHZH",
    "bank_name": "UBS Switzerland AG",
    "city": "Zürich",
    "basis": "national_register",
    "authoritative": true
  },
  "sepa": {
    "member": true,
    "schemes": ["SCT", "SDD"],
    "vop_required": false,
    "vop_participant": null
  },
  "issuer": {
    "type": "bank",
    "name": "UBS Switzerland AG",
    "classification": "default"
  },
  "bank_code_check": {
    "value": "00230",
    "status": "verified",
    "match": "register",
    "register": "SIX BankMaster (Swiss IID / BC-Nummer register)",
    "authoritative": true,
    "institution": {
      "name": "UBS Switzerland AG",
      "street": "Bahnhofstrasse 45",
      "post_code": "8098",
      "town": "Zürich",
      "country": "CH"
    },
    "as_of": "2026-08"
  },
  "risk_indicators": {
    "issuer_type": "bank",
    "country_risk": "standard",
    "test_bic": false,
    "sepa_reachable": true,
    "sepa_reachable_scope": "country",
    "vop_coverage": false
  },
  "clearing": {
    "iid": "00230",
    "name": "UBS Switzerland AG",
    "type": "bank",
    "town": "Zürich",
    "sic": true,
    "instant_payments_chf": true,
    "eurosic": true,
    "qr_iid": null
  },
  "formatted": "CH10 0023 0000 0000 1234 5",
  "cost_usdc": 0.005,
  "processing_ms": 1.23
}

Response fields

Top-level fields:

FieldTypePresentDescription
ibanstringAlwaysCleaned IBAN (uppercase, no spaces)
validbooleanAlwaysWhether the IBAN passed all validation checks
countryobjectValid IBANsCountry code and name
check_digitsstringValid IBANsThe two-digit check number
bbanobjectValid IBANsParsed BBAN components
bicobject | nullValid IBANsBIC/SWIFT code and institution data (null if no match found)
sepaobjectValid IBANsSEPA membership, schemes, and VoP requirement
issuerobjectValid IBANs with BICInstitution classification
bank_code_checkobjectValid IBANsWhether the bank code resolves in reference data — and how much that answer is worth (see What "verified" means)
next_stepsarrayWhen applicableMachine-readable follow-ups (screen compliance, verify the payee, …), each with the reason it is suggested
risk_indicatorsobjectValid IBANsComposite risk signal for compliance
clearingobject | nullValid CH/LI IBANsSwiss clearing data from the SIX BankMaster (BC-Nummer, rail participation, QR-IID); null when the IID is not listed
formattedstringValid IBANsIBAN with spaces every 4 characters
errorstringInvalid IBANsError code
error_detailstringInvalid IBANsHuman-readable error description
cost_usdcnumberAlwaysCost of this request in USDC
processing_msnumberAlwaysProcessing time in milliseconds

country object:

FieldTypeDescription
codestringISO 3166-1 alpha-2 country code
namestringFull country name in English

bban object:

FieldTypeDescription
bank_codestringBank/institution identifier extracted from BBAN
branch_codestring?Branch code (present for countries like FR, GB, ES, IT)
account_numberstringAccount number extracted from BBAN

bic object (present when a matching BIC is found):

FieldTypeDescription
codestringBIC/SWIFT code as published by the source, 8 or 11 characters; compare on bic8
bic8stringThe eight characters of the institution, and the field to compare a supplied BIC against. The branch code (the last three characters of code) is informational: in cooperative networks it names the local bank and the first eight its clearing institution
redirected_fromstring?The bank code you asked about, when the register answered for the one that took over its clearing (CH and LI: SIX redirects a concatenated IID). The IBAN stays valid: a redirect is not a retirement
bank_namestring | nullFinancial institution name
citystring | nullCity of the institution
basisstringWhere the bank code to BIC pairing came from. national_register — the country's own register publishes this BIC for this bank code (today DE, AT, BE, SK, CZ, BG, CH, LI and SM); curated_map — our maintained bank-code map, an exact key and not an allocation record; directory_prefix — the BIC-prefix fallback, which can match several institutions (see bank_code_check.candidates)
authoritativebooleanWhether this BIC may be stored and settled against. Derived from basis, true only for national_register. See "Settle against it, or treat it as advisory?" below

sepa object:

FieldTypeDescription
memberbooleanWhether this country is in the SEPA zone
schemesstring[]Available SEPA schemes: SCT (Credit Transfer), SDD (Direct Debit), SCT_INST (Instant)
vop_requiredbooleanWhether Verification of Payee is mandatory (EU regulation, since Oct 2025 for eurozone)
vop_participantboolean | nullBank-level VoP readiness: true when the resolved institution is listed as ready in the EPC VoP scheme register; null when no institution was resolved, or when that register was not consulted for the call

issuer object (present when BIC is resolved):

FieldTypeDescription
typestring | nullbank (traditional), digital_bank (neobank), emi (Electronic Money Institution), payment_institution — or null when no institution could be substantiated (for example, the bank code is not a listed IBAN issuer)
namestringInstitution name — who holds the matching BIC. May be stated even when type is null: naming the BIC holder is a fact, calling it your counterparty's bank would be a guess
classificationstringcurated — the type is a positive identification from maintained EMI/neobank/payment-institution lists; default — the type falls back to bank because most BIC holders are banks. Count on curated; treat default as a presumption
iban_issuerstring?Only for countries that publish a list of IBAN-issuing providers (today: NL). confirmed — the code is on that list; not_listed — it is not, type drops to null, and next_steps tells you the account may not exist

vIBAN detection: If issuer.type is emi, digital_bank, or payment_institution, the IBAN is more likely to be a virtual IBAN (vIBAN). This is useful for AML/CFT compliance under the EU AMLR regulation (July 2027).

bank_code_check object:

FieldTypeDescription
valuestringThe bank code taken from the BBAN, echoed for your logs
statusstringverified — the code resolves to an institution we can name; not_in_register — it does not, in reference data we hold for this country; unavailable — no opinion, either because we hold no reference data for this country or because we could not answer just now (see reason)
reasonstring?Why the verdict is not verified. Present on every not_in_register and every unavailable, absent on verified. not_allocated — a national register denies the code, and this is the only value that licenses "do not send"; absent_from_reference_data — our composite map does not carry it, which says nothing about the country's own register; no_reference_data_for_country; register_names_no_holder — the register defines this code space and publishes no holder, which is silence and not a denial; national_register_unavailable — the register this country is normally decided against could not be consulted, so the verdict beside it carries composite weight; lookup_failed — the lookup could not run at all. The last two describe us, never your beneficiary
matchstring | nullregister — exact key in the reference set (deterministic); prefix — fallback BIC-prefix search, possible only where bank codes are letters; check candidates
registerstring | nullHuman name of the reference set consulted
authoritativebooleantrue only where the reference set IS the national register — see below
candidatesnumber?On match: "prefix": how many institutions matched. More than 1 means the answer is indicative
institutionobject?What the national register publishes about the allocated institution — name, seat address, LEI where available. Only on authoritative answers. Depth varies: CH/LI and AT full street address, DE postal code + town (its register has no street), BE name only, BG name only — in Cyrillic, as the register publishes it — SK name only, with Slovak diacritics as published, CZ name only, with Czech diacritics as published. Absent fields are null, never guessed. This is the institution holding the bank code, not a branch, and not proof of any account
as_ofstringMonth of the reference data

risk_indicators object:

FieldTypeDescription
issuer_typestring | nullSame as issuer.type; null when no institution was substantiated
country_riskstringstandard, elevated (FATF grey list), or high (FATF black list / EU high-risk)
test_bicbooleanWhether the BIC is a test/sandbox code
sepa_reachablebooleanWhether the account's country is in the SEPA zone
sepa_reachable_scopestringcountry — the reachability statement is about the country's schemes, never about this specific account
vop_coveragebooleanWhether VoP is mandatory for this country

What "verified" means — and what it does not

bank_code_check.status: "verified" means the bank code resolves to an institution we can name in the reference data consulted. How much that is worth is exactly what authoritative says:

  • authoritative: true — the reference set is the national register itself. Today: CH and LI (SIX BankMaster), DE (Bundesbank Bankleitzahlendatei), AT (OeNB directory), BE (NBB code list), BG (Bulgarian National Bank BAE register — the verdict covers the four-letter bank code, IBAN positions 5-8; branch digits are not separately verified), SK (the Národná banka Slovenska prevodník of identification codes for the domestic payment system), CZ (the Česká národní banka číselník of payment-system codes: Czech law makes it the allocation of IBAN positions 5-8, and each edition applies from its own effective date, not from the day we read it). In these countries, not_in_register means the code is not allocated — a strong reason to stop a payment. San Marino is deliberately not on that list: the Central Bank's list of operating banks names the holder of a code it carries, so a hit is verified with an institution block, but it does not publish the allocation of the ABI space, so a miss stays absent_from_reference_data and authoritative stays false. See San Marino bank codes. Finland is not on that list either since 16 September 2026: the Finance Finland codes are a transcribed list dated 2025-10 that nothing refreshes, allocated to banking groups rather than to institutions, so a hit is verified and names the group, while a miss stays absent_from_reference_data with authoritative: false. See Finnish bank codes.
  • authoritative: false — the reference set is our composite bank-code map, assembled from BIC directories. A hit names who holds the matching BIC; it does not prove that this institution issues IBANs. Where bank codes are letter-based, match: "prefix" with candidates > 1 means the answer is indicative only.

A failure on our side is reported as unavailable, never as a verdict. When the reference data cannot be read — an unreadable database, a table missing after a bad deploy, a lookup that times out — the answer is status: "unavailable" with reason: "lookup_failed" and register: null. It is never not_in_register, because that verdict means "no institution holds this code" and an outage on our side is not evidence about your beneficiary. reason is what separates the two situations in one token: lookup_failed and national_register_unavailable are ours to fix, everything else is a statement about the code. All of them call for the same handling: carry on, and let a payee name check decide.

None of this confirms that the account exists, is open, or belongs to a particular person. A structurally valid IBAN naming a real institution can still be fabricated. For payee verification use the banks' own Verification of Payee flow (sepa.vop_required tells you when it is mandated) or a name check with your counterparty — whenever a response leaves that gap open, next_steps says so explicitly.

Settle against it, or treat it as advisory?

The BIC in the response is derived from the bank code the IBAN carries, and how much that derivation is worth depends entirely on what produced it. bic.basis says which, and bic.authoritative turns that into the one boolean a payment engine can branch on:

  • basis: "national_register", authoritative: true — the country's own register publishes this BIC for this bank code. Today that is Switzerland, Liechtenstein, Germany, Austria, Belgium, Bulgaria, Slovakia, Czech Republic and San Marino: the SIX BankMaster carries the exact 11-character BIC per IID (BC-Nummer) for CH and LI, the Bundesbank Bankleitzahlendatei the exact 11-character BIC per BLZ, and the OeNB, NBB, Bulgarian National Bank BAE, NBS, ČNB and BCSM registers publish the institution's BIC per bank code. San Marino is the one place where this flag is true while bank_code_check.authoritative is false — the Central Bank's list pairs a BIC with a code it carries, but does not publish the allocation of the code space. The German register is why a Sparkasse resolves to its own BIC rather than to the shared Landesbank BIC8, and the Swiss one is why IID 30020 resolves to Crédit Mutuel de la Vallée SA (RBABCH22180) rather than to Entris Banking AG (RBABCH22) — the field labels that pairing, it does not create it. Compare a supplied BIC on bic8; the branch code is informational, and in a cooperative network it names the local bank while the first eight name its clearing institution. Safe to store and settle against — with one carve-out: a code the register itself marks as retired keeps the national_register basis but drops authoritative to false; read bank_code_check.retired and superseded_by.
  • basis: "curated_map", authoritative: false — our own maintained bank-code map made the pairing on an exact key. Usually right, and not an allocation record: no authority stands behind it.
  • basis: "directory_prefix", authoritative: false — the BIC-prefix fallback. It can match several institutions at once; bank_code_check.candidates says how many, and next_steps raises bic_is_advisory when it is more than one.

Outside a national_register basis, treat the BIC as advisory: fine for display, enrichment and routing hints, and to be confirmed with the beneficiary or your bank before it becomes a stored settlement instruction.

bic.authoritative and bank_code_check.authoritative answer different questions. Switzerland used to be where they visibly differed and no longer is: the BIC now comes from the SIX BankMaster's own column, so both are true there. San Marino is where they still part, in the other direction — the pairing is the supervisor's (bic.authoritative: true) while the code space is not its to settle (bank_code_check.authoritative: false). The first is about the code's existence, the second about the pairing that produced the BIC.

National check digits (checks.national_check_digits)

Several countries keep a check key of their own inside the BBAN, older than the IBAN. It is a second check, independent of MOD-97: a tool that builds an IBAN around a wrong account number gets the IBAN check digits right without effort, but not the national key. IBANforge recomputes that key from the IBAN alone, in the same call and at no extra cost.

CountryschemeThe key that is checked
France, Monacofr_rib_keyThe RIB key, the last two digits of the BBAN, over the bank code, the branch code and the account number (letters in the account number are converted by the RIB table)
Belgiumbe_mod97The last two digits: the first ten digits modulo 97, or 97 when the remainder is 0
Italy, San Marinoit_cinThe CIN, the control letter at the start of the BBAN, over the ABI, the CAB and the account number
Spaines_dcThe two DC digits, positions 9 and 10 of the BBAN
United Kingdomsee modulus_checkThe Vocalink modulus check over the sort code and the account number

For these six countries a valid IBAN carries a national_check_digits block, and checks.national_check_digits repeats its status. In the other countries (the United Kingdom aside) the check is not_checked: the German account-number methods, one per bank code, are not checked yet.

{
  "valid": true,
  "checks": { "national_check_digits": "fail", "…": "…" },
  "national_check_digits": {
    "country": "FR",
    "scheme": "fr_rib_key",
    "status": "fail",
    "detail": "The RIB key (the last two digits of the BBAN) does not match the bank code, branch code and account number: this account number cannot have been issued as written."
  }
}
  • pass: the key matches, so the account number is well formed. It does not prove that the account exists or is open.
  • fail: the key does not match. This account number cannot have been issued as written: a typo, or a number made up. valid stays true, because the IBAN check digits are right; read the two separately, and confirm the details with the beneficiary before paying. detail says which digits disagree and never gives the expected key.

Invalid IBAN (200)

When the IBAN is invalid, the response still returns 200 but with valid: false:

{
  "iban": "CH5604835012345678000",
  "valid": false,
  "error": "checksum_failed",
  "error_detail": "Modulo 97 check returned 49, expected 1.",
  "cost_usdc": 0.005
}

cost_usdc shows the x402 price of the call; it is 0 when a key or the keyless trial served it.

Error codes

CodeDescription
invalid_formatIBAN contains characters other than letters, digits, spaces and hyphens, or is too short
unsupported_countryCountry code is not recognized
wrong_lengthIBAN length does not match expected length for this country
invalid_check_digitsCharacters 3 and 4 are not two digits, or are 00, 01 or 99
checksum_failedMOD-97 checksum verification failed
invalid_bban_structureLength and MOD-97 pass, but the national part does not follow the country's layout

Every code, and the HTTP statuses of a refused request (400, 402, 413, 429), is in the error reference.

Code examples

cURL

curl -X POST https://api.ibanforge.com/v1/iban/validate \
  -H "Content-Type: application/json" \
  -d '{"iban": "DE89 3704 0044 0532 0130 00"}'

Python

import requests
 
response = requests.post(
    "https://api.ibanforge.com/v1/iban/validate",
    json={"iban": "DE89370400440532013000"},
)
 
data = response.json()
if data["valid"]:
    print(f"Bank: {data['bic']['bank_name']}")
    print(f"Country: {data['country']['name']}")
    print(f"SEPA: {data['sepa']['member']}")
    print(f"Issuer type: {data['issuer']['type']}")
    print(f"Risk: {data['risk_indicators']['country_risk']}")
else:
    print(f"Invalid: {data['error_detail']}")

TypeScript

const response = await fetch(
  "https://api.ibanforge.com/v1/iban/validate",
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ iban: "DE89370400440532013000" }),
  }
);
 
const data = await response.json();
 
if (data.valid) {
  console.log(`Bank: ${data.bic.bank_name}`);
  console.log(`SEPA: ${data.sepa.member}, VoP: ${data.sepa.vop_required}`);
  console.log(`Issuer: ${data.issuer.type} — ${data.issuer.name}`);
  console.log(`Country risk: ${data.risk_indicators.country_risk}`);
} else {
  console.log(`Invalid: ${data.error_detail}`);
}

Put these checks to work

Choose a first step for your software or your supplier file.

Integrate IBAN checks into your software

Try a validation, inspect the response, then connect your application through the API or an existing integration.

Explore the API workflow

Check a supplier file

Upload a CSV or Excel file and preview the findings for free. Purchase the annotated workbook if you need the full report. No account or subscription required.

Explore the file audit

Available bank information varies by country and source. These checks do not confirm the account holder or guarantee that a payment will succeed.