KVK API with Java and Spring Boot: Querying Dutch Business Data in Your Java Application
KVKBase Team

KVK API with Java and Spring Boot: Querying Dutch Business Data in Your Java Application

Integrate the KVKBase API into your Java Spring Boot project. Code examples for RestClient, exception handling, caching and a reusable service class, suited for enterprise Java developers.

kvkapijavaspring-bootdevelopersintegration

Java remains one of the dominant languages in enterprise software. From large webshops to fintech platforms and HR systems, Java and Spring Boot form the backbone of many critical systems. If you work in such an environment and need Dutch business data from the Chamber of Commerce, this guide shows you how to integrate the KVKBase API cleanly using modern Spring Boot patterns.

Requirements

  • Java 21 or higher (LTS)
  • Spring Boot 3.3+
  • Maven or Gradle
  • An API key from KVKBase

Add the Spring Web starter to your project if it’s not already included:

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Step 1: Configuration

Add your API key to application.yml or application.properties. Never hardcode the key in your source code.

# application.yml
kvkbase:
  api-key: ${KVKBASE_API_KEY}
  base-url: https://api.kvkbase.nl/api/v1
  timeout-ms: 5000

Then create a configuration properties class:

// KvkbaseProperties.java
package com.yourcompany.kvk.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "kvkbase")
public class KvkbaseProperties {

    private String apiKey;
    private String baseUrl;
    private int timeoutMs = 5000;

    // Getters and setters
    public String getApiKey() { return apiKey; }
    public void setApiKey(String apiKey) { this.apiKey = apiKey; }

    public String getBaseUrl() { return baseUrl; }
    public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }

    public int getTimeoutMs() { return timeoutMs; }
    public void setTimeoutMs(int timeoutMs) { this.timeoutMs = timeoutMs; }
}

Step 2: Data Classes (Records)

Use Java records for API responses. They are immutable, concise and work well for reading JSON data.

// KvkCompany.java
package com.yourcompany.kvk.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public record KvkCompany(
    String kvkNummer,
    String naam,
    String rechtsvorm,
    boolean actief,
    String startdatum,
    String sbiCode,
    String sbiOmschrijving,
    KvkAddress address
) {}
// KvkAddress.java
package com.yourcompany.kvk.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public record KvkAddress(
    String straat,
    String huisnummer,
    String postcode,
    String plaats,
    String land
) {}

Step 3: Configure RestClient

Spring Boot 3.2+ includes the new RestClient that replaces RestTemplate. Configure a bean with the right headers and timeout:

// KvkbaseClientConfig.java
package com.yourcompany.kvk.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestClient;

@Configuration
public class KvkbaseClientConfig {

    @Bean
    public RestClient kvkbaseRestClient(KvkbaseProperties props) {
        var factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(props.getTimeoutMs());
        factory.setReadTimeout(props.getTimeoutMs());

        return RestClient.builder()
                .baseUrl(props.getBaseUrl())
                .defaultHeader("x-api-key", props.getApiKey())
                .defaultHeader("Accept", "application/json")
                .requestFactory(factory)
                .build();
    }
}

Step 4: KvkbaseService

The service class contains all business logic around KVK lookups. Centralise error handling here so the rest of your application doesn’t need to worry about HTTP details.

// KvkbaseService.java
package com.yourcompany.kvk.service;

import com.yourcompany.kvk.exception.CompanyNotFoundException;
import com.yourcompany.kvk.exception.KvkbaseApiException;
import com.yourcompany.kvk.model.KvkCompany;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatusCode;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

@Service
public class KvkbaseService {

    private static final Logger log = LoggerFactory.getLogger(KvkbaseService.class);

    private final RestClient restClient;

    public KvkbaseService(RestClient kvkbaseRestClient) {
        this.restClient = kvkbaseRestClient;
    }

    /**
     * Retrieves company data by KVK number.
     *
     * @throws CompanyNotFoundException if the KVK number does not exist
     * @throws KvkbaseApiException for other API errors
     */
    public KvkCompany getByKvkNumber(String kvkNumber) {
        log.debug("KVK lookup for number: {}", kvkNumber);

        return restClient.get()
                .uri("/kvk/{kvkNumber}", kvkNumber)
                .retrieve()
                .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
                    if (response.getStatusCode().value() == 404) {
                        throw new CompanyNotFoundException(kvkNumber);
                    }
                    throw new KvkbaseApiException(
                        "Client error during KVK lookup: " + response.getStatusCode()
                    );
                })
                .onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
                    throw new KvkbaseApiException(
                        "Server error during KVK lookup: " + response.getStatusCode()
                    );
                })
                .body(KvkCompany.class);
    }

    /**
     * Checks whether a company is actively registered.
     * Returns false for unknown KVK numbers.
     */
    public boolean isActivelyRegistered(String kvkNumber) {
        try {
            KvkCompany company = getByKvkNumber(kvkNumber);
            return company.actief();
        } catch (CompanyNotFoundException e) {
            return false;
        }
    }
}

Step 5: Custom Exceptions

Create separate exception classes so you can distinguish between “not found” and “API error” downstream:

// CompanyNotFoundException.java
package com.yourcompany.kvk.exception;

public class CompanyNotFoundException extends RuntimeException {
    private final String kvkNumber;

    public CompanyNotFoundException(String kvkNumber) {
        super("No company found with KVK number: " + kvkNumber);
        this.kvkNumber = kvkNumber;
    }

    public String getKvkNumber() { return kvkNumber; }
}
// KvkbaseApiException.java
package com.yourcompany.kvk.exception;

public class KvkbaseApiException extends RuntimeException {
    public KvkbaseApiException(String message) {
        super(message);
    }
}

Step 6: Caching with Spring Cache

KVK data rarely changes. Cache results locally to limit API calls and keep latency low. Spring Cache works well for this pattern.

Add the cache starter:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Optional: Caffeine for an in-memory LRU cache -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

Enable caching and configure TTL:

// CacheConfig.java
package com.yourcompany.kvk.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager("kvkCompanies");
        manager.setCaffeine(Caffeine.newBuilder()
                .expireAfterWrite(6, TimeUnit.HOURS)
                .maximumSize(10_000));
        return manager;
    }
}

Annotate the service method with @Cacheable:

@Cacheable(value = "kvkCompanies", key = "#kvkNumber")
public KvkCompany getByKvkNumber(String kvkNumber) {
    // ... existing implementation
}

A KVK number is now looked up at most once every 6 hours. A good balance between freshness and efficiency.

Step 7: REST Controller

If you want to expose an internal API endpoint so the front-end or other services can retrieve company data:

// KvkController.java
package com.yourcompany.kvk.controller;

import com.yourcompany.kvk.exception.CompanyNotFoundException;
import com.yourcompany.kvk.model.KvkCompany;
import com.yourcompany.kvk.service.KvkbaseService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/companies")
public class KvkController {

    private final KvkbaseService kvkbaseService;

    public KvkController(KvkbaseService kvkbaseService) {
        this.kvkbaseService = kvkbaseService;
    }

    @GetMapping("/{kvkNumber}")
    public ResponseEntity<KvkCompany> getCompany(@PathVariable String kvkNumber) {
        try {
            KvkCompany company = kvkbaseService.getByKvkNumber(kvkNumber);
            return ResponseEntity.ok(company);
        } catch (CompanyNotFoundException e) {
            return ResponseEntity.notFound().build();
        }
    }

    @GetMapping("/{kvkNumber}/active")
    public ResponseEntity<Boolean> isActive(@PathVariable String kvkNumber) {
        boolean active = kvkbaseService.isActivelyRegistered(kvkNumber);
        return ResponseEntity.ok(active);
    }
}

Step 8: Unit Tests with MockRestServiceServer

Test the service class without real HTTP calls using MockRestServiceServer:

// KvkbaseServiceTest.java
package com.yourcompany.kvk.service;

import com.yourcompany.kvk.exception.CompanyNotFoundException;
import com.yourcompany.kvk.model.KvkCompany;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestClient;

import static org.assertj.core.api.Assertions.*;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
import static org.springframework.test.web.client.response.MockRestResponseCreators.*;

@SpringBootTest
class KvkbaseServiceTest {

    @Autowired
    private KvkbaseService service;

    @Autowired
    private RestClient kvkbaseRestClient;

    @Test
    void getByKvkNumber_returnsCompany() {
        MockRestServiceServer server = MockRestServiceServer.bindTo(kvkbaseRestClient).build();

        server.expect(requestTo("https://api.kvkbase.nl/api/v1/kvk/12345678"))
              .andExpect(method(HttpMethod.GET))
              .andRespond(withSuccess("""
                  {
                    "kvkNummer": "12345678",
                    "naam": "Test BV",
                    "rechtsvorm": "Besloten Vennootschap",
                    "actief": true,
                    "startdatum": "2010-01-01"
                  }
                  """, MediaType.APPLICATION_JSON));

        KvkCompany company = service.getByKvkNumber("12345678");

        assertThat(company.naam()).isEqualTo("Test BV");
        assertThat(company.actief()).isTrue();
    }

    @Test
    void getByKvkNumber_throwsExceptionWhenNotFound() {
        MockRestServiceServer server = MockRestServiceServer.bindTo(kvkbaseRestClient).build();

        server.expect(requestTo("https://api.kvkbase.nl/api/v1/kvk/99999999"))
              .andRespond(withStatus(HttpStatus.NOT_FOUND));

        assertThatThrownBy(() -> service.getByKvkNumber("99999999"))
            .isInstanceOf(CompanyNotFoundException.class);
    }
}

Real-world Scenario: KVK Validation During Order Processing

A common use case: verify when creating a B2B order that the customer has an active KVK number.

@Service
public class OrderService {

    private final KvkbaseService kvkbaseService;
    private final OrderRepository orderRepository;

    public OrderService(KvkbaseService kvkbaseService, OrderRepository orderRepository) {
        this.kvkbaseService = kvkbaseService;
        this.orderRepository = orderRepository;
    }

    public Order createOrder(OrderRequest request) {
        // Verify KVK before creating order
        if (request.kvkNumber() != null) {
            boolean active = kvkbaseService.isActivelyRegistered(request.kvkNumber());
            if (!active) {
                throw new InvalidCompanyException(
                    "KVK number " + request.kvkNumber() + " is not active"
                );
            }

            // Enrich order with company name
            KvkCompany company = kvkbaseService.getByKvkNumber(request.kvkNumber());
            return orderRepository.save(new Order(
                request.kvkNumber(),
                company.naam(),
                request.products()
            ));
        }

        return orderRepository.save(new Order(null, null, request.products()));
    }
}

Summary

With a few Spring Boot components you have a robust KVK integration:

  • KvkbaseProperties for configuration via application.yml
  • RestClient bean with API key and timeout
  • KvkbaseService with centralised error handling
  • @Cacheable for efficient result reuse
  • Custom exceptions for clear error distinction
  • Unit tests via MockRestServiceServer

Read more: KVK API with Node.js and TypeScript for a similar approach in JavaScript environments, and Business data for your webshop for checkout integrations specifically aimed at e-commerce.