ATverify

Webhooks

Receive real-time notifications when validations and batch jobs complete, without polling the API.

Overview

Webhooks let vatverify push results to your server the moment they are ready. Instead of polling /v1/validate or checking batch status on a timer, you register an HTTPS endpoint and vatverify sends an HTTP POST to it whenever a relevant event occurs.

This is especially useful for batch jobs, where results may take several seconds to produce. Your endpoint receives the full response payload, the same data you would get by calling the API directly.

Webhooks require a Pro or Business plan. Test-mode API keys do not trigger delivery.


Events

vatverify currently emits two event types:

EventFired when
validation.completedA single /v1/validate request finishes
batch.completedA /v1/validate/batch job finishes processing all numbers

Payload structure

Every webhook POST has the same envelope:

{
  "event": "validation.completed",
  "created_at": "2026-04-01T12:34:56.789Z",
  "data": {
    "data": {
      "valid": true,
      "vat_number": "DE811569869",
      "country": { "code": "DE", "name": "Germany" },
      "company": {
        "name": "Zalando SE",
        "address": "Tamara-Danz-Str. 1, 10243 Berlin"
      },
      "verified_at": "2026-04-01T12:34:55.000Z"
    },
    "meta": {
      "request_id": "0190f8ea-a5b2-7000-a123-000000000000",
      "cached": false,
      "source": "vies",
      "source_status": "live",
      "latency_ms": 312
    }
  }
}

The inner data field is the complete API response, identical to what you would receive if you called /v1/validate directly. For batch.completed events, it contains the full batch result object. The per-delivery unique ID is carried on the Vatverify-Delivery-Id header, not inside the payload.


Verifying signatures

Every webhook request includes four headers:

  • Vatverify-Timestamp: Unix timestamp (seconds) at delivery time
  • Vatverify-Signature: HMAC-SHA256 hex digest, prefixed with sha256=
  • Vatverify-Event: event name (e.g. validation.completed)
  • Vatverify-Delivery-Id: UUID v7 uniquely identifying this delivery attempt

The signature is computed over the string {timestamp}.{raw_body} using the signing secret returned when you created the endpoint, then prefixed with the literal string sha256=. Strip the prefix before comparing hex bytes.

Always verify the signature before processing a webhook. This confirms the request originated from vatverify and was not tampered with in transit.

vatverify rejects replays: if the timestamp is more than 5 minutes old, consider the request invalid regardless of signature validity.

import { createHmac, timingSafeEqual } from "crypto"

export function verifyWebhookSignature(
  rawBody: string,
  timestamp: string,
  signature: string,
  secret: string,
): boolean {
  const fiveMinutes = 5 * 60 * 1000
  const requestTime = parseInt(timestamp, 10) * 1000

  if (Date.now() - requestTime > fiveMinutes) return false

  // Incoming header is "sha256=<hex>"; strip the prefix before comparing.
  const receivedHex = signature.startsWith("sha256=")
    ? signature.slice("sha256=".length)
    : signature

  const payload = `${timestamp}.${rawBody}`
  const expectedHex = createHmac("sha256", secret).update(payload).digest("hex")

  const expectedBuf = Buffer.from(expectedHex, "hex")
  const receivedBuf = Buffer.from(receivedHex, "hex")

  if (expectedBuf.length !== receivedBuf.length) return false

  return timingSafeEqual(expectedBuf, receivedBuf)
}

Use this in your endpoint handler before doing anything with the payload:

// Next.js App Router example
export async function POST(req: Request) {
  const rawBody = await req.text()
  const timestamp = req.headers.get("Vatverify-Timestamp") ?? ""
  const signature = req.headers.get("Vatverify-Signature") ?? ""

  const valid = verifyWebhookSignature(
    rawBody,
    timestamp,
    signature,
    process.env.VATVERIFY_WEBHOOK_SECRET!,
  )

  if (!valid) return new Response("Unauthorized", { status: 401 })

  const event = JSON.parse(rawBody)
  // handle event.event, event.data ...

  return new Response(null, { status: 204 })
}

Delivery reliability

Webhooks are delivered through a durable message queue. If your endpoint does not respond with a 2xx status within 10 seconds, delivery will be retried up to 3 times with exponential back-off before the attempt is marked as failed.

To avoid retries, respond with a 2xx as quickly as possible, even before you finish processing the event. Enqueue heavy work asynchronously.


Managing endpoints

Use the API to register and manage your webhook endpoints:

Each API key may have up to 5 registered endpoints.