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.
Related guides
Add VAT validation to your Go application
Free plan: 20 requests/month, no credit card. Zero dependencies — pure standard library.