{"slug": "your-ai-agent-found-a-new-iban-on-an-invoice-should-it-pay-iban-checks-in-python", "title": "Your AI agent found a new IBAN on an invoice. Should it pay? (IBAN checks in Python, and their limits)", "summary": "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.", "body_md": "An invoice arrives. Same supplier, same amount pattern, but the IBAN is different. If an AI agent prepares your payments, what should it do?\n\nBank-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.\n\n*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.*\n\nNine 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.\n\n``` python\nimport os, requests\n\nURL = (\"https://api.apify.com/v2/acts/fast_data_asap~iban-validator-bank-lookup\"\n       \"/run-sync-get-dataset-items\")\nHEADERS = {\"Authorization\": f\"Bearer {os.environ['APIFY_TOKEN']}\"}\n\nibans = [\n    \"DE89370400440532013000\",        # original\n    \"DE89 3704 0044 0532 0130 01\",   # one digit typo\n    \"DE62370400440532013001\",        # changed account + recomputed check digits\n    \"DE40370400440532013009\",\n    \"DE18370400440532013017\",\n    \"NL91ABNA0417164300\", \"FR1420041010050500013M02606\", \"AT611904300234573201\",\n    \"DE12345\",\n]\nresp = requests.post(URL, headers=HEADERS, timeout=120,\n                     json={\"ibans\": ibans, \"includeBankInfo\": True})\nresp.raise_for_status()\nfor r in resp.json():\n    print(r[\"input\"], r[\"valid\"], r[\"checksumValid\"], r[\"nationalChecksumValid\"], r[\"bankName\"], r[\"error\"])\n```\n\n| Input | Valid | MOD-97 | National check | Bank | \n|---|---|---|---|---|\n| DE89 …013000 (original) | yes | ok | ok | Commerzbank | \n| DE89 …013001 (typo) | **no** | fails | ok | Commerzbank | \n| DE62 …013001 (changed, recomputed) | **yes** | ok | ok | Commerzbank | \n| DE40 …013009 (changed, recomputed) | **yes** | ok | ok | Commerzbank | \n| DE18 …013017 (changed, recomputed) | **yes** | ok | ok | Commerzbank | \n| NL91 ABNA… | yes | ok | – | ABN AMRO | \n| FR14 2004… | yes | ok | ok | La Banque Postale | \n| AT61 1904… | yes | ok | – | (no bank data) | \n| DE12345 | **no** | fails | – | wrong length | \n\nReal output, run on 12 November 2026. Malformed rows are not charged.\n\nTwo takeaways:\n\nA validator answers \"is this a well-formed IBAN, and which bank is it?\". It cannot answer \"does this account belong to my supplier?\".\n\nThe useful signal is not `valid`, it is the **comparison with what you already know**:\n\n| Signal | Source | What the agent should do | \n|---|---|---|\n| `valid: false` | validator | reject, ask for corrected invoice | \n| IBAN differs from vendor master data | your ERP | **hold payment** , never update master data automatically | \n| `bankName` /`bic` differs from the known bank | validator + ERP | hold, flag as high risk | \n| country of the bank differs from the supplier's country | validator + ERP | hold, flag as high risk | \n| new IBAN confirmed by the supplier through a known channel | human | release | \n\nHere is that rule as code:\n\n``` php\ndef iban_decision(invoice_iban: str, vendor: dict, check: dict) -> tuple[str, str]:\n    \"\"\"vendor = your master data, check = one row from the validator.\"\"\"\n    if not check[\"valid\"]:\n        return \"reject\", check[\"error\"]\n    if check[\"iban\"].replace(\" \", \"\") == vendor[\"iban\"].replace(\" \", \"\"):\n        return \"pay\", \"IBAN matches master data\"\n    reasons = [\"IBAN differs from master data\"]\n    if check.get(\"bic\") and vendor.get(\"bic\") and check[\"bic\"][:8] != vendor[\"bic\"][:8]:\n        reasons.append(f\"bank changed: {vendor.get('bank')} -> {check.get('bankName')}\")\n    if check[\"countryCode\"] != vendor[\"iban\"][:2]:\n        reasons.append(\"bank country changed\")\n    return \"hold\", \"; \".join(reasons) + \" - confirm with supplier via known phone number\"\n```\n\nThe 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.\n\nSince 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.\n\nSo the full chain for an accounts-payable agent looks like this:\n\n[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).\n\nThis is about payment hygiene, not legal advice.\n\n**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?", "url": "https://wpnews.pro/news/your-ai-agent-found-a-new-iban-on-an-invoice-should-it-pay-iban-checks-in-python", "canonical_source": "https://dev.to/fast_data_asap/your-ai-agent-found-a-new-iban-on-an-invoice-should-it-pay-iban-checks-in-python-and-their-p53", "published_at": "2026-11-12 07:45:33+00:00", "updated_at": "2026-11-12 07:51:30.223982+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "artificial-intelligence"], "entities": ["Apify", "Commerzbank", "ABN AMRO", "La Banque Postale"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/your-ai-agent-found-a-new-iban-on-an-invoice-should-it-pay-iban-checks-in-python", "markdown": "https://wpnews.pro/news/your-ai-agent-found-a-new-iban-on-an-invoice-should-it-pay-iban-checks-in-python.md", "text": "https://wpnews.pro/news/your-ai-agent-found-a-new-iban-on-an-invoice-should-it-pay-iban-checks-in-python.txt", "jsonld": "https://wpnews.pro/news/your-ai-agent-found-a-new-iban-on-an-invoice-should-it-pay-iban-checks-in-python.jsonld"}}