Automating Per-Country VAT Reporting and VIES Validation
A practical guide for developers embedding EU VAT compliance into marketplaces and platforms using a cloud accounting API.
Most VAT bugs in platform code trace back to one assumption: that a valid VIES result means you can zero-rate the sale. It does not, and building on that shortcut produces returns that look fine until an auditor asks why an intra-EU exemption was applied without transport evidence or a consistent place-of-supply record. The gap between "VAT number checks out" and "this supply is correctly treated" is where most integrations quietly go wrong.
This guide walks through how to automate per-country VAT reporting and VIES validation with a cloud accounting API, using Nordlet's accounting API as the implementation context. The goal is a system that captures the right evidence at the moment of sale, posts immutable ledger entries, and produces return-ready data grouped by scheme and Member State, without reconstructing totals in a spreadsheet later.
What VIES actually tells you, and what it does not
VIES is the EU VAT Information Exchange System. When you submit a country code and VAT number, it queries the relevant national database in real time and returns whether that VAT information is valid or invalid. That is the whole scope. It confirms that a number is registered and, where the Member State supports it, activated for intra-EU trade.
It does not establish the right to exempt an intra-Community supply. The European Commission is explicit about this. A valid VIES response is one piece of evidence among several. You still need to confirm the customer is the actual recipient, that the supply qualifies as intra-EU, that transport or dispatch evidence exists, and that the invoice carries the required reverse-charge wording.
So the first design decision is structural: keep VIES status and VAT treatment in separate fields. Never wire viesStatus: valid directly to zeroRated: true. The moment you collapse those two concepts, you lose the ability to defend a treatment when the evidence is questioned.
Prerequisites before writing integration code
Before any endpoint gets called, define the scope of what you are reporting. This part is boring and skipping it is expensive.
- Which countries the business is established or has fixed establishments in
- Which countries it holds VAT registrations in
- Whether it sells goods, services, digital services, or a mix
- Whether transactions are B2B, B2C, or both
- Whether it uses domestic registrations, OSS, IOSS, reverse charge, or marketplace deemed-supplier rules
- Which outputs the API must produce: domestic return data, OSS/IOSS data, invoice registers like Lithuania's i.SAF, ledger postings only, or payloads for a separate filing provider
This matters because OSS does not absorb every transaction. Services supplied in a country where the business has an establishment generally belong in that country's domestic return, not the OSS return. Model establishments separately from the start or you will misroute those supplies at aggregation time.
Do not hard-code one VAT rate per country either. The EU has standard, reduced, super-reduced, parking, zero-rated, and product-specific treatments, and they change. The reliable source for a specific product in a specific Member State is that country's tax authority, with the Commission's TEDB offering a cross-country overview. Record the source and version behind each rate decision so a later rate-table update does not silently rewrite how an old transaction appears.
Modeling the tax decision, not just the rate
The core of a durable integration is a tax-category layer sitting between products and rates. Map products to categories, categories to country-specific tax codes, then to effective-dated rates:
product/service
→ tax category
→ country-specific tax code
→ effective-dated rate and treatment
The unsafe version that causes reporting drift is product SKU → 21% VAT. The same product carries different rates in different countries, and a rate can change mid-period. Nordlet's own VAT compliance guidance recommends exactly this category-based mapping rather than binding products directly to percentages.
Your tax-category taxonomy should be stable and internal: standard-rated goods, reduced-rated goods, electronically supplied services, ordinary services, exempt supply, zero-rated supply, intra-EU B2B reverse charge, intra-EU distance sale, IOSS import, deemed-supplier transaction. Everything downstream keys off this taxonomy.
VIES validation as an asynchronous evidence workflow
The official VIES service exposes a SOAP/WSDL interface taking a two-letter country code and VAT number. Its documented faults matter for your error handling: invalid input, global and Member State concurrency limits, service unavailability, Member State unavailability, and timeouts. Treat those as distinct outcomes, not as a single "failed" bucket.
A workflow that holds up under load and audit looks like this:
- Normalize the VAT ID: uppercase the prefix, strip permitted spaces or punctuation, and keep the original user-entered value separately.
- Validate the country code and number shape locally before you spend a network call.
- Submit the normalized number to VIES.
- Store the normalized ID, request and response timestamps, the valid/invalid/not-processed state, the raw response, any service error, the customer and invoice IDs, and the application decision.
- Cache successful results for a defined, documented period.
- Retry transient technical failures with backoff.
- Route unresolved or contradictory cases to manual review.
- Revalidate when a cached result is stale or before a transaction where the evidence is material.
Nordlet checks an EU VAT number when a customer is added and re-checks it before invoicing when the earlier result is stale, which is the behavior you want to replicate whether the check happens in the platform or in your own code.
The critical rule: do not make checkout synchronous with VIES. A service outage is not an invalid number. If VIES times out during a sale, hold the transaction for review or assign it a provisional state. Blocking checkout on a Commission service's uptime is a support ticket generator, and misclassifying a good customer as invalid is worse.
Separating validation status from treatment
Once you have a VIES result, the decision engine has more work to do. Store these as distinct fields:
{
"viesStatus": "valid",
"vatTreatment": "intra_eu_b2b_reverse_charge",
"evidenceStatus": "reviewed",
"placeOfSupply": "DE"
}
Before applying a reverse-charge or exemption, the engine should test whether the customer is genuinely the recipient, whether the customer country and VAT prefix are consistent, whether the supply qualifies as intra-EU, whether special place-of-supply rules apply, whether transport evidence exists, whether the seller is acting as deemed supplier, and whether the invoice contains the required wording. A valid VIES status that passes none of these other tests is not a green light.
Creating an immutable VAT decision at invoice time
When the invoice is created, persist the full decision alongside it, not just the final rate:
invoice
├─ line tax category
├─ place of supply
├─ VAT scheme
├─ country-specific rate
├─ rate-table version
├─ VIES evidence reference
├─ location-evidence references
├─ legal-basis reference
└─ marketplace-role flags
EU invoice rules require supplier and customer details, invoice number, description, taxable amount, VAT rate, VAT amount, and any reverse-charge or exemption information. Credit notes and amendments need an unambiguous reference to the original invoice.
Nordlet's ledger is immutable, so corrections happen through new entries such as credit notes or reversing entries rather than edits to closed records. Build your consumer around that model even if your storage could technically overwrite. Editing a closed invoice destroys the link between the original filing and the correction, which is precisely the lineage an auditor follows.
Posting, webhooks, and idempotency
A sale posts double-entry accounting entries that preserve both the commercial transaction and the VAT liability split by country and scheme. A domestic sale might post:
Dr Accounts receivable / payment clearing
Cr Revenue
Cr VAT payable — DE — domestic
An intra-EU B2B reverse-charge sale posts revenue with no output VAT and a reference to the reverse-charge evidence. The exact chart of accounts is yours; what matters is that the reporting dimensions stay attached to every posting: Member State, scheme, tax category, rate, period, source invoice, and correction lineage.
Drive state changes with webhooks rather than polling. Nordlet emits events like sale_invoice.paid, signs deliveries, and retries with backoff. Give every write a deterministic idempotency key:
sale:{platform_order_id}:invoice:{invoice_version}
Your consumer has to be idempotent too. A webhook can arrive twice, arrive late, or fire after the original request already succeeded. Verify signatures, record event IDs, quarantine malformed events, process through a durable queue, and reconcile webhook state against the API on a schedule. If you have not worked with signed webhooks and idempotency keys before, this is where the most subtle duplicate-entry bugs live.
Grouping for OSS and producing return-ready exports
For eligible OSS transactions, group ledger data by scheme, Member State of consumption, supply type, rate category, rate, taxable amount, VAT amount, dispatch or establishment Member State, currency, and correction period.
The Union scheme return separates supplies by Member State of consumption and distinguishes services from the Member State of identification, services from other fixed establishments, and goods dispatched from different Member States. It reports taxable and VAT amounts separately for standard and reduced rates. Exempt and zero-rated supplies stay out of the OSS return entirely.
A few operational details that catch people:
- Union and non-Union OSS periods are quarterly; IOSS is monthly. Returns and payment are due by the end of the following month.
- A nil return is required for every period even when no relevant supplies were made. Generate a period checklist and create a zero-value return automatically.
- OSS returns are made in euros, using the ECB reference rate on the last day of the tax period where conversion is needed. Store the source currency, FX rate, rate date, converted amount, and method.
Exports should come from the locked ledger, never a side spreadsheet. Spreadsheet totals lose corrections, dispatch countries, rates, and evidence. An export contains the return period, scheme, Member State of consumption, dispatch country, supply category, rate, taxable and VAT amounts, currency and applied FX rate, current-period and prior-period correction amounts, source invoice IDs, credit-note references, and reconciliation totals.
Be precise about what the API delivers here. Nordlet computes OSS and IOSS returns from invoices and the ledger including prior-period corrections, ships domestic return packs for Lithuania (FR0600), Germany (UStVA) and Poland (JPK_V7M), and generates VMI-ready i.SAF registers for Lithuania. That is return-ready data. One API call does not file every country's VAT return through every national gateway, and no return is submitted to an authority on your behalf. Keep accounting, return generation, export, gateway submission, and payment confirmation as separate states in your system. The EU VAT engine guide documents which cases the engine resolves and where its warnings fire.
Locking the period
After review and approval, reconcile invoices to ledger entries, reconcile ledger VAT balances to the return dataset, export the return, record the filing and payment references, then lock the period. Nordlet blocks postings to a locked month, including through the API. Corrections after locking go into a later period with explicit links to the original document and the affected return period.
Common failure modes and how to avoid them
| Failure mode | Why it happens | Mitigation |
|---|---|---|
| Treating a valid VIES result as sufficient for zero-rating | VIES does not establish exemption rights | Store customer, supply, transport, and invoice evidence alongside the result |
| Treating an invalid result as proof of no registration | The number may not be activated for intra-EU trade, or the database may lag | Distinguish invalid, not-processed, and technical failure; route disputes to review |
| Making checkout synchronous with VIES | Concurrency limits, timeouts, outages | Async validation, short-lived caching, backoff, provisional states |
| Caching results forever | Registration and activation status change | Store timestamps and expiry; re-check before invoicing when stale |
| Mapping products directly to VAT percentages | Rates and exemptions depend on country and date | Product to category to country code to effective-dated rate |
| Reconstructing OSS from spreadsheets | Totals omit corrections, dispatch countries, evidence | Derive from the immutable ledger with dimensions on every entry |
| Including domestic-establishment services in OSS | Certain services belong in the domestic return | Model establishments separately, route by scheme before aggregation |
| Omitting nil returns | OSS requires a return every period | Generate a checklist and auto-create zero-value returns |
| Editing closed invoices to correct | Destroys the filing-to-correction link | Use credit notes and reversing entries in a later period |
What we would do first
If we were starting this integration tomorrow, we would build the tax-category layer and the VAT decision record before touching a single reporting endpoint. Get the taxonomy stable, get products mapped to categories, and get the decision record capturing raw country signals plus the reason a place of supply was chosen. Everything else — VIES, posting, OSS grouping, export — depends on that model being right.
Then we would wire VIES as an asynchronous evidence step with the four states separated, and run it against a sandbox company with fixtures for the ugly cases (timeouts, not-processed results, mismatched prefixes) before letting it near production. The failure modes above are almost entirely avoidable if the data model carries evidence and reporting dimensions from the first posting.
FAQ
Does a valid VIES response mean I can zero-rate an intra-EU B2B sale?
No. VIES confirms a VAT number is registered and, where supported, activated for intra-EU trade. The right to exempt an intra-Community supply depends on additional evidence: the customer being the actual recipient, transport or dispatch proof, correct place of supply, and required invoice wording. Store the VIES result as one input, not the decision.
Should VIES validation block checkout?
It should not. VIES can time out or hit concurrency limits, and an outage is not the same as an invalid number. Validate asynchronously, cache successful results for a documented period, retry transient failures, and hold ambiguous cases for review with a provisional state rather than failing the sale.
Can a cloud accounting API file my VAT returns automatically?
It can generate return-ready data grouped by scheme and Member State, computed from the immutable ledger with corrections included. Whether it also submits through each national filing gateway depends on the product and country. Nordlet computes OSS and IOSS returns, ships domestic return packs for Lithuania, Germany and Poland, and generates VMI-ready i.SAF registers for Lithuania, but it does not submit anything to a tax authority on your behalf. Treat accounting, return generation, export, gateway submission, and payment as separate states rather than assuming one call files everywhere.
How do I handle corrections after a period is locked?
Post them in a subsequent period with explicit links to the original document and the affected return period. Nordlet blocks new postings to a locked month, including through the API, so corrections flow through credit notes or reversing entries rather than edits. This preserves the lineage between the original filing and the adjustment.