Async VAT Validation: Webhooks, Batch Processing, and Background Validation

Sync VAT validation works for most use cases, but it breaks down in three scenarios: high-latency checkout flows, bulk revalidation of existing customers, and VIES downtime. Async validation solves all three. Submit the request, get an immediate 202, and receive the result via webhook.

When sync validation is not enough

Checkout latency

VIES can take 1 to 3 seconds per lookup depending on the member state. In a checkout flow, that delay is visible to the customer. Some countries (Italy, Spain, Poland) are consistently slow. Async validation lets you accept the order immediately and process the VAT check in the background.

Bulk revalidation

If you have 500+ B2B customers, revalidating their VAT numbers quarterly means 500+ sequential API calls with rate limiting delays. With async batch validation, you submit all numbers in a single request (up to 200 for Pro, 1,000 for Business) and receive one webhook when all results are ready.

VIES downtime resilience

When a country's VIES service is down, sync validation fails with a 503 error. Async validation queues the request and retries automatically with exponential backoff over several hours. When the service recovers, the result is delivered via webhook. Your application never has to handle retry logic.

How async validation works

Single async validation

POST one number to /v1/validate/async, get a 202 with a request_id, and receive a validation.completed or validation.failed webhook when processing finishes.

Batch async validation

POST an array of numbers to /v1/validate/async/batch. Vatly validates each format immediately. Invalid formats are rejected in the 202 response (never queued). Valid items are processed in the background and a single batch.completed webhook is delivered when all items finish.

Example: submitting a batch

const response = await fetch("https://api.vatly.dev/v1/validate/async/batch", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer vtly_live_your_api_key",
  },
  body: JSON.stringify({
    vat_numbers: ["DE123456789", "NL987654321B01", "FR12345678901"],
    cache: true,
  }),
});

const { data } = await response.json();
// data.batch_id: "550e8400-..."
// data.accepted: 3
// data.rejected: []
// data.status: "pending"
// Results arrive via webhook when processing completes

Handling webhook deliveries

Every webhook is signed with HMAC-SHA256 using your signing secret. Always verify the signature before processing the payload.

Webhook handler example

import { createHmac } from "crypto";

// Express / Node.js example
app.post("/webhooks/vatly", (req, res) => {
  const signature = req.headers["x-vatly-signature"];
  const timestamp = req.headers["x-vatly-timestamp"];
  const body = req.body; // raw string

  // Verify signature
  const expected = createHmac("sha256", process.env.VATLY_WEBHOOK_SECRET)
    .update(`${timestamp}.${body}`)
    .digest("hex");

  if (signature !== `sha256=${expected}`) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(body);

  switch (event.event) {
    case "validation.completed":
      // event.data contains the validation result
      updateCustomerVatStatus(event.data.vat_number, event.data.valid);
      break;
    case "batch.completed":
      // event.data.results contains all items
      for (const item of event.data.results) {
        if ("data" in item) {
          updateCustomerVatStatus(item.data.vat_number, item.data.valid);
        }
      }
      break;
    case "validation.failed":
      // event.data.error has the failure reason
      flagForManualReview(event.data.vat_number, event.data.error);
      break;
  }

  res.status(200).send("OK");
});

Use case: quarterly customer revalidation

Companies with hundreds of B2B customers should revalidate VAT numbers periodically. VAT registrations can be revoked, businesses can close, and numbers can become invalid at any time. Quarterly revalidation is a common practice for compliance.

With async batch validation, the process is simple:

  • Query your database for all active customer VAT numbers
  • Submit them in a single batch request (up to 200 or 1,000 depending on your plan)
  • Receive a batch.completed webhook with all results
  • Update your records and flag any newly invalid numbers for review
// Quarterly revalidation script
const customers = await db.query("SELECT vat_number FROM customers WHERE active = true");
const vatNumbers = customers.map((c) => c.vat_number);

const response = await fetch("https://api.vatly.dev/v1/validate/async/batch", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.VATLY_API_KEY}`,
  },
  body: JSON.stringify({ vat_numbers: vatNumbers }),
});

const { data } = await response.json();
console.log(`Batch ${data.batch_id}: ${data.accepted} accepted, ${data.rejected.length} rejected`);
// Results will arrive via webhook

Get started

Async validation and webhooks are available on Pro (starting at 24.17/month) and Business plans.

Frequently asked questions

What happens if VIES is down when I submit an async request?

Vatly queues the request and retries automatically with exponential backoff over several hours. When the service comes back, the result is delivered via webhook. If it stays down for 24+ hours, you receive a validation.failed webhook.

Do async validations count against my monthly quota?

Yes. Each VAT number is counted once when the request is accepted (the 202 response), not when the result is delivered.

Can I mix sync and async validation?

Yes. The sync endpoints (GET /v1/validate, POST /v1/validate/batch) continue to work as before. Use sync for real-time validation and async for bulk processing or when you want resilience against downtime.

What is the maximum batch size for async?

Pro: 200 items per batch. Business: 1,000 items per batch.