Aller au contenu
IBANforge
← Retour au blog

Contrôler un IBAN allemand et lire son BIC en Python et JavaScript

·8 min read

Révisé le 4 novembre 2026. Les exemples préparent une revue des coordonnées bancaires ; ils n’autorisent aucun paiement.

La Bankleitzahlendatei de la Bundesbank associe les codes banque allemands à leurs références et renseigne les retraits annoncés et les codes successeurs. IBANforge expose la référence consultée dans bank_code_check et la provenance du BIC dans bic. Une somme de contrôle correcte ne suffit pas à obtenir ces informations.

Ces exemples conviennent à un import fournisseurs ou à une reprise de données. Ils conservent chaque entrée, son index et la réponse complète, sans remplacer un BIC ni choisir un compte à la place de l’utilisateur.

Ce que signifie l’observation

observationLecture à faire
invalid_ibanL’API répond valid: false ; lire error et error_detail, sans supposer qu’il s’agit forcément d’une faute de frappe.
outside_german_scopeL’IBAN n’est pas allemand ; ces exemples ne décident pas de la portée d’un autre registre.
bank_code_retiredLire le retrait et superseded_by ; un code successeur ne permet pas de reconstruire le compte.
bank_code_not_allocatedLe registre consulté n’alloue pas le code, selon la date de la réponse.
bank_reference_foundLe contrôle du code banque est verified, authoritative: true, avec source et date. Ce n’est pas un compte vérifié.
reviewAutre résultat, notamment source indisponible, partielle ou sans date ; conserver l’incertitude.

Toutes les lignes portent review_required: true. Les noms ci-dessus appartiennent à cet exemple de revue, pas au contrat de l’API. Le résultat complet demeure dans source_response ; la référence de validation décrit tous ses champs.

Python 3.10 ou ultérieur

Bibliothèque standard seulement. Enregistrer le bloc dans review_iban.py. La clé, facultative pour l’essai unitaire et requise ici pour un lot, se lit dans la variable d’environnement IBANFORGE_API_KEY. Ne pas l’inscrire dans le fichier.

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 côté serveur, Node.js 20 ou ultérieur

Enregistrer ce bloc dans review-iban.mjs. Même variable d’environnement, mêmes observations et aucun paquet supplémentaire. Garder la clé côté serveur.

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

Exécuter un premier exemple

Après enregistrement du module, ces commandes utilisent un IBAN documentaire. Elles effectuent un appel à l’API ; ce n’est pas une simulation.

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")))'

Pour une liste, appeler review_many([...]) en Python ou await reviewMany([...]) en JavaScript, avec une clé conservée. Les fonctions découpent en lots de 100 entrées au plus, conservent doublons et ordre, puis vérifient le nombre de réponses et l’IBAN des réponses valides avant de les associer aux lignes. Une liste vide ne déclenche aucun appel. Une valeur qui n’est pas une chaîne arrête la liste avant tout appel.

Le contrôle vérifie le nombre et la forme des réponses, puis compare l’IBAN des réponses valides à l’entrée normalisée. Une entrée invalide peut revenir brute ou tronquée : son verdict et sa position sont conservés. Un nombre incorrect, une réponse mal formée ou une réponse valide attribuée à un autre IBAN arrête le traitement. Des résultats invalides ou des doublons identiques ne permettent pas de détecter toute inversion ; leur association dépend de l’ordre garanti par l’API.

Erreurs, consommation et reprise

  • Un HTTP 200 peut contenir valid: false. Le code le conserve comme résultat métier.
  • Un HTTP 402 arrête le traitement : vérifier la clé, l’allocation restante et cause.reason dans la réponse API avant une nouvelle tentative. Les exemples n’achètent rien et ne répètent pas l’appel.
  • Un HTTP 429 expose Retry-After dans l’erreur ; ils ne lancent aucune boucle de reprise.
  • Les autres erreurs HTTP, le délai dépassé, un JSON illisible ou une réponse incohérente interrompent aussi le traitement. Ne pas les convertir en IBAN invalide.

Si un lot ultérieur échoue, les lots précédents ont déjà pu consommer des unités. Ces fonctions rendent la liste seulement à la fin : pour un traitement durable, conserver un point de reprise par lot et ne pas recommencer tout le fichier aveuglément. Un délai dépassé ne prouve pas que le serveur n’a rien compté.

Le premier appel unitaire peut utiliser l’essai sans clé. Le parcours de lot ci-dessus utilise une clé API : une entrée consomme une unité, y compris un doublon ou un IBAN invalide. cost_usdc: 0 avec une clé ne signifie pas absence de consommation. Allocations, crédits et autres accès : documentation des lots et tarifs.

Garder la provenance, puis décider dans votre processus

Conserver les blocs bank_code_check et bic, leurs dates et next_steps avec le résultat. La date de l’édition du registre n’est ni la date d’un paiement ni celle d’une vérification du compte. Un BIC peut manquer malgré un code banque identifié ; un registre peut être indisponible, y compris pour l’Allemagne. review n’est donc pas un cas impossible.

Ces exemples ne confirment ni l’existence du compte, ni son titulaire, ni l’absence de sanctions. Un code retiré ne donne pas de nouvelle coordonnée bancaire utilisable automatiquement ; le contrôle d’un changement fournisseur appartient au processus de l’entreprise.

Pour les étapes suivantes : premier parcours API, recettes d’intégration, résolution IBAN vers BIC et reprise de données fournisseurs.