VAT Monitoring Webhooks – API Reference

VAT monitoring subscriptions check each registered VAT ID once per day. When the status, company name, or address changes – or when the VIES network was unreachable for the full day — vatnode delivers a signed HTTP POST to every webhook URL you have registered.

Dashboard-only. Subscriptions are created and managed in the dashboard UI. There is no API for creating, pausing or deleting them.

Schedule

Each subscribed VAT ID is checked once per day at 06:00 UTC. If the VIES network (or the country’s national fallback) is unavailable, vatnode automatically retries every hour through 22:00 UTC. If all retries fail, a single VIES_UNAVAILABLE webhook is delivered at 23:00 UTC so you know the daily check did not complete.

In practice this means change webhooks arrive shortly after 06:00 UTC on a normal day, and no later than 23:00 UTC on a day when VIES had an outage.

Payload Format

Every event is delivered as a JSON HTTP POST with the following shape:

Webhook payload
{
  "id": "01HZ...",
  "version": "1",
  "event": "VAT_BECAME_INVALID",
  "data": {
    "vatId": "DE123456789",
    "countryCode": "DE",
    "subscriptionId": "01HZ...",
    "previousState": "valid",
    "newState": "invalid"
  },
  "timestamp": "2026-05-19T06:03:12.847Z"
}

id is a stable, unique identifier for the event – use it to deduplicate retries. version is the envelope schema version (currently 1), so you can safely version-handle future changes.

Request headers:

HeaderValue
X-Vatnode-Signaturet=<unix-ts>,v1=<hex-hmac>
X-Vatnode-EventThe event type (see below)
Content-Typeapplication/json
User-Agentvatnode-Webhook/1.0

Event Types

VAT_BECAME_VALID

The monitored VAT ID was previously invalid in VIES and is now valid.

VAT_BECAME_VALID — data
{
  "vatId": "DE123456789",
  "countryCode": "DE",
  "subscriptionId": "01HZ...",
  "previousState": "invalid",
  "newState": "valid"
}

VAT_BECAME_INVALID

The monitored VAT ID was previously valid and is now invalid – typical when a counterparty is deregistered.

VAT_BECAME_INVALID — data
{
  "vatId": "DE123456789",
  "countryCode": "DE",
  "subscriptionId": "01HZ...",
  "previousState": "valid",
  "newState": "invalid"
}

COMPANY_NAME_CHANGED

The company name returned by VIES for this VAT ID has changed since the last successful check. Only fires while the VAT ID is valid.

COMPANY_NAME_CHANGED — data
{
  "vatId": "DE123456789",
  "countryCode": "DE",
  "subscriptionId": "01HZ...",
  "previousName": "ACME GmbH",
  "newName": "ACME Holdings GmbH"
}

COMPANY_ADDRESS_CHANGED

The company address returned by VIES has changed since the last successful check. Only fires while the VAT ID is valid.

COMPANY_ADDRESS_CHANGED — data
{
  "vatId": "DE123456789",
  "countryCode": "DE",
  "subscriptionId": "01HZ...",
  "previousAddress": "Hauptstr. 1, 10115 Berlin",
  "newAddress": "Friedrichstr. 99, 10117 Berlin"
}

VIES_UNAVAILABLE

Sent at 23:00 UTC only when VIES (and the country’s national fallback, if any) was unreachable for every retry attempt that day. The previous state of the VAT ID remains recorded – no validity change is implied.

VIES_UNAVAILABLE — data
{
  "vatId": "DE123456789",
  "countryCode": "DE",
  "subscriptionId": "01HZ...",
  "reason": "VIES unreachable for full day"
}

webhook.test

Sent only when you click Send test for a webhook in the dashboard (and once automatically right after you create one). It is signed exactly like a real event but carries no VAT data – safe to ignore in your handler. Use it to confirm your endpoint is reachable and your signature check works.

webhook.test — data
{
  "message": "This is a test event from vatnode. Your endpoint is reachable.",
  "webhookId": "01HZ..."
}

Signature Verification

Every delivery carries an X-Vatnode-Signature header of the form t=<unix-ts>,v1=<hex-hmac>. To verify:

  1. Parse t (Unix timestamp in seconds) and v1 (hex HMAC).
  2. Use your per-webhook signing key — the full vat_wh_… string, prefix included, shown once in the dashboard when you create the webhook. (If you lose it, delete the webhook and create a new one.)
  3. Compute HMAC_SHA256(signingKey, "{t}.{rawBody}") as a hex string and compare with v1 in constant time.
  4. Reject deliveries where t is more than ~5 minutes from your server clock to mitigate replay attacks.
Node.js verification
import { createHmac, timingSafeEqual } from 'crypto'

export function verifyVatnodeSignature(rawBody, header, signingKey) {
  const parts = Object.fromEntries(
    header.split(',').map((kv) => kv.split('='))
  )
  const t = Number(parts.t)
  const v1 = parts.v1
  if (!t || !v1) return false
  if (Math.abs(Date.now() / 1000 - t) > 300) return false

  const expected = createHmac('sha256', signingKey)
    .update(`${t}.${rawBody}`)
    .digest('hex')

  const a = Buffer.from(expected, 'hex')
  const b = Buffer.from(v1, 'hex')
  return a.length === b.length && timingSafeEqual(a, b)
}
Python verification
import hmac, hashlib, time

def verify_vatnode_signature(raw_body: bytes, header: str, signing_key: str) -> bool:
    parts = dict(kv.split('=', 1) for kv in header.split(','))
    t = int(parts.get('t', 0))
    v1 = parts.get('v1', '')
    if not t or not v1:
        return False
    if abs(time.time() - t) > 300:
        return False

    expected = hmac.new(
        signing_key.encode(),
        f"{t}.{raw_body.decode()}".encode(),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, v1)

Retry Policy

vatnode considers any 2xx response a successful delivery. Non-2xx responses and network errors are retried with exponential backoff:

  • Up to 5 attempts total.
  • Delays between attempts: 1s, 2s, 4s, 8s (exponential, starting after the first failure).
  • Each attempt has a 30 second request timeout.
  • 4xx responses (except 429) are treated as permanent failures and are not retried – fix your endpoint and the next event will deliver normally.
  • 5xx and 429 trigger a retry.

Idempotency

Because deliveries can be retried, your endpoint must be idempotent. Every event carries a stable top-level id that is identical across retries of the same event – store the ids you have already processed and skip duplicates.

Endpoint Requirements

  • Must be reachable on the public internet over HTTPS. We block delivery to private or loopback addresses (SSRF protection).
  • Respond within 30 seconds and return a 2xx status to acknowledge.
  • Verify the signature before trusting the payload.