Zum Inhalt springen
IBANforge
← Zurück zum Blog

Deutsche IBAN prüfen und BIC auslesen: Python und JavaScript

·8 min read

Überarbeitet am 4. November 2026. Diese Beispiele bereiten eine Prüfung von Bankdaten vor; sie geben keine Zahlung frei.

Die Bankleitzahlendatei der Bundesbank ordnet deutschen Bankleitzahlen Referenzdaten zu und verzeichnet angekündigte Löschungen sowie Nachfolge-Bankleitzahlen. IBANforge gibt die verwendete Referenz unter bank_code_check und die Herkunft der BIC unter bic zurück. Eine korrekte Prüfsumme allein liefert diese Angaben nicht.

Die Beispiele eignen sich zur Durchsicht vor einem Lieferantenimport oder einer Datenmigration. Sie behalten jede Eingabe, ihren Index und die vollständige Antwort bei, ohne eine BIC zu ersetzen oder selbst ein Konto auszuwählen.

Bedeutung der Beobachtung

observationEinordnung
invalid_ibanDie API antwortet mit valid: false; error und error_detail lesen, statt immer einen Tippfehler anzunehmen.
outside_german_scopeKeine deutsche IBAN; dieses Beispiel beurteilt die Abdeckung anderer Register nicht.
bank_code_retiredLöschhinweis und superseded_by lesen; aus einer Nachfolge-Bankleitzahl lässt sich das Konto nicht rekonstruieren.
bank_code_not_allocatedDas abgefragte Register weist die Bankleitzahl zum angegebenen Stand nicht zu.
bank_reference_foundDas Ergebnis zur Bankleitzahl ist verified, authoritative: true, mit Quelle und Stand. Das Konto ist damit nicht bestätigt.
reviewJedes andere Ergebnis, etwa eine nicht verfügbare, teilweise oder undatierte Quelle; die Unsicherheit bleibt sichtbar.

Jede Zeile trägt review_required: true. Die Bezeichnungen oben gehören zu diesem Prüfbeispiel, nicht zum API-Vertrag. Die vollständige Antwort bleibt in source_response; die Validierungsreferenz beschreibt alle Felder.

Python ab Version 3.10

Nur die Standardbibliothek wird verwendet. Den Block als review_iban.py speichern. Der Schlüssel kommt aus der Umgebungsvariable IBANFORGE_API_KEY, nicht aus dem Quelltext. Für den Einzeltest ist er optional, für den hier gezeigten Stapelaufruf erforderlich.

import json
import os
import re
from urllib.error import HTTPError
from urllib.request import Request, urlopen
 
API = "https://api.ibanforge.com"
KEY = os.environ.get("IBANFORGE_API_KEY", "").strip()
 
 
def normalise(value: str) -> str:
    if not isinstance(value, str):
        raise ValueError("IBAN must be a string")
    return re.sub(r"[\s\ufeff-]", "", value).upper()
 
 
def post(path: str, payload: dict) -> dict:
    headers = {"Content-Type": "application/json"}
    if KEY:
        headers["Authorization"] = "Bearer " + KEY
    request = Request(API + path, data=json.dumps(payload).encode(),
                      headers=headers, method="POST")
    try:
        with urlopen(request, timeout=30) as response:
            return json.load(response)
    except HTTPError as error:
        if error.code == 429:
            raise RuntimeError("HTTP 429; Retry-After=" +
                               error.headers.get("Retry-After", "unknown")) from None
        if error.code == 402:
            raise RuntimeError("HTTP 402; inspect access and remaining allowance") from None
        raise RuntimeError(f"HTTP {error.code}") from None
 
 
def interpret(original: str, answer: dict, index: int) -> dict:
    if (not isinstance(answer, dict) or type(answer.get("valid")) is not bool
            or not isinstance(answer.get("iban"), str)
            or (answer["valid"] and answer["iban"] != normalise(original))):
        raise ValueError("Unexpected or misaligned API response")
    check = {} if answer.get("bank_code_check") is None else answer["bank_code_check"]
    bic = {} if answer.get("bic") is None else answer["bic"]
    if not isinstance(check, dict) or not isinstance(bic, dict):
        raise ValueError("Unexpected bank reference")
    for flag in ("authoritative", "retired"):
        if flag in check and type(check[flag]) is not bool:
            raise ValueError("Unexpected bank reference flag")
    observation = "review"
    if answer["valid"] is False:
        observation = "invalid_iban"
    elif not answer["iban"].startswith("DE"):
        observation = "outside_german_scope"
    elif check.get("retired") is True:
        observation = "bank_code_retired"
    elif (check.get("status") == "not_in_register"
          and check.get("reason") == "not_allocated"
          and check.get("authoritative") is True):
        observation = "bank_code_not_allocated"
    elif (check.get("status") == "verified" and check.get("authoritative") is True
          and check.get("register") and check.get("as_of")):
        observation = "bank_reference_found"
    return {"index": index, "input": original, "observation": observation,
            "review_required": True, "source_response": answer}
 
 
def review_one(iban: str) -> dict:
    normalise(iban)
    return interpret(iban, post("/v1/iban/validate", {"iban": iban}), 0)
 
 
def review_many(ibans: list[str]) -> list[dict]:
    if not isinstance(ibans, list):
        raise ValueError("Expected a list of IBAN strings")
    for iban in ibans:
        normalise(iban)
    if not ibans:
        return []
    if not KEY:
        raise ValueError("IBANFORGE_API_KEY is required for a batch")
    output = []
    for start in range(0, len(ibans), 100):
        batch = ibans[start:start + 100]
        body = post("/v1/iban/batch", {"ibans": batch})
        results = body.get("results") if isinstance(body, dict) else None
        if not isinstance(results, list) or len(results) != len(batch):
            raise ValueError("Unexpected batch length")
        output.extend(interpret(raw, result, start + offset)
                      for offset, (raw, result) in enumerate(zip(batch, results)))
    return output

JavaScript auf dem Server, Node.js ab Version 20

Diesen Block als review-iban.mjs speichern. Gleiche Umgebungsvariable und Beobachtungen, ohne zusätzliches Paket. Der Schlüssel bleibt auf dem Server.

const API = "https://api.ibanforge.com";
const KEY = (process.env.IBANFORGE_API_KEY ?? "").trim();
 
function normalise(value) {
  if (typeof value !== "string") throw new Error("IBAN must be a string");
  return value.replace(/[\s-]/g, "").toUpperCase();
}
 
function isObject(value) {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}
 
async function post(path, payload) {
  const headers = { "Content-Type": "application/json" };
  if (KEY) headers.Authorization = `Bearer ${KEY}`;
  const response = await fetch(API + path, {
    method: "POST", headers, body: JSON.stringify(payload),
    signal: AbortSignal.timeout(30_000),
  });
  if (response.status === 429) {
    throw new Error(`HTTP 429; Retry-After=${response.headers.get("Retry-After") ?? "unknown"}`);
  }
  if (response.status === 402) throw new Error("HTTP 402; inspect access and remaining allowance");
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}
 
export function interpret(original, answer, index) {
  if (!isObject(answer) || typeof answer.valid !== "boolean" || typeof answer.iban !== "string"
      || (answer.valid && answer.iban !== normalise(original))) {
    throw new Error("Unexpected or misaligned API response");
  }
  const check = answer.bank_code_check ?? {};
  const bic = answer.bic ?? {};
  if (!isObject(check) || !isObject(bic)) throw new Error("Unexpected bank reference");
  for (const flag of ["authoritative", "retired"]) {
    if (flag in check && typeof check[flag] !== "boolean") throw new Error("Unexpected bank reference flag");
  }
  let observation = "review";
  if (answer.valid === false) observation = "invalid_iban";
  else if (!answer.iban.startsWith("DE")) observation = "outside_german_scope";
  else if (check.retired === true) observation = "bank_code_retired";
  else if (check.status === "not_in_register" && check.reason === "not_allocated" && check.authoritative === true) {
    observation = "bank_code_not_allocated";
  } else if (check.status === "verified" && check.authoritative === true && check.register && check.as_of) {
    observation = "bank_reference_found";
  }
  return { index, input: original, observation, review_required: true, source_response: answer };
}
 
export async function reviewOne(iban) {
  normalise(iban);
  return interpret(iban, await post("/v1/iban/validate", { iban }), 0);
}
 
export async function reviewMany(ibans) {
  if (!Array.isArray(ibans)) throw new Error("Expected a list of IBAN strings");
  ibans.forEach(normalise);
  if (!ibans.length) return [];
  if (!KEY) throw new Error("IBANFORGE_API_KEY is required for a batch");
  const output = [];
  for (let start = 0; start < ibans.length; start += 100) {
    const batch = ibans.slice(start, start + 100);
    const body = await post("/v1/iban/batch", { ibans: batch });
    if (!isObject(body) || !Array.isArray(body.results) || body.results.length !== batch.length) {
      throw new Error("Unexpected batch length");
    }
    output.push(...body.results.map((result, offset) => interpret(batch[offset], result, start + offset)));
  }
  return output;
}

Ein erstes Beispiel ausführen

Nach dem Speichern des Moduls verwenden diese Befehle eine IBAN aus der Dokumentation. Sie rufen die API auf; es handelt sich nicht um Simulationen.

python3 -c 'from review_iban import review_one; import json; print(json.dumps(review_one("DE89370400440532013000")))'
node --input-type=module -e 'import { reviewOne } from "./review-iban.mjs"; console.log(JSON.stringify(await reviewOne("DE89370400440532013000")))'

Für eine Liste mit gespeichertem Schlüssel review_many([...]) in Python oder await reviewMany([...]) in JavaScript aufrufen. Die Funktionen bilden Stapel mit höchstens 100 Einträgen, behalten Duplikate und Reihenfolge bei und prüfen die Anzahl der Antworten sowie die IBAN gültiger Antworten vor der Zuordnung zu den Eingabezeilen. Eine leere Liste löst keinen Aufruf aus. Ein Eintrag, der keine Zeichenkette ist, stoppt die gesamte Liste vor dem ersten Aufruf.

Die Prüfung kontrolliert Anzahl und Aufbau der Antworten und vergleicht die IBAN gültiger Antworten mit der normalisierten Eingabe. Ungültige Eingaben können unverändert oder gekürzt zurückkommen; Befund und Position bleiben erhalten. Eine falsche Anzahl, eine fehlerhaft aufgebaute Antwort oder eine gültige Antwort zu einer anderen IBAN stoppt die Verarbeitung. Bei ungültigen Ergebnissen und identischen Duplikaten lässt sich nicht jede Vertauschung erkennen; die Zuordnung hängt von der durch die API garantierten Reihenfolge ab.

Fehler, Verbrauch und Fortsetzung

  • HTTP 200 kann valid: false enthalten. Der Code behält dies als fachliches Ergebnis.
  • HTTP 402 stoppt die Verarbeitung: Schlüssel, verbleibendes Kontingent und cause.reason in der API-Antwort vor einem neuen Versuch prüfen. Die Beispiele kaufen nichts und wiederholen keinen Aufruf automatisch.
  • HTTP 429 nennt Retry-After im Fehler; es gibt keine Wiederholungsschleife.
  • Andere HTTP-Fehler, Zeitüberschreitungen, unlesbares JSON und widersprüchliche Antworten stoppen ebenfalls. Sie dürfen nicht als ungültige IBAN ausgegeben werden.

Schlägt ein späterer Stapel fehl, können frühere Stapel bereits Einheiten verbraucht haben. Diese Funktionen geben die Liste erst am Ende zurück. Ein dauerhafter Import braucht einen Fortsetzungspunkt je Stapel; nicht die ganze Datei ungeprüft erneut senden. Eine Zeitüberschreitung beweist nicht, dass der Server nichts gezählt hat.

Der erste Einzelaufruf kann den Test ohne Schlüssel nutzen. Der Stapelweg oben verwendet einen API-Schlüssel: Jeder Eintrag verbraucht eine Einheit, auch Duplikate und ungültige IBANs. cost_usdc: 0 mit einem Schlüssel bedeutet nicht, dass keine Einheit verbraucht wurde. Kontingente, Guthaben und weitere Zugänge: Stapeldokumentation und Preise.

Herkunft behalten und im eigenen Prozess entscheiden

bank_code_check, bic, deren Datumsangaben und next_steps zusammen mit dem Ergebnis behalten. Der Registerstand ist weder ein Zahlungsdatum noch das Datum einer Kontoprüfung. Eine BIC kann trotz erkannter Bankleitzahl fehlen; ein Register kann auch für Deutschland nicht verfügbar sein. review ist somit ein möglicher Fall.

Die Beispiele bestätigen weder die Existenz des Kontos noch dessen Inhaber oder eine Sanktionsfreigabe. Ein Löschhinweis liefert keine automatisch verwendbare neue Bankverbindung; die Prüfung einer Lieferantenänderung bleibt Teil des Unternehmensprozesses.

Weitere Schritte: erster API-Ablauf, Integrationsrezepte, IBAN-zu-BIC-Auflösung und Übernahme von Lieferantendaten.