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:
| Event | Fired when |
|---|---|
validation.completed | A single /v1/validate request finishes |
batch.completed | A /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 timeVatverify-Signature: HMAC-SHA256 hex digest, prefixed withsha256=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:
- Create webhook: register a new endpoint and receive its signing secret
- List webhooks: view all registered endpoints (secrets not returned)
- Delete webhook: remove an endpoint immediately
- Test webhook: fire a
testevent directly to verify reachability
Each API key may have up to 5 registered endpoints.