EU VAT Number Validation in Go

A complete guide to validating EU VAT numbers in Go using the vatnode REST API. Uses only the standard library — net/http and encoding/json — no external dependencies required.

Basic validation

Go — basic validation
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
)

type CountryVat struct {
	VatName      string    `json:"vatName"`
	VatAbbr      string    `json:"vatAbbr"`
	Currency     string    `json:"currency"`
	StandardRate float64   `json:"standardRate"`
	ReducedRates []float64 `json:"reducedRates"`
}

type VatResult struct {
	Valid          bool       `json:"valid"`
	VatID          string     `json:"vatId"`
	CountryCode    string     `json:"countryCode"`
	CountryName    string     `json:"countryName"`
	CompanyName    *string    `json:"companyName"`
	CompanyAddress *string    `json:"companyAddress"`
	Source         string     `json:"source"`
	CheckID        string     `json:"checkId"`
	VerifiedAt     string     `json:"verifiedAt"`
	CountryVat     CountryVat `json:"countryVat"`
}

func ValidateVAT(vatID string) (*VatResult, error) {
	apiKey := os.Getenv("VATNODE_API_KEY")
	endpoint := "https://api.vatnode.dev/v1/vat/" + url.PathEscape(vatID)

	req, err := http.NewRequest(http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode == http.StatusServiceUnavailable {
		return nil, fmt.Errorf("VIES_UNAVAILABLE")
	}
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
	}

	var result VatResult
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, err
	}
	return &result, nil
}

func main() {
	result, err := ValidateVAT("IE6388047V")
	if err != nil {
		if err.Error() == "VIES_UNAVAILABLE" {
			fmt.Println("VIES temporarily unavailable — queue for retry")
			return
		}
		fmt.Printf("Error: %v
", err)
		return
	}

	fmt.Printf("Valid: %v
", result.Valid)
	if result.CompanyName != nil {
		fmt.Printf("Company: %s
", *result.CompanyName)
	}
	fmt.Printf("Check ID: %s
", result.CheckID) // store on invoice
}

With context and timeout

Production code should always pass a context with a deadline:

Go — with context
import (
	"context"
	"net/http"
	"time"
)

func ValidateVATWithContext(ctx context.Context, vatID, apiKey string) (*VatResult, error) {
	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(
		ctx,
		http.MethodGet,
		"https://api.vatnode.dev/v1/vat/"+url.PathEscape(vatID),
		nil,
	)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		// context.DeadlineExceeded or network error
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	// ... same decoding as above
}

Handle VIES_UNAVAILABLE: When vatnode returns HTTP 503, queue the validation for async retry and proceed without blocking the user. Do not retry synchronously in a loop — VIES outages can last minutes.

Add VAT validation to your Go application

Free plan: 20 requests/month, no credit card. Zero dependencies — pure standard library.