KVK API with Go: Querying Dutch Company Data in Golang
KVKBase Team

KVK API with Go: Querying Dutch Company Data in Golang

Integrate the KVKBase API into your Go project. Includes code examples for HTTP clients, structs, error handling, and a reusable client library — suitable for both beginners and experienced Go developers.

kvkapigolanggodevelopersintegration

Go is one of the fastest-growing programming languages for backend services, microservices, and command-line tools. Its combination of high performance, straightforward concurrency, and a powerful standard library makes Go a popular choice for teams building reliable API integrations. In this guide, we show you how to integrate the KVKBase API into your Go project to query Dutch company data.

Why Go for KVK integrations?

Go has an excellent built-in HTTP client, solid JSON support, and strict typing via structs. That makes it ideal for API integrations where you need to:

  • Quickly validate KVK numbers during customer registration
  • Retrieve company data for your CRM or ERP
  • Batch-process large lists of KVK numbers
  • Build a microservice or internal tool that needs business data

The KVKBase API returns structured JSON, which maps seamlessly to Go’s encoding/json package and struct definitions.

Requirements

  • Go 1.21 or higher
  • An API key from KVKBase
  • No external dependencies needed — only the Go standard library

Step 1: Define your structs

Start by defining structs that represent the API response. Go’s JSON unmarshaling works best when you add struct tags matching the JSON field names.

// kvkbase/types.go

package kvkbase

type Address struct {
	Street      string `json:"straat"`
	HouseNumber string `json:"huisnummer"`
	PostalCode  string `json:"postcode"`
	City        string `json:"plaats"`
	Country     string `json:"land"`
}

type SbiCode struct {
	Code        string `json:"code"`
	Description string `json:"omschrijving"`
}

type Company struct {
	KvkNumber   string    `json:"kvkNummer"`
	Name        string    `json:"naam"`
	LegalForm   string    `json:"rechtsvorm"`
	Active      bool      `json:"actief"`
	Address     Address   `json:"adres"`
	SbiCodes    []SbiCode `json:"sbiCodes"`
	VatNumber   string    `json:"btwNummer,omitempty"`
}

type APIResponse struct {
	Success bool     `json:"succes"`
	Data    *Company `json:"data,omitempty"`
	Error   string   `json:"fout,omitempty"`
}

Step 2: Build a reusable client

Create a client struct that manages your API key and HTTP client. This is the idiomatic Go approach: dependency injection instead of global variables.

// kvkbase/client.go

package kvkbase

import (
	"encoding/json"
	"fmt"
	"net/http"
	"time"
)

const baseURL = "https://api.kvkbase.nl/api/v1"

type Client struct {
	apiKey     string
	httpClient *http.Client
}

func NewClient(apiKey string) *Client {
	return &Client{
		apiKey: apiKey,
		httpClient: &http.Client{
			Timeout: 10 * time.Second,
		},
	}
}

func (c *Client) GetCompany(kvkNumber string) (*Company, error) {
	url := fmt.Sprintf("%s/kvk/%s", baseURL, kvkNumber)

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}
	req.Header.Set("x-api-key", c.apiKey)
	req.Header.Set("Accept", "application/json")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("API call failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode == 404 {
		return nil, fmt.Errorf("KVK number %s not found", kvkNumber)
	}
	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	var result APIResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("JSON decode failed: %w", err)
	}

	if !result.Success || result.Data == nil {
		return nil, fmt.Errorf("API error: %s", result.Error)
	}

	return result.Data, nil
}

Step 3: Basic usage

// main.go

package main

import (
	"fmt"
	"log"
	"os"

	"yourproject/kvkbase"
)

func main() {
	apiKey := os.Getenv("KVKBASE_API_KEY")
	if apiKey == "" {
		log.Fatal("KVKBASE_API_KEY environment variable not set")
	}

	client := kvkbase.NewClient(apiKey)

	company, err := client.GetCompany("12345678")
	if err != nil {
		log.Fatalf("Error: %v", err)
	}

	fmt.Printf("Company name: %s\n", company.Name)
	fmt.Printf("KVK number:  %s\n", company.KvkNumber)
	fmt.Printf("Legal form:  %s\n", company.LegalForm)
	fmt.Printf("Active:      %v\n", company.Active)
	fmt.Printf("Address:     %s %s, %s %s\n",
		company.Address.Street,
		company.Address.HouseNumber,
		company.Address.PostalCode,
		company.Address.City,
	)
}

Step 4: Concurrent batch lookups with goroutines

One of Go’s greatest strengths is concurrency through goroutines. This is especially useful when you need to process a list of KVK numbers without waiting for each request to complete sequentially.

// batch.go

package kvkbase

import (
	"sync"
)

type BatchResult struct {
	KvkNumber string
	Company   *Company
	Err       error
}

func (c *Client) BulkLookup(kvkNumbers []string, maxConcurrent int) []BatchResult {
	results := make([]BatchResult, len(kvkNumbers))
	sem := make(chan struct{}, maxConcurrent) // semaphore for rate limiting
	var wg sync.WaitGroup

	for i, kvk := range kvkNumbers {
		wg.Add(1)
		go func(idx int, number string) {
			defer wg.Done()
			sem <- struct{}{}        // acquire slot
			defer func() { <-sem }() // release slot

			company, err := c.GetCompany(number)
			results[idx] = BatchResult{
				KvkNumber: number,
				Company:   company,
				Err:       err,
			}
		}(i, kvk)
	}

	wg.Wait()
	return results
}

Set maxConcurrent to avoid hitting the API’s rate limits. A value of 5 to 10 is typically safe.

numbers := []string{"12345678", "87654321", "11223344"}
results := client.BulkLookup(numbers, 5)

for _, r := range results {
	if r.Err != nil {
		fmt.Printf("%s: ERROR - %v\n", r.KvkNumber, r.Err)
		continue
	}
	fmt.Printf("%s: %s (%s)\n", r.KvkNumber, r.Company.Name, r.Company.LegalForm)
}

Error handling in Go

Go’s explicit error handling makes integrations robust. The client uses fmt.Errorf with %w for error wrapping, preserving context as errors propagate upstream:

company, err := client.GetCompany("00000000")
if err != nil {
	if strings.Contains(err.Error(), "not found") {
		// KVK number does not exist
		fmt.Println("Unknown KVK number")
	} else {
		// Network or server error
		log.Printf("API error: %v", err)
	}
	return
}

Using in an HTTP handler (Echo / Gin / chi)

If you’re using Go to build a web API, the pattern is the same. Here is an example using the popular chi framework:

// handlers/kvk.go

package handlers

import (
	"encoding/json"
	"net/http"

	"yourproject/kvkbase"
	"github.com/go-chi/chi/v5"
)

type KvkHandler struct {
	client *kvkbase.Client
}

func NewKvkHandler(client *kvkbase.Client) *KvkHandler {
	return &KvkHandler{client: client}
}

func (h *KvkHandler) GetCompany(w http.ResponseWriter, r *http.Request) {
	kvkNumber := chi.URLParam(r, "kvkNumber")

	company, err := h.client.GetCompany(kvkNumber)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadGateway)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(company)
}

Register the handler in your router:

r := chi.NewRouter()
kvkHandler := handlers.NewKvkHandler(kvkbase.NewClient(os.Getenv("KVKBASE_API_KEY")))
r.Get("/api/company/{kvkNumber}", kvkHandler.GetCompany)

Testing

Go’s testing package is built in. Use a mock HTTP server to test your client without making real API calls:

// kvkbase/client_test.go

package kvkbase_test

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"yourproject/kvkbase"
)

func TestGetCompany(t *testing.T) {
	// Set up mock server
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		response := kvkbase.APIResponse{
			Success: true,
			Data: &kvkbase.Company{
				KvkNumber: "12345678",
				Name:      "Test BV",
				LegalForm: "BV",
				Active:    true,
			},
		}
		json.NewEncoder(w).Encode(response)
	}))
	defer server.Close()

	client := kvkbase.NewClient("test-key")

	company, err := client.GetCompany("12345678")
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}

	if company.Name != "Test BV" {
		t.Errorf("Expected 'Test BV', got '%s'", company.Name)
	}
}

Next steps

With this foundation you can go in any direction:

  • Add caching with sync.Map or Redis to avoid repeat lookups
  • Integrate with your customer onboarding flow to auto-fill company details (see also: KVK data for customer onboarding)
  • Build a webhook listener that reacts to company status changes
  • Combine with company status monitoring for proactive alerts

Ready to start? Get your API key at kvkbase.nl.