Your AI agent found a new IBAN on an invoice. Should it pay? (IBAN checks in Python, and their limits) A developer tested IBAN validation against nine inputs and found that MOD-97 checksum checks catch single-digit typos but pass altered account numbers with recomputed check digits, meaning a validator can confirm an IBAN is well-formed but not that it belongs to the supplier. The writeup recommends that payment agents treat any IBAN differing from vendor master data as a hold, flag bank or country changes as high risk, and require human confirmation through a known channel before releasing payment, with the agent never allowed to move a payment from hold to pay on its own. An invoice arrives. Same supplier, same amount pattern, but the IBAN is different. If an AI agent prepares your payments, what should it do? Bank-detail changes are a classic invoice-fraud pattern, and agents make it more interesting: they read every PDF, never get tired and will happily "fix" vendor master data if the instructions say so. So I tested what an IBAN check actually catches. Disclosure: I built the IBAN Validator & Bank Lookup https://apify.com/fast data asap/iban-validator-bank-lookup on Apify, which I use below. The lesson applies to any validator. Nine inputs: a known-good German IBAN, the same IBAN with one typo, three IBANs where I changed the account number and recomputed the two check digits what a fraudster would do , plus Dutch, French, Austrian and a truncated one. python import os, requests URL = "https://api.apify.com/v2/acts/fast data asap~iban-validator-bank-lookup" "/run-sync-get-dataset-items" HEADERS = {"Authorization": f"Bearer {os.environ 'APIFY TOKEN' }"} ibans = "DE89370400440532013000", original "DE89 3704 0044 0532 0130 01", one digit typo "DE62370400440532013001", changed account + recomputed check digits "DE40370400440532013009", "DE18370400440532013017", "NL91ABNA0417164300", "FR1420041010050500013M02606", "AT611904300234573201", "DE12345", resp = requests.post URL, headers=HEADERS, timeout=120, json={"ibans": ibans, "includeBankInfo": True} resp.raise for status for r in resp.json : print r "input" , r "valid" , r "checksumValid" , r "nationalChecksumValid" , r "bankName" , r "error" | Input | Valid | MOD-97 | National check | Bank | |---|---|---|---|---| | DE89 …013000 original | yes | ok | ok | Commerzbank | | DE89 …013001 typo | no | fails | ok | Commerzbank | | DE62 …013001 changed, recomputed | yes | ok | ok | Commerzbank | | DE40 …013009 changed, recomputed | yes | ok | ok | Commerzbank | | DE18 …013017 changed, recomputed | yes | ok | ok | Commerzbank | | NL91 ABNA… | yes | ok | – | ABN AMRO | | FR14 2004… | yes | ok | ok | La Banque Postale | | AT61 1904… | yes | ok | – | no bank data | | DE12345 | no | fails | – | wrong length | Real output, run on 12 November 2026. Malformed rows are not charged. Two takeaways: A validator answers "is this a well-formed IBAN, and which bank is it?". It cannot answer "does this account belong to my supplier?". The useful signal is not valid , it is the comparison with what you already know : | Signal | Source | What the agent should do | |---|---|---| | valid: false | validator | reject, ask for corrected invoice | | IBAN differs from vendor master data | your ERP | hold payment , never update master data automatically | | bankName / bic differs from the known bank | validator + ERP | hold, flag as high risk | | country of the bank differs from the supplier's country | validator + ERP | hold, flag as high risk | | new IBAN confirmed by the supplier through a known channel | human | release | Here is that rule as code: php def iban decision invoice iban: str, vendor: dict, check: dict - tuple str, str : """vendor = your master data, check = one row from the validator.""" if not check "valid" : return "reject", check "error" if check "iban" .replace " ", "" == vendor "iban" .replace " ", "" : return "pay", "IBAN matches master data" reasons = "IBAN differs from master data" if check.get "bic" and vendor.get "bic" and check "bic" :8 = vendor "bic" :8 : reasons.append f"bank changed: {vendor.get 'bank' } - {check.get 'bankName' }" if check "countryCode" = vendor "iban" :2 : reasons.append "bank country changed" return "hold", "; ".join reasons + " - confirm with supplier via known phone number" The important design choice: the agent can never move from "hold" to "pay" on its own. It can prepare the evidence old vs new bank, invoice, sender address , but the release needs a human or a bank-side confirmation. Since October 2025, EU payment service providers have to offer Verification of Payee for euro credit transfers: before you send money, your bank checks whether the payee name matches the account holder of that IBAN and tells you "match", "close match" or "no match". That is the check that would expose the fraudulent IBANs above, because the name on the account would not be your supplier's. So the full chain for an accounts-payable agent looks like this: IBAN Validator & Bank Lookup on Apify https://apify.com/fast data asap/iban-validator-bank-lookup : bulk IBAN validation with MOD-97 and national checksums, SEPA flag, bank name and BIC German banks from the Bundesbank file , $0.0005 per IBAN, runs offline inside the actor. For the full supplier check company register, VAT ID, IBAN see our free Agent Skill verify-supplier-de https://github.com/ichkifokkus/data-recipes/blob/main/skills/verify-supplier-de/SKILL.md . This is about payment hygiene, not legal advice. Question: if you let an agent prepare payments, where do you draw the line: may it ever update vendor bank details, or is that always a human step?