Async VAT Validation: Webhooks and Batch Processing

To process a CSV locally, follow the Python CSV validation guide.

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

In our monitoring, VIES takes 1 to 3 seconds per lookup depending on the member state, with some countries consistently at the slow end. In a checkout flow, that delay is visible to the customer. 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 moves that failure handling out of your request path: Avatcado retries VIES briefly during the lookup and falls back to a recent cached result when one exists. If the registry is down and there is no cached result, you receive a validation.failed webhook and the validation is refunded to your quota, so you can resubmit once the service recovers.

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. Avatcado 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.avatcado.com/v1/validate/async/batch", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer avat_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/avatcado", (req, res) => {
  const signature = req.headers["x-avatcado-signature"];
  const timestamp = req.headers["x-avatcado-timestamp"];
  const body = req.body; // raw string

  // Verify signature
  const expected = createHmac("sha256", process.env.AVATCADO_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.error has the failure reason (event.data contains the vat_number)
      flagForManualReview(event.data.vat_number, event.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.avatcado.com/v1/validate/async/batch", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.AVATCADO_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 €29/month) and Business plans.

Frequently asked questions

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

Async validation is designed to keep registry failures out of your request path. When the lookup runs, Avatcado retries VIES briefly and falls back to a recent cached result when one exists: for a number with a cached result, that result is served (marked stale if it is past the TTL) and delivered via webhook as usual, so your workflow does not even notice the outage. If the upstream registry is down and no cached result exists, the request fails fast rather than hanging: you receive a validation.failed webhook with the failure reason in event.error, and the validation is refunded to your quota, so the failed attempt costs nothing. Your webhook handler should route these failures to a retry queue or manual review and resubmit once the registry recovers. This is the main operational difference from sync validation, where the same situation surfaces as a 503 you have to catch inline while the customer waits.

Do async validations count against my monthly quota?

Yes. Each VAT number in an async request is counted once when the request is accepted, meaning at the 202 response, not when the webhook later delivers the result, so a 500-number batch consumes 500 validations from your quota at submission time. Two refinements make the accounting fair. Numbers that fail format validation are rejected in the 202 response itself and never queued, so obviously malformed input in a batch does not consume quota for a lookup that could never succeed. And when a validation fails because the upstream registry was down with no cached result available, you receive a validation.failed webhook and that validation is refunded to your quota, so you can resubmit after the outage without paying twice. Plan bulk revalidation with this in mind: a quarterly sweep of your active customer base costs its size in validations, on top of whatever your checkout traffic already consumes that month.

Can I mix sync and async validation?

Yes. The sync endpoints (GET /v1/validate and POST /v1/validate/batch) continue to work exactly as before; async adds POST /v1/validate/async and POST /v1/validate/async/batch alongside them, on the same API key, and both styles draw from the same monthly quota and benefit from the same cache. The sensible split follows the use case. Use sync where a human is waiting on the answer: checkout and signup flows, where the result decides tax treatment before the order completes. Use async where nobody is waiting and volume or resilience matters: quarterly revalidation of your customer base, backfilling validation for imported records, or any bulk job where retrying around VIES downtime should not be your code's problem. A common production pattern combines them: sync at checkout for the immediate decision, then a scheduled async batch that revalidates active subscribers and flags any number that has gone invalid before the next billing cycle runs.

What is the maximum batch size for async?

Pro allows up to 200 numbers per async batch and Business up to 1,000, compared with 50 per request on the synchronous batch endpoint, which makes async the right tool once a job outgrows what you want to hold open in a single HTTP request. Submit the array to POST /v1/validate/async/batch: format validation happens immediately, invalid formats are rejected in the 202 response and never queued, and accepted items process in the background with a single batch.completed webhook delivered when all items finish, carrying per-item results you can iterate over. Sizing a job is simple arithmetic: revalidating 3,000 customers on Business takes three batches of 1,000 submitted one after another, while on Pro it takes fifteen batches of 200. Every accepted item still counts against your monthly quota (10,000 on Pro, 50,000 on Business), so the practical ceiling for a bulk sweep is usually the quota rather than the per-batch limit.

Sources

Related guides