Every number on this invoice is valid. You still shouldn't pay it.
Here's an invoice from a German supplier: German VAT number, German BIC, and an IBAN that passes its mod-97 checksum. A normal IBAN or VAT validator will say all three are valid. But that IBAN belongs to a Dutch bank, so the invoice is asking you to pay a German supplier into a Dutch account. That's what changed-bank-details fraud usually looks like: a real supplier, real-looking IDs, and an account that doesn't fit.
I added a tool to my invoice validator MCP server that checks for exactly this. It's called check_invoice_payee, and it returns one verdict: pay, review or hold. Here's what it gives back for that invoice. This is real output from the free demo endpoint, trimmed for length:
{
"verdict": "hold",
"risk_score": 90,
"summary": "Hold payment: BIC is from DE but the IBAN is from NL: the two don't belong to the same bank account; Supplier is in DE but wants payment to a NL bank account: the classic changed-bank-details fraud pattern. Confirm by phone using a known number.",
"reasons": [
{ "code": "bic_iban_country_mismatch", "severity": "high",
"message": "BIC is from DE but the IBAN is from NL: the two don't belong to the same bank account" },
{ "code": "iban_country_differs_from_supplier", "severity": "high",
"message": "Supplier is in DE but wants payment to a NL bank account: the classic changed-bank-details fraud pattern. Confirm by phone using a known number" }
],
"checks": {
"iban": { "valid": true, "country": "NL", "sepa": true, "formatted": "NL91 ABNA 0417 1643 00", "bank_code": "ABNA" },
"bic": { "valid": false, "country": "DE", "bank_code": "COBA", "matches_iban_country": false,
"errors": ["country_mismatch_with_iban"] },
"vat": { "valid": true, "kind": "eu_vat", "country": "DE", "number": "DE136695976" }
}
}
Look at checks.iban.valid: it's true. A per-field validator stops there. What matters is comparing the fields against each other.
Swap in a German IBAN (DE89370400440532013000) and keep everything else the same, and you get:
{"verdict":"pay","risk_score":0,"summary":"All identifiers are valid and consistent with each other. OK to pay.","reasons":[]}
What it cross-checks
You send whatever the invoice has (iban, bic, vat, ein), plus supplier_country, and optionally supplier_name, currency and live. It then checks:
- IBAN vs supplier country. A German supplier asking to be paid into a Dutch or Lithuanian account gets flagged.
- BIC vs IBAN. Both have to point at the same country, or they can't be the same account.
-
VAT ownership. With
live: true, the company name EU VIES has on file is compared withsupplier_name. - The smaller stuff: unregistered VAT numbers, test BICs, and EUR invoices paid into non-SEPA accounts.
Each problem comes back as a reasons[] entry with a machine-readable code and a plain-English message, so an agent can act on it and a person can read it.
What it doesn't do: it checks formats, checksums, how the IDs relate to each other, and (with live) VIES. It can't prove a bank account belongs to a particular company. Nothing short of the bank can. A hold means "call the supplier on a number you already have", not "this is fraud".
Add it to Claude Desktop or Cursor
The server is hosted (remote, Streamable HTTP), so there's nothing to install. For the free demo (10 tool calls per IP per day, no key), add this to your MCP config:
{
"mcpServers": {
"invoice-validator": {
"url": "https://invoice-validator.mohamed-bahaa53000.workers.dev/demo/mcp"
}
}
}
In Cursor that goes in ~/.cursor/mcp.json. In Claude (desktop or web), go to Settings → Connectors → Add custom connector and paste the URL. If your client only supports stdio servers, including Claude Desktop's claude_desktop_config.json, bridge it with mcp-remote:
{
"mcpServers": {
"invoice-validator": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://invoice-validator.mohamed-bahaa53000.workers.dev/demo/mcp"]
}
}
}
Then just ask: "Check this payee before I pay it: supplier in Germany, IBAN NL91ABNA0417164300, BIC COBADEFFXXX, VAT DE136695976." The model calls check_invoice_payee and you get the verdict and the reasons.
For production use, the same server runs behind RapidAPI (the free Basic plan gives 100 requests a month):
{
"mcpServers": {
"invoice-validator": {
"url": "https://invoice-payment-id-validator.p.rapidapi.com/mcp",
"headers": {
"X-RapidAPI-Key": "YOUR_RAPIDAPI_KEY",
"X-RapidAPI-Host": "invoice-payment-id-validator.p.rapidapi.com"
}
}
}
}
Or just call it over plain HTTP
curl -X POST https://invoice-validator.mohamed-bahaa53000.workers.dev/demo/v1/payee-check \
-H 'content-type: application/json' \
-d '{"supplier_country":"DE","supplier_name":"Muster GmbH","iban":"NL91ABNA0417164300","bic":"COBADEFFXXX","vat":"DE136695976","currency":"EUR"}'
The paid route is POST /v1/payee-check on the RapidAPI host. The server also has single-ID tools (validate_iban, validate_bic, validate_eu_vat, validate_gb_vat, validate_ein) for when you only need one check.
Docs and the MCP Registry manifest are at github.com/Mowaah/invoice-validator-mcp. A simple rule for an accounts-payable agent: auto-pay only on pay, send review to a person, and never pay on hold until someone has phoned the supplier.
If you'd rather click than code, there's a free IBAN & VAT checker in the browser, and I'll also set the whole invoice-triage workflow up in your n8n for $149.
Top comments (0)