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

Send a POST request to /v1/subscriptions with the VAT ID you want to monitor. This endpoint requires dashboard session authentication — you would typically call it from your backend when onboarding a new B2B customer.

cURL — create subscription
curl https://api.vatnode.dev/v1/subscriptions \
  -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer vat_live_your_key_here" \
  -d '{"vatId": "DE134214316"}'

Response (201 Created):

Response JSON
{
  "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: register a webhook endpoint

Webhook endpoints must use HTTPS. vatnode will reject plain HTTP URLs. Send a POST to /v1/webhooks with your endpoint URL.

cURL — register webhook
curl https://api.vatnode.dev/v1/webhooks \
  -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer vat_live_your_key_here" \
  -d '{"url": "https://app.example.com/webhooks/vatnode"}'

Response (201 Created):

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

Save the secret immediately. The secret value is only returned once at creation time and is never shown again. Store it in your environment variables. You will use it to verify the signature on every incoming webhook request.

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! // whsec_... 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 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.