A mistyped IBAN or a made-up VAT number on a supplier invoice is cheap to catch before payment and expensive to fix after. Here's a small pre-payment check you can add to any accounts-payable flow with two HTTP calls.
Just want to check one number? Use the free IBAN & VAT checker in your browser, no sign-up.
I'll use the Invoice & Payment ID Validator API on RapidAPI (I built it; there's a free plan with 100 requests a month): https://rapidapi.com/MohamedBahaa/api/invoice-payment-id-validator
1. Check the IBAN
curl --request GET \
--url 'https://invoice-payment-id-validator.p.rapidapi.com/v1/iban?value=DE89370400440532013000' \
--header 'x-rapidapi-host: invoice-payment-id-validator.p.rapidapi.com' \
--header 'x-rapidapi-key: YOUR_RAPIDAPI_KEY'
Real response:
{
"input": "DE89370400440532013000",
"iban": "DE89370400440532013000",
"country": "DE",
"check_digits": "89",
"bban": "370400440532013000",
"valid": true,
"country_name": "Germany",
"bank_code": "37040044",
"branch_code": null,
"account_number": "0532013000",
"formatted": "DE89 3704 0044 0532 0130 00",
"sepa": true,
"errors": []
}
What it checks: the country's IBAN length, the ISO 13616 mod-97 checksum, and SEPA membership. It also splits out the bank code, branch code, and account number for 20 common countries. Change one digit and you get "valid": false with "errors": ["bad_checksum"].
What it doesn't check: whether the account is open or who owns it. A valid checksum means the IBAN is well-formed, not that it belongs to your supplier. For that you still need the supplier's confirmation, or your bank's payee-verification service where one exists.
2. Check the EU VAT number, live against VIES
curl --request GET \
--url 'https://invoice-payment-id-validator.p.rapidapi.com/v1/vat/eu?value=IE6388047V&live=true' \
--header 'x-rapidapi-host: invoice-payment-id-validator.p.rapidapi.com' \
--header 'x-rapidapi-key: YOUR_RAPIDAPI_KEY'
Real response (request_date trimmed):
{
"input": "IE6388047V",
"valid": true,
"country": "IE",
"number": "6388047V",
"vat": "IE6388047V",
"format_valid": true,
"checksum": "not_checked",
"errors": [],
"vies": {
"checked": true,
"registered": true,
"name": "GOOGLE IRELAND LIMITED",
"address": "3RD FLOOR, GORDON HOUSE, BARROW STREET, DUBLIN 4",
"error": null
}
}
With live=true, the API asks the EU's VIES service whether the number is registered. When the member state shares it, you also get the registered name and address, which you can compare with the invoice. Without live=true, it only checks the format (fast, offline). VIES is sometimes down for individual countries. When it is, vies.error tells you, so treat that as "retry later", not "invalid".
UK VAT (/v1/vat/gb), US EIN (/v1/ein) and BIC/SWIFT (/v1/bic) work the same way with ?value=.
3. Check a whole invoice in one call
curl --request POST \
--url 'https://invoice-payment-id-validator.p.rapidapi.com/v1/batch' \
--header 'content-type: application/json' \
--header 'x-rapidapi-host: invoice-payment-id-validator.p.rapidapi.com' \
--header 'x-rapidapi-key: YOUR_RAPIDAPI_KEY' \
--data '{"items":[{"type":"iban","value":"DE89370400440532013000"},{"type":"eu_vat","value":"IE6388047V","live":true}]}'
Up to 50 IDs per call (up to 10 of them live VIES checks).
4. A simple pay / hold rule
- Hold if
iban.validis false - Hold if
vies.registeredis false - Flag for review if the VIES name doesn't roughly match the supplier name on the invoice
- Retry later if
vies.erroris set - Otherwise, pass it to approval
5. Do it in n8n
If your invoices arrive by email, my free n8n template turns vendor invoice PDFs in Gmail into clean rows in Google Sheets: https://mohabaha0.gumroad.com/l/ap-invoice-triage-free
To add the checks above, put an HTTP Request node after the extraction step: use POST /v1/batch with the JSON body from step 3 and your two x-rapidapi-* headers. Then add an IF node with the pay / hold rule.
If you use AI agents, the API is also an MCP server at /mcp, so Claude or Cursor can call the validators as tools.
Questions or edge cases (odd country formats, VIES quirks)? Drop them in the comments.
Top comments (5)
The pay/hold rule is the right level of detail for this. One thing worth saying about the checksum: mod-97 validity proves structure, not ownership, so a structurally valid IBAN can still point at the wrong account, which makes the VIES name-match flag do more work than it looks like. Also worth keeping vies.error on a retry schedule rather than treating one failed lookup as a failed validation, since VIES has maintenance windows and flagging a good invoice over a transient error is its own kind of pain.
Agreed on both. On VIES, the API already treats a failed lookup as "unknown", not "invalid": you get vies.checked=false and the offline format and check-digit result stands, so a maintenance window never fails a good invoice. Retrying later is left to the caller, and I should say that more clearly in the docs. On ownership, you're right that mod-97 only proves structure. That's why I added a payee check that compares the IBAN country with the supplier's country, the BIC with the IBAN, and the VIES name with the supplier name. It still can't prove who owns the account, so a "hold" means "phone the supplier on a number you already have", not "fraud". I wrote it up here: dev.to/mowa/every-id-on-this-invoi...
The unknown-vs-invalid split is the part most implementations get wrong in the opposite direction: they fail closed on a maintenance window and hold good invoices hostage to VIES uptime, which trains AP teams to override the check. The cross-check set is a sensible middle layer, and the "phone the supplier on a number you already have" framing is the honest one. An out-of-band callback to a number you sourced yourself is the only control in that chain that survives a fully spoofed document, so the hold meaning matters more than any of the automated checks. The cost of that is scale: a manual call per flagged invoice doesn't survive volume, which is exactly what makes keeping the cross-checks strict worth it, since everything they reject never reaches the callback queue. Thanks for the write-up link too, gives anyone landing here the full flow.
Agreed, and the override habit is the real cost. A check that cries wolf every VIES maintenance window gets switched off. In the payee check a VIES outage is only an info-level reason (vies_unavailable): on its own it doesn't move the verdict, so the invoice still lands on pay, review or hold from the offline checks. "Review" is for the softer mismatches and "hold" for the ones that justify the callback, which is how I try to keep manual calls to the few that need them. Curious what you do for retries: fixed backoff per member state, or just re-check whatever is still unpaid before the payment run?
The payment-run boundary is the honest answer. A fixed backoff per member state buys precision the workflow doesn't need: the verdict only matters again when someone is about to pay, so re-checking whatever is still unpaid at that point is simpler and self-heals an entire maintenance window at once. The one place it isn't enough is a run already in flight when VIES drops mid-way. There a short retry with jitter on vies_unavailable beats either stalling the whole run or paying unverified, and with your info-level split that maps cleanly: outage discovered at the boundary means re-check, outage mid-run with offline checks clean means the invoice keeps its offline verdict and only the callback-worthy ones wait. Fixed backoff as a primary strategy mostly ends up paying you to poll a service that was going to be back either way.