DEV Community

Howth Technology Factory
Howth Technology Factory

Posted on • Originally published at howthtechnologyfactory.pro

Model VIES VAT validation with three states, not true/false

If your VAT-validation interface returns only true or false, where does an upstream VIES outage go?

Too often it becomes false. That turns a temporary service failure into a customer-data failure.

VIES is a European Commission search service over national VAT databases. Those national systems can be unavailable, and the information they return is not uniform. Model that reality in the contract:

type VatValidation =
  | {
      status: "valid";
      vatNumber: string;
      checkedAt: string;
      businessName: string | null;
      address: string | null;
      consultationNumber: string | null;
    }
  | {
      status: "invalid";
      vatNumber: string;
      checkedAt: string;
    }
  | {
      status: "unavailable";
      vatNumber: string;
      checkedAt: string;
      error: string;
      retryable: true;
    };
Enter fullscreen mode Exit fullscreen mode

The discriminated union forces callers to choose an action.

  • valid: continue and retain the check record.
  • invalid: pause and ask for correction or other evidence.
  • unavailable: retry later; never reinterpret the outage as an invalid VAT number.

It also makes optional national data explicit. Business name and address may be absent even when a number is valid. A consultation number is available only when the requester VAT number is included in the check.

Normalize once, and show what was checked

Normalization belongs at the boundary of the validation service. Remove formatting that the contract explicitly permits, normalize the country prefix consistently, and reject impossible shapes before making a network request. Do not scatter slightly different cleanup rules across a checkout form, an invoicing job and a support tool.

The result should contain the normalized VAT number sent upstream. That makes the record reproducible and gives a user something concrete to correct. It also prevents a subtle debugging failure: logging the raw input while the service checks a transformed value. When those values differ, an operator can otherwise spend time investigating a request that VIES never received.

Normalization must not become guesswork. A validator should not silently invent a country prefix or keep stripping characters until an input happens to pass. If an application wants to infer a country from customer data, make that a separate, visible business rule and preserve both the original and derived values.

Separate input failures from upstream unavailability

The three result states describe completed checks, but an API also needs a clear failure taxonomy around them. Malformed input and missing credentials are caller problems. Timeouts, connection failures and an unavailable national service are operational problems. A successful response saying that the number is not valid is a business result.

These categories should drive different behaviour. Caller problems return immediately and should not be retried. Temporary operational failures can enter a bounded retry policy. An invalid result should not be retried automatically in the hope that it changes. That distinction reduces unnecessary VIES traffic and stops a retry queue from hiding genuine data-quality work.

When a retry budget is exhausted, return unavailable with a stable error category and the last attempt time. Avoid exposing raw stack traces or transport details to end users. Keep those details in restricted logs, correlated through a request identifier.

Make retries observable and idempotent

A reliable retry policy has an attempt limit, increasing delays and jitter. Without jitter, a group of workers that fail together can call the same recovering service together again. Without a limit, a supposedly temporary workflow can stay open indefinitely.

Retries must not create duplicate business effects. The validation call itself is a lookup, but the surrounding workflow may reserve an order, send a message or advance an invoice. Keep those side effects outside the retry loop. Persist the validation outcome first, then let the business process consume it idempotently.

Useful operational measurements include attempt count, latency, result state and a coarse upstream-error category. Segment them by country prefix when that is permitted and useful, because VIES depends on national services. Do not put full VAT numbers, business names or addresses into metric labels; high-cardinality personal data is both expensive and difficult to govern.

Preserve one result for every batch input

Batch validation should behave like a map operation, not an all-or-nothing transaction. A temporary failure for one country must not erase successful answers for the other rows. Return one correlated output per input, in a documented order or with a stable item identifier.

That contract makes partial retry straightforward. A follow-up job can select only unavailable items while leaving valid and invalid evidence untouched. It also makes reconciliation possible: the caller can prove that every submitted row reached a terminal result for that attempt.

Be explicit about concurrency. More parallel requests are not automatically better when the dependency is a federation of national systems. Use a bounded worker pool, respect timeouts, and lower concurrency during widespread failures. If the upstream service provides no formal retry-after signal, your own backoff policy should still avoid tight loops.

Store evidence with a retention boundary

For each attempt, retain the normalized value, check time, outcome and optional response fields that the workflow can justify. If a requester VAT number produced a consultation number, keep that relationship with the record. Do not describe the record as a complete tax determination: it proves what the lookup returned at a particular time.

Names, addresses and VAT identifiers may be sensitive business or personal data. Apply access controls and deletion rules from the transaction system instead of retaining validator responses forever. Logs used for engineering diagnosis can often keep a request ID and result category without copying the full response.

These boundaries also improve testing. The pure normalization and mapping functions can be tested without the network; the VIES adapter can be tested against fixtures for valid, invalid and unavailable responses; and the retry coordinator can use a fake clock to prove attempt limits and backoff without slow test suites.

Five tests catch most integration mistakes:

  1. A valid number with name and address.
  2. A valid number without one or both optional identity fields.
  3. An invalid number after deterministic normalization.
  4. A national-service failure represented as unavailable.
  5. A mixed batch that keeps the outcome and timestamp for every input.

Add contract tests for the less obvious edges: a retryable timeout must never map to invalid; omitted identity fields must remain null rather than failing decoding; a second processing attempt must not repeat downstream side effects; and logs or metric labels must not contain the full VAT number. Those tests protect the operational meaning of the states, not just their TypeScript shape.

A valid VIES response is still not a complete tax decision. It confirms registration status for cross-border EU trade at that moment; transaction context remains separate.

The worked state-machine guide and primary sources cover the operational reasoning. You can compare a concrete implementation through the Howth product page, Apify, or MCPize. Verify live pricing and limits before use.

Disclosure: This article was prepared with AI assistance and checked against the cited European Commission sources and the live product contracts.

Top comments (0)