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
| Header | Value | Required |
|---|---|---|
Content-Type | application/json | Yes |
Authorization | Bearer ifk_... (free API key) | One of the two |
X-PAYMENT | x402 payment token | One of the two |
Body
{
"iban": "CH10 0023 0000 0000 1234 5"
}| Field | Type | Description |
|---|---|---|
iban | string | The 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:
| Field | Type | Present | Description |
|---|---|---|---|
iban | string | Always | Cleaned IBAN (uppercase, no spaces) |
valid | boolean | Always | Whether the IBAN passed all validation checks |
country | object | Valid IBANs | Country code and name |
check_digits | string | Valid IBANs | The two-digit check number |
bban | object | Valid IBANs | Parsed BBAN components |
bic | object | null | Valid IBANs | BIC/SWIFT code and institution data (null if no match found) |
sepa | object | Valid IBANs | SEPA membership, schemes, and VoP requirement |
issuer | object | Valid IBANs with BIC | Institution classification |
bank_code_check | object | Valid IBANs | Whether the bank code resolves in reference data — and how much that answer is worth (see What "verified" means) |
next_steps | array | When applicable | Machine-readable follow-ups (screen compliance, verify the payee, …), each with the reason it is suggested |
risk_indicators | object | Valid IBANs | Composite risk signal for compliance |
clearing | object | null | Valid CH/LI IBANs | Swiss clearing data from the SIX BankMaster (BC-Nummer, rail participation, QR-IID); null when the IID is not listed |
formatted | string | Valid IBANs | IBAN with spaces every 4 characters |
error | string | Invalid IBANs | Error code |
error_detail | string | Invalid IBANs | Human-readable error description |
cost_usdc | number | Always | Cost of this request in USDC |
processing_ms | number | Always | Processing time in milliseconds |
country object:
| Field | Type | Description |
|---|---|---|
code | string | ISO 3166-1 alpha-2 country code |
name | string | Full country name in English |
bban object:
| Field | Type | Description |
|---|---|---|
bank_code | string | Bank/institution identifier extracted from BBAN |
branch_code | string? | Branch code (present for countries like FR, GB, ES, IT) |
account_number | string | Account number extracted from BBAN |
bic object (present when a matching BIC is found):
| Field | Type | Description |
|---|---|---|
code | string | BIC/SWIFT code (8 characters) |
bank_name | string | null | Financial institution name |
city | string | null | City of the institution |
basis | string | Where 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) |
authoritative | boolean | Whether 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:
| Field | Type | Description |
|---|---|---|
member | boolean | Whether this country is in the SEPA zone |
schemes | string[] | Available SEPA schemes: SCT (Credit Transfer), SDD (Direct Debit), SCT_INST (Instant) |
vop_required | boolean | Whether Verification of Payee is mandatory (EU regulation, since Oct 2025 for eurozone) |
vop_participant | boolean | null | Bank-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):
| Field | Type | Description |
|---|---|---|
type | string | null | bank (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) |
name | string | Institution 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 |
classification | string | curated — 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_issuer | string? | 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.typeisemi,digital_bank, orpayment_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:
| Field | Type | Description |
|---|---|---|
value | string | The bank code taken from the BBAN, echoed for your logs |
status | string | verified — 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) |
reason | string? | 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 |
match | string | null | register — exact key in the reference set (deterministic); prefix — fallback BIC-prefix search, possible only where bank codes are letters; check candidates |
register | string | null | Human name of the reference set consulted |
authoritative | boolean | true only where the reference set IS the national register — see below |
candidates | number? | On match: "prefix": how many institutions matched. More than 1 means the answer is indicative |
institution | object? | 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_of | string | Month of the reference data |
risk_indicators object:
| Field | Type | Description |
|---|---|---|
issuer_type | string | null | Same as issuer.type; null when no institution was substantiated |
country_risk | string | standard, elevated (FATF grey list), or high (FATF black list / EU high-risk) |
test_bic | boolean | Whether the BIC is a test/sandbox code |
sepa_reachable | boolean | Whether the account's country is in the SEPA zone |
sepa_reachable_scope | string | country — the reachability statement is about the country's schemes, never about this specific account |
vop_coverage | boolean | Whether 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_registermeans 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"withcandidates > 1means 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 thenational_registerbasis but dropsauthoritativetofalse; readbank_code_check.retiredandsuperseded_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.candidatessays how many, andnext_stepsraisesbic_is_advisorywhen 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.authoritativeandbank_code_check.authoritativeanswer 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, sobic.authoritativeisfalse. 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
| Code | Description |
|---|---|
invalid_format | IBAN contains invalid characters or is too short |
unsupported_country | Country code is not recognized |
wrong_length | IBAN length does not match expected length for this country |
checksum_failed | MOD-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}`);
}