Push, not poll.
Signed, retried, durable.
Register an HTTPS endpoint. The moment a validation or batch job finishes, vatverify POSTs the full response to your server, HMAC-signed and retried through a durable queue.
Register an endpoint, get a secret.
POST /v1/webhooks with an HTTPS URL, or use the dashboard. The response includes a whsec_ signing secret, returned once. Store it server-side. The dashboard shows a red dot on endpoints with recent failures and a full response-body drawer so you can debug without spelunking logs.
curl -X POST "https://api.vatverify.dev/v1/webhooks" \ -H "Authorization: Bearer $VATVERIFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks/vatverify" }'import { Vatverify } from "@vatverify/node"const vatverify = new Vatverify()const endpoint = await vatverify.webhooks.create( "https://example.com/webhooks/vatverify",)endpoint.id // "019d917e-4fa4-766e-9e0f-d977b47bf2c6"endpoint.secret // "whsec_..." (shown once; store it)endpoint.url // "https://example.com/webhooks/vatverify"{ "id": "019d917e-4fa4-766e-9e0f-d977b47bf2c6", "url": "https://example.com/webhooks/vatverify", "secret": "whsec_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", "created_at": "2026-04-21T10:00:00.000Z" }
See the full endpoint schema: Register a webhook endpoint · List · Delete · Test.
Three events, one envelope.
Every POST carries the same shape: event, created_at, data. The inner data field is the exact response the API would have returned.
validation.completedFires after eachGET /v1/validaterequest finishes.Innerdatais the full validate response:data.valid,data.country,data.company,meta.source.batch.completedFires when aPOST /v1/validate/batchjob finishes every item.Innerdatais the full batch response:data.summaryplus per-itemdata.results.testFires when you callPOST /v1/webhooks/:id/test. Bypasses the queue for quick sanity checks.Innerdatais a fixed confirmation object. Useful to validate signature verification before going live.
What lands on your server.
Four headers carry signature, timestamp, event, and delivery id. The body is the JSON envelope: exactly what you'd get if you called the API yourself.
POST https://example.com/webhooks/vatverifyContent-Type: application/jsonVatverify-Event: validation.completedVatverify-Timestamp: 1776067200Vatverify-Signature: sha256=7b5c1e8f4a9d...Vatverify-Delivery-Id: 019d91a4-e2cd-7c9a-9f2b-3a1e4b6c8d2f{ "event": "validation.completed", "created_at": "2026-04-21T10:00:00.000Z", "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-21T09:59:59.421Z" }, "meta": { "request_id": "0190f8ea-a5b2-7000-a123-000000000000", "cached": false, "source": "vies", "source_status": "live", "latency_ms": 312 } }}Vatverify-Signaturesha256=<64-char-hex>HMAC-SHA256 over `{timestamp}.{raw_body}`. Always prefixed with sha256=.
Vatverify-Timestampunix secondsDelivery time in seconds (not milliseconds). Reject if older than 5 minutes.
Vatverify-Eventvalidation.completed · batch.completed · testEvent type. The same value is repeated inside the JSON body.
Vatverify-Delivery-IdUUIDv7Unique per delivery attempt. Use to dedupe on retries.
Constant-time, replay-protected.
Compute HMAC-SHA256 over {timestamp}.{raw_body} with your signing secret, then compare in constant time. Reject anything older than 5 minutes.
import { createHmac, timingSafeEqual } from "crypto"export function verifyWebhookSignature( rawBody: string, timestamp: string, signature: string, secret: string,): boolean { // Reject replays older than 5 minutes const fiveMinutes = 5 * 60 * 1000 const requestTime = parseInt(timestamp, 10) * 1000 if (Date.now() - requestTime > fiveMinutes) return false // Header is "sha256=<hex>"; strip the prefix const receivedHex = signature.startsWith("sha256=") ? signature.slice("sha256=".length) : signature const payload = `${timestamp}.${rawBody}` const expectedHex = createHmac("sha256", secret) .update(payload) .digest("hex") const expected = Buffer.from(expectedHex, "hex") const received = Buffer.from(receivedHex, "hex") if (expected.length !== received.length) return false return timingSafeEqual(expected, received)}Full walk-through with a Next.js App Router handler example: Webhooks guide.
Durable queue, not fire-and-forget.
Events are handed to a durable message queue before the client response returns, so a slow endpoint never blocks your API call.
Exponential backoff. A delivery is marked failed only after the 4th attempt (initial + 3 retries) times out or returns non-2xx.
Per-attempt wait for a 2xx. Respond fast and enqueue heavy work after acknowledging.
Reject signatures where Vatverify-Timestamp is older than 300 seconds. Stops replay attacks cold.
Register up to five HTTPS endpoints per API key. Test-mode keys never trigger validation or batch events.
Included on Pro and Business.
Webhook delivery is part of every Pro and Business plan at no extra cost. Free and Starter use the synchronous API response directly.