Tutorial9 min readAlberto García

VAT API Go (Golang): High-Performance Tax ID Validation

Go's standard library net/http handles TaxID API calls without any third-party dependencies. This guide covers the typed client, context-based timeouts, all four status handling, and a sync.Map cache.

gogolangvatapitutorial

Go's standard library handles HTTP calls without any third-party dependencies, making the TaxID API integration straightforward. The full implementation — struct types, HTTP client, context timeout, all error states, and a thread-safe in-memory cache — fits in a single file. For the use-case guide with step-by-step wiring, see the Go VAT validation use case.

Response Types

govat/types.go
package vat

type Status string

const (
	StatusActive           Status = "active"
	StatusInactive         Status = "inactive"
	StatusFormatInvalid    Status = "format_invalid"
	StatusServiceUnavail   Status = "service_unavailable"
)

type Response struct {
	Valid       bool   `json:"valid"`
	Status      Status `json:"status"`
	Vat         string `json:"vat"`
	CountryCode string `json:"country_code"`
	CompanyName string `json:"company_name"`
	Address     string `json:"address"`
	Cached      bool   `json:"cached"`
	RequestID   string `json:"request_id"`
}

HTTP Client

govat/client.go
package vat

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"
)

const baseURL = "https://taxid.dev/api/v1"

var httpClient = &http.Client{Timeout: 6 * time.Second}

func Validate(ctx context.Context, vatNumber string) (*Response, error) {
	normalised := strings.ToUpper(strings.ReplaceAll(vatNumber, " ", ""))
	country := normalised[:2]

	req, err := http.NewRequestWithContext(
		ctx,
		http.MethodGet,
		fmt.Sprintf("%s/validate/%s/%s", baseURL, country, normalised),
		nil,
	)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Authorization", "Bearer "+os.Getenv("TAXID_API_KEY"))

	resp, err := httpClient.Do(req)
	if err != nil {
		return &Response{Status: StatusServiceUnavail}, nil // network error → unavailable
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("taxid api: %d", resp.StatusCode)
	}

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

Thread-Safe Cache with sync.Map

govat/cache.go
package vat

import (
	"context"
	"sync"
	"time"
)

type cachedResult struct {
	result    *Response
	cachedAt  time.Time
}

type Client struct {
	cache  sync.Map
	maxAge time.Duration
}

func NewClient(cacheMaxAge time.Duration) *Client {
	return &Client{maxAge: cacheMaxAge}
}

func (c *Client) Validate(ctx context.Context, vatNumber string) (*Response, error) {
	key := strings.ToUpper(strings.ReplaceAll(vatNumber, " ", ""))

	// Check cache
	if v, ok := c.cache.Load(key); ok {
		entry := v.(cachedResult)
		if time.Since(entry.cachedAt) < c.maxAge {
			r := *entry.result
			r.Cached = true
			return &r, nil
		}
	}

	result, err := Validate(ctx, vatNumber)
	if err != nil {
		return nil, err
	}

	// Only cache definitive results
	if result.Status != StatusServiceUnavail {
		c.cache.Store(key, cachedResult{result: result, cachedAt: time.Now()})
	}

	return result, nil
}

// Usage:
// client := vat.NewClient(23 * time.Hour)
// result, err := client.Validate(ctx, "DE123456789")

HTTP Handler (net/http)

gohandlers/vat.go
package handlers

import (
	"encoding/json"
	"net/http"
	"your/module/vat"
)

var vatClient = vat.NewClient(23 * time.Hour)

func ValidateVatHandler(w http.ResponseWriter, r *http.Request) {
	vatNumber := r.URL.Query().Get("vat")
	if vatNumber == "" {
		http.Error(w, `{"error":"vat_required"}`, http.StatusBadRequest)
		return
	}

	result, err := vatClient.Validate(r.Context(), vatNumber)
	if err != nil {
		http.Error(w, `{"error":"api_error"}`, http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"valid":       result.Valid,
		"status":      result.Status,
		"companyName": result.CompanyName,
	})
}

Start validating EU VAT numbers

Free plan — 100 validations/month. No credit card required.

AG
Alberto García

Founder, TaxID

Building EU VAT validation tools for developers. Obsessed with compliance automation and developer experience.