Vue.js VAT Validation: Real-time Tax ID Checks with Composition API
Integrate real-time EU VAT number validation into Vue.js 3 applications using the Composition API. Custom composable, debounced input, and a server-side proxy pattern to protect your API key.
Vue.js 3's Composition API makes it straightforward to encapsulate VAT validation logic in a reusable composable. The key design principle is the same as any frontend framework: never call the TaxID API directly from the browser. Your API key would be visible in network requests. Instead, build a thin backend endpoint (any server-side framework works — Node.js, PHP, Python, Go) that accepts the VAT number and calls TaxID, then return only the fields your UI needs.
The validation composable manages three reactive states: idle (no input or input too short), loading (API call in flight), and result (one of valid, invalid, or unavailable). Debouncing is essential — without it, every keystroke triggers an API call. A 600ms debounce is the right balance between responsiveness and API quota efficiency.
For the company name confirmation pattern that B2B checkout flows require, bind the composable's company_name and address return values to a read-only confirmation block below the input. Show it only when the state is 'valid'. This gives users the visual confirmation they need before submitting the form: they can see the registered company name and address match what they expect.
Error handling has two distinct cases: format_invalid (user input error — show a format hint immediately) and service_unavailable (VIES is down — show a non-blocking warning and proceed with standard VAT). Never block the user from completing checkout when VIES is unavailable; that creates a hard dependency on an external service with variable uptime.
For server-side rendered applications using Nuxt.js, the same composable works client-side with a Nuxt server route as the backend proxy. The useFetch composable in Nuxt 3 can replace the raw fetch call in the composable, adding SSR support and automatic hydration.
Implementation steps
- 1
Create the server-side proxy endpoint
Add a backend route that accepts POST with a vatNumber body, calls GET /api/v1/validate/:country/:vat with your server-side API key, and returns only the fields your frontend needs: valid, status, companyName, address. This keeps the API key server-side and gives you a place to add rate limiting.
- 2
Build the useVatValidation composable
Create a composable that accepts a vatNumber ref, debounces changes by 600ms using watchEffect and a manual setTimeout, calls your proxy endpoint on each debounced change, and returns reactive refs for state ('idle' | 'loading' | 'valid' | 'invalid' | 'unavailable'), companyName, address, and errorMessage.
- 3
Build the VatInput component
Create a Vue component that takes a modelValue prop (for v-model), emits update:modelValue and vatValidated events, uses useVatValidation internally, and renders inline feedback below the input: a spinner when loading, a green confirmation box with company name when valid, a red error message when invalid, and a yellow warning when unavailable.
- 4
Handle the vatValidated event in the parent form
Listen for the vatValidated event from VatInput in your checkout form component. When the event fires with a valid result, store the vatNumber and companyName in your form state. When the input is cleared, reset these to null. Submit both to your backend along with the order data so the VAT validation is recorded alongside the order.
- 5
Add Nuxt server route for Nuxt 3 projects
For Nuxt 3, create server/routes/api/validate-vat.post.ts. Use the event body to get vatNumber, call the TaxID API with useRuntimeConfig().taxidApiKey, and return the filtered result. Register the key in nuxt.config.ts under runtimeConfig.taxidApiKey (not runtimeConfig.public — keeps it server-only).
Code example
Node.js
const res = await fetch(
'http://localhost:3000/api/v1/validate/DE/DE123456789',
{ headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
const { valid, status, company_name, company_address } = await res.json();
if (valid) {
console.log(`Valid EU business: ${company_name}`);
} else if (status === 'service_unavailable') {
// VIES is temporarily down — retry or allow with manual check
console.log('VIES unavailable — check back in a few minutes');
} else {
console.log('Invalid VAT number — charge local tax rate');
}Python
import requests
res = requests.get(
"http://localhost:3000/api/v1/validate/DE/DE123456789",
headers={"Authorization": "Bearer YOUR_API_KEY"}
)
data = res.json()
if data["valid"]:
print(f"Valid: {data['company_name']}")
elif data["status"] == "service_unavailable":
print("VIES temporarily unavailable")
else:
print("Invalid VAT number")API response
The TaxID API returns a consistent JSON response for every validation request:
{
"valid": true,
"status": "active",
"country_code": "DE",
"vat_number": "123456789",
"company_name": "Example GmbH",
"company_address": "Musterstraße 1, 10115 Berlin",
"request_date": "2026-05-10T00:00:00.000Z",
"cached": false,
"request_id": "req_01j..."
}Error handling
The API uses a consistent Stripe-style error format. Always handle service_unavailable separately — VIES has occasional downtime and you should not reject valid customers during outages.
activeVAT number is valid and the business is registered
invalidVAT number format is wrong or not registered in VIES
service_unavailableVIES or the national system is temporarily down — retry later
Frequently asked questions
Can I use Vue 2 with the Options API instead?
Yes. Extract the debounce and fetch logic from the composable into a mixin, and use data() for the reactive state instead of ref(). The fetch call and response handling are identical — only the Vue-specific wiring changes. Vue 2 with the @vue/composition-api plugin can also use the composable pattern directly.
How do I test the composable with Vue Test Utils?
Mock the fetch call using vi.spyOn(global, 'fetch') in Vitest or jest.spyOn in Jest. Mount a wrapper component that uses the composable, trigger input changes, advance timers past the debounce threshold with vi.runAllTimers(), then assert the reactive state. Avoid making real API calls in tests — they are non-deterministic and consume quota.
Does this work with Pinia or Vuex for global state?
Yes. If VAT validation status needs to be accessible from multiple components (for example, a checkout summary and a billing form), move the composable's state into a Pinia store action. The composable stays as a local helper that calls the store action rather than managing its own state.
Further reading
React VAT Validation: Real-time Checks for B2B Forms
A complete React pattern for EU VAT validation: custom useVatValidation hook, debounced input, and a company confirmatio…
VAT API Node.js: Quick Start Guide for Backend Developers
The shortest path from zero to a working VAT validation call in Node.js. No extra dependencies needed on Node 18+ — just…
Evaluating EU VAT APIs? Compare TaxID with:
Related use cases
Stripe EU VAT: Validate Tax IDs Before Charging Customers
Stripe EU VAT integration guide: validate EU VAT numbers server-side before applying zero-rate B2B e...
UK VAT validation in Shopify B2B
Validate UK VAT numbers for B2B customers in Shopify. Required under UK Making Tax Digital rules for...
WooCommerce Spain NIF/CIF validation
Validate Spanish NIF and CIF numbers in WooCommerce checkout. Automatically apply B2B tax exemptions...