VAT Monitoring with Webhooks: Detect Invalid EU Registrations Automatically

When a B2B customer's EU VAT registration lapses mid-subscription, subsequent invoices may carry an unexpected VAT liability. vatnode monitors EU VAT registrations and fires webhooks the moment status changes — so you can act before the next billing cycle.

Why monitoring matters

Validating a customer's VAT number at signup is a good start, but registrations can lapse. A company may deregister from VAT voluntarily, restructure, or simply let their registration expire. If you continue issuing zero-VAT invoices after that happens — relying on a status check that is weeks or months old — you may be applying the reverse charge mechanism without a valid legal basis.

Polling the API manually on a schedule is one option, but it adds operational overhead and still leaves a gap between checks. A better pattern is to subscribe once and receive an event when something changes. That is exactly what vatnode monitoring does.

For the initial validation at checkout or contract creation, see the guide on how to validate EU VAT numbers. Monitoring is the complementary layer for keeping that status current over the lifetime of a subscription. It is available from the Starter plan, which includes up to 25 monitored VAT numbers and 3 webhook endpoints.

Webhook events vatnode sends

When vatnode detects a change during a daily re-validation, it creates an event and delivers it to all active webhook endpoints registered on your account. The four change event types are:

Event typeWhen it firesRecommended action
VAT_BECAME_INVALIDVAT number is deregistered or no longer active in VIESPause reverse charge billing; notify finance team
VAT_BECAME_VALIDPreviously invalid number is now active againResume reverse charge billing after re-confirming status
COMPANY_NAME_CHANGEDLegal trading name updated in the EU registryUpdate invoice recipient name; flag for customer review
COMPANY_ADDRESS_CHANGEDRegistered address updated in the EU registryUpdate invoice recipient address

Name and address change events are only fired when the VAT number is currently valid. If a registration becomes invalid, only VAT_BECAME_INVALID is sent — not a name or address event for the same check cycle.

How to set up VAT monitoring

Step 1: subscribe to a VAT number

Add the VAT IDs you want to watch from the monitoring dashboard. Paste a VAT number, and vatnode starts re-validating it on a schedule. Each subscription tracks the last known validity, company name, and registered address so it can detect when any of them change.

Each subscription looks like this:

Subscription
{
  "id": "sub_01hx9z3k4qw8v2r7n6m5p0y1c",
  "vatId": "DE134214316",
  "countryCode": "DE",
  "status": "active",
  "lastCheckedAt": null,
  "lastCompanyName": null,
  "lastCompanyAddress": null,
  "lastValid": null,
  "createdAt": "2026-04-23T09:15:00.000Z",
  "updatedAt": "2026-04-23T09:15:00.000Z"
}

The first daily check will populate lastValid, lastCompanyName, and lastCompanyAddress. No event is fired on the first check — vatnode needs a baseline to compare against before it can detect a change.

Step 2: receive the webhook delivery

Each webhook endpoint has an HTTPS URL and a signing secret. When a monitored VAT number changes, vatnode POSTs a signed JSON event to that URL. The secret is shown only once when the endpoint is created — store it in your environment variables and use it to verify the signature on every incoming request.

Webhook endpoint setup is rolling out via the dashboard.If you need webhook delivery configured for your account today, reach out and we'll set up your endpoint and signing secret directly. The payload format and signature scheme below are final and stable.

A configured endpoint looks like this:

Webhook endpoint
{
  "id": "wh_01hx9z8m2nq4v6p3k7r0y5w9c",
  "url": "https://app.example.com/webhooks/vatnode",
  "status": "active",
  "secret": "vat_wh_9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "createdAt": "2026-04-23T09:16:00.000Z",
  "updatedAt": "2026-04-23T09:16:00.000Z"
}

Step 3: handle the webhook event in your server

vatnode signs every delivery with an X-vatnode-Signature header. The value is t=<unix_timestamp>,v1=<hmac_sha256>. The HMAC is computed over timestamp.payload using a signing key derived from your webhook secret and webhook ID. Always verify the signature before acting on the event.

Below is a complete Node.js / Express handler that verifies the signature and handles the VAT_BECAME_INVALID event:

Node.js / TypeScript — webhook handler
import express from 'express'
import { createHmac } from 'crypto'

const app = express()

// Use raw body middleware for this route — signature verification requires
// the exact bytes that were sent, before any JSON parsing.
app.post(
  '/webhooks/vatnode',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const signatureHeader = req.headers['x-vatnode-signature'] as string | undefined
    if (!signatureHeader) {
      return res.status(401).json({ error: 'Missing signature header' })
    }

    // Parse t=<timestamp>,v1=<signature>
    const parts = Object.fromEntries(
      signatureHeader.split(',').map((part) => part.split('=') as [string, string])
    )
    const timestamp = parts['t']
    const receivedSig = parts['v1']

    if (!timestamp || !receivedSig) {
      return res.status(401).json({ error: 'Malformed signature header' })
    }

    // Reject events older than 5 minutes to prevent replay attacks
    const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10)
    if (age > 300) {
      return res.status(401).json({ error: 'Webhook timestamp too old' })
    }

    // Verify HMAC-SHA256 signature
    const webhookSecret = process.env.VATNODE_WEBHOOK_SECRET! // vat_wh_... signing key from creation
    const payloadStr = req.body.toString('utf8')
    const expectedSig = createHmac('sha256', webhookSecret)
      .update(`${timestamp}.${payloadStr}`)
      .digest('hex')

    if (expectedSig !== receivedSig) {
      return res.status(401).json({ error: 'Invalid signature' })
    }

    // Signature verified — safe to parse and process
    const event = JSON.parse(payloadStr) as {
      event: string
      data: {
        vatId: string
        countryCode: string
        subscriptionId: string
        previousState?: string
        newState?: string
        previousName?: string
        newName?: string
        previousAddress?: string
        newAddress?: string
      }
      timestamp: string
    }

    // Always respond 200 before doing any async work
    res.status(200).json({ received: true })

    // Handle event types
    switch (event.event) {
      case 'VAT_BECAME_INVALID': {
        const { vatId, countryCode } = event.data
        console.log(`VAT number ${vatId} (${countryCode}) is no longer valid`)

        // Pause reverse charge billing for this customer
        await billingService.pauseReverseCharge(vatId)

        // Alert the finance team
        await notificationService.notify({
          channel: 'finance-alerts',
          message: `Customer VAT number ${vatId} has become invalid. Billing paused pending review.`,
        })
        break
      }

      case 'VAT_BECAME_VALID': {
        const { vatId } = event.data
        console.log(`VAT number ${vatId} is now valid again`)
        // Resume reverse charge billing after re-confirming with your team
        await billingService.resumeReverseCharge(vatId)
        break
      }

      case 'COMPANY_NAME_CHANGED': {
        const { vatId, newName } = event.data
        console.log(`Company name for ${vatId} changed to: ${newName}`)
        await customerService.updateCompanyName(vatId, newName ?? null)
        break
      }

      case 'COMPANY_ADDRESS_CHANGED': {
        const { vatId, newAddress } = event.data
        console.log(`Company address for ${vatId} changed to: ${newAddress}`)
        await customerService.updateCompanyAddress(vatId, newAddress ?? null)
        break
      }

      default:
        console.log(`Unhandled vatnode event: ${event.event}`)
    }
  }
)

app.listen(3000)

The webhook payload shape for a VAT_BECAME_INVALID event:

Webhook payload — VAT_BECAME_INVALID
{
  "event": "VAT_BECAME_INVALID",
  "data": {
    "vatId": "DE134214316",
    "countryCode": "DE",
    "subscriptionId": "sub_01hx9z3k4qw8v2r7n6m5p0y1c",
    "previousState": "valid",
    "newState": "invalid"
  },
  "timestamp": "2026-04-23T07:04:12.381Z"
}

Step 4: take action on the event

The right action depends on your billing model. For subscription businesses, the most common response to VAT_BECAME_INVALID is to:

  • Stop applying the reverse charge mechanism on the next invoice — charge VAT at the applicable rate instead.
  • Notify the customer so they can update their VAT details or confirm their registration status with their tax authority.
  • Alert your finance or accounts team so they can review open invoices if needed.

For COMPANY_NAME_CHANGED and COMPANY_ADDRESS_CHANGED, the recommended action is to update the invoice recipient details in your billing system and flag the customer record for review — the legal entity may have been restructured.

Replacing a direct VIES integration? The vatnode REST API handles VIES querying and national registry fallback without requiring any SOAP libraries in your codebase. See the VIES REST API alternative guide for a side-by-side comparison and migration steps.

Frequently asked questions

How often does vatnode check monitored VAT numbers?

vatnode re-validates all active monitoring subscriptions daily. Each check goes through VIES and, where available, national registry fallback via local tax authority and company registry APIs. Changes detected during a check trigger webhook events immediately — you do not need to wait for a polling interval.

What happens if the webhook delivery fails?

vatnode retries failed deliveries with exponential backoff — up to 5 attempts. The retry schedule is approximately 1 s, 2 s, 4 s, 8 s, and 16 s after the previous attempt. Every delivery attempt (success or failure) is logged and visible in the dashboard under your webhook's delivery history. HTTP 4xx responses (except 429) are treated as permanent failures and are not retried.

Which EU countries are covered by monitoring?

All 27 EU member states + Northern Ireland (XI) are checked via VIES on every monitoring run. National registry fallback adds deeper coverage for a subset of countries where tax authority and company registry APIs are available — this is used automatically when a VIES country node is temporarily unavailable, so monitoring continuity is maintained even during partial VIES outages.

Next steps

Ready to monitor EU VAT registrations automatically?

Starter plan includes 25 monitored VAT numbers and 3 webhook endpoints — no credit card required to get started.