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.

Endpoint

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

Cost: $0.005 USDC per request

Request

Headers

HeaderValueRequired
Content-Typeapplication/jsonYes
AuthorizationBearer ifk_... (free API key)One of the two
X-PAYMENTx402 payment tokenOne of the two

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": "UBSWCHZH",
    "bank_name": "UBS Switzerland AG",
    "city": "Zürich",
    "basis": "curated_map",
    "authoritative": false
  },
  "sepa": {
    "member": true,
    "schemes": ["SCT", "SDD"],
    "vop_required": false,
    "vop_participant": false
  },
  "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 (8 characters)
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 and BG); 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

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. 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), FI (Finance Finland codes — allocated to banking groups, so a hit confirms the group), 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). In these countries, not_in_register means the code is not allocated — a strong reason to stop a payment.
  • 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 Germany, Austria, Belgium and Bulgaria: the Bundesbank Bankleitzahlendatei carries the exact 11-character BIC per BLZ, and the OeNB, NBB and BNB BAE registers publish the institution's BIC per bank code. The German register is why a Sparkasse resolves to its own BIC rather than to the shared Landesbank BIC8 — the field labels that pairing, it does not create it. 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, and Switzerland is where they visibly differ. The SIX BankMaster answers authoritatively that the bank code is allocated — bank_code_check.authoritative: true — while the BIC beside it still comes from our curated map, so bic.authoritative is false. The first is about the code's existence, the second about the pairing that produced the BIC.

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 42, expected 1.",
  "cost_usdc": 0.005
}

Error codes

CodeDescription
invalid_formatIBAN contains invalid characters or is too short
unsupported_countryCountry code is not recognized
wrong_lengthIBAN length does not match expected length for this country
checksum_failedMOD-97 checksum verification failed

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}`);
}