vatverify home
/v1/webhooks

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

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
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"  }'
@vatverify/node
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"
201 Created · application/json
{
  "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.

Events

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 each GET /v1/validate request finishes.Inner data is the full validate response: data.valid, data.country, data.company, meta.source.
  • batch.completedFires when a POST /v1/validate/batch job finishes every item.Inner data is the full batch response: data.summary plus per-item data.results.
  • testFires when you call POST /v1/webhooks/:id/test. Bypasses the queue for quick sanity checks.Inner data is a fixed confirmation object. Useful to validate signature verification before going live.
Delivery

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.

validation.completed · signed POST
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    }  }}
Headerson every delivery
Vatverify-Signaturesha256=<64-char-hex>

HMAC-SHA256 over `{timestamp}.{raw_body}`. Always prefixed with sha256=.

Vatverify-Timestampunix seconds

Delivery time in seconds (not milliseconds). Reject if older than 5 minutes.

Vatverify-Eventvalidation.completed · batch.completed · test

Event type. The same value is repeated inside the JSON body.

Vatverify-Delivery-IdUUIDv7

Unique per delivery attempt. Use to dedupe on retries.

Verify

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.

verify.ts · Node crypto
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.

Reliability

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.

3 retries

Exponential backoff. A delivery is marked failed only after the 4th attempt (initial + 3 retries) times out or returns non-2xx.

10s timeout

Per-attempt wait for a 2xx. Respond fast and enqueue heavy work after acknowledging.

5-minute window

Reject signatures where Vatverify-Timestamp is older than 300 seconds. Stops replay attacks cold.

5 endpoints / key

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.