dij-digital/kvk-laravel

KVK API package for Laravel

Maintainers

Package info

github.com/DIJ-digital/kvk-laravel

pkg:composer/dij-digital/kvk-laravel

Statistics

Installs: 56

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

0.1.1 2026-03-05 10:06 UTC

This package is not auto-updated.

Last update: 2026-03-05 10:07:20 UTC


README

A Laravel package for the Dutch KVK (Kamer van Koophandel) Handelsregister API. Provides a clean, typed, and fakable interface for searching the company registry.

Requirements

  • PHP ^8.4|^8.5
  • Laravel ^12

Installation

composer require dij-digital/kvk-laravel

The service provider and facade are auto-discovered.

Publish the config file:

php artisan vendor:publish --tag=kvk

Configuration

Add your API key to .env:

KVK_API_KEY=your-api-key
KVK_BASE_URL=https://api.kvk.nl

For testing against the KVK sandbox, use:

KVK_API_KEY=l7xx1f2691f2520d487b902f4e0b57a0b197
KVK_BASE_URL=https://api.kvk.nl/test

Usage

Search

Search the KVK registry using the fluent API:

use DIJ\Kvk\Facades\KVK;

$result = KVK::search()
    ->kvkNumber('69599068')
    ->city('Amsterdam')
    ->includeInactiveRegistrations()
    ->get();

foreach ($result->items as $item) {
    echo $item->kvkNumber;  // '69599068'
    echo $item->name;       // 'Test BV Donald'
    echo $item->type;       // 'hoofdvestiging'
    echo $item->active;     // 'Ja'
}

Or pass a SearchParameters DTO directly:

use DIJ\Kvk\Data\SearchParameters;
use DIJ\Kvk\Facades\KVK;

$params = new SearchParameters(
    kvkNumber: '69599068',
    city: 'Amsterdam',
    includeInactiveRegistrations: true,
);
$result = KVK::search()->search($params);

Both approaches produce identical API calls.

Available search parameters

Method Type Description
kvkNumber(string) string 8-digit KVK number
rsin(string) string 9-digit RSIN number
branchNumber(string) string 12-digit branch number
name(string) string Trade name or statutory name
streetName(string) string Street name
city(string) string City name
postalCode(string) string Postal code (with houseNumber or poBoxNumber)
houseNumber(int) int House number (with postalCode)
houseLetter(string) string House letter (with houseNumber)
poBoxNumber(int) int PO box number (with postalCode)
type(array) array Filter: hoofdvestiging, nevenvestiging, rechtspersoon
includeInactiveRegistrations(bool) bool Include dissolved companies (default: false)
page(int) int Page number (default: 1)
resultsPerPage(int) int Results per page (default: 100, max: 100)

Base Profile

Retrieve detailed company information by KVK number using the fluent API:

use DIJ\Kvk\Facades\KVK;

// Get main company profile
$profile = KVK::baseProfile('69599068')->get();
echo $profile->kvkNumber;    // '69599068'
echo $profile->name;         // 'Test Stichting Bolderbast'
echo $profile->statutoryName; // 'Stichting Bolderbast'

// Get owner information
$owner = KVK::baseProfile('69599068')->owner();
echo $owner->legalForm;      // 'BesloteVennootschap'
foreach ($owner->addresses as $address) {
    echo $address->city;     // 'Woerden'
}

// Get main branch details
$mainBranch = KVK::baseProfile('69599068')->mainBranch();
echo $mainBranch->branchNumber;    // '000037178598'
echo $mainBranch->totalEmployees;  // 15

// Get all branches listing
$branches = KVK::baseProfile('69599068')->branches();
echo $branches->totalBranchCount;  // 1
foreach ($branches->branches as $branch) {
    echo $branch->firstTradeName;  // 'Test BV Donald'
}

// Include GPS coordinates in addresses (geoData)
$owner = KVK::baseProfile('69599068')->geoData()->owner();
foreach ($owner->addresses as $address) {
    echo $address->geoData?->gpsLatitude;  // 52.08151653230184
}

Available base profile options

Method Description
get() Main company profile — name, registration dates, trade names, SBI activities
owner() Owner information — legal form, addresses, websites
mainBranch() Main branch details — employees, addresses, trade names, SBI activities
branches() All branches listing — counts and summary per branch
geoData(bool) Include GPS coordinates in address responses (default: false)

Branch Profile

Retrieve detailed branch information by branch number (vestigingsnummer):

use DIJ\Kvk\Facades\KVK;

$branch = KVK::branchProfile('000037178598')->get();
echo $branch->branchNumber;   // '000037178598'
echo $branch->kvkNumber;      // '68750110'
echo $branch->firstTradeName; // 'Test BV Donald'

// Include GPS coordinates in addresses
$branch = KVK::branchProfile('000037178598')->geoData()->get();
foreach ($branch->addresses as $address) {
    echo $address->geoData?->gpsLatitude;
}

Available branch profile options

Method Description
get() Branch profile — trade names, employees, addresses, websites, and SBI activities
geoData(bool) Include GPS coordinates in address responses (default: false)

Naming (Trade Names)

Retrieve trade names by KVK number:

use DIJ\Kvk\Facades\KVK;

$naming = KVK::naming('69599068')->get();
echo $naming->kvkNumber;     // '69599068'
echo $naming->statutoryName; // 'Stichting Bolderbast'
echo $naming->name;          // 'Test Stichting Bolderbast'

foreach ($naming->branches as $branch) {
    echo $branch->branchNumber;     // '000037178598'
    echo $branch->firstTradeName;   // 'Test Stichting Bolderbast' (commercial)
    echo $branch->name;             // 'Stichting Branch' (non-commercial)
    echo $branch->alsoKnownAs;      // 'Branch Alias'
}

Available naming options

Method Description
get() Trade names profile — statutory name, primary name, aliases, and branch-level names

Subscriptions (Mutatieservice)

Monitor changes to KVK registry entries via the Mutatieservice (change notifications):

use DIJ\Kvk\Facades\KVK;

// List all subscriptions
$subscriptions = KVK::subscriptions()->get();
echo $subscriptions->customerId;  // 'customer-123'
foreach ($subscriptions->subscriptions as $sub) {
    echo $sub->id;         // 'subscription-456'
    echo $sub->startDate;  // '2024-01-01T00:00:00Z'
    echo $sub->active;     // true
}

// Get signals (change notifications) for a subscription
$signals = KVK::subscriptions()
    ->subscription('subscription-456')
    ->from('2024-01-01T00:00:00Z')
    ->to('2024-12-31T23:59:59Z')
    ->page(1)
    ->resultsPerPage(50)
    ->signals();

foreach ($signals->signals as $signal) {
    echo $signal->kvkNumber;   // '69792917'
    echo $signal->signalType;  // 'SignaalGewijzigdeInschrijving'
    echo $signal->timestamp;   // '2024-05-14T15:25:13.773Z'
}

// Get a specific signal's full details
$signal = KVK::subscriptions()
    ->subscription('subscription-456')
    ->signal('signal-001');

echo $signal->messageId;              // '3e96fad5-...'
echo $signal->signalType;             // 'SignaalGewijzigdeInschrijving'
echo $signal->registrationTimestamp;  // '2024-05-14T15:25:13.773Z'
echo $signal->relatesTo['kvkNummer']; // '69792917'

Available subscription options

Method Description
get() List all subscriptions for your API key
subscription(string) Scope to a specific subscription (returns a scoped builder)

Available signal options (on scoped subscription)

Method Description
from(string) Filter signals from this ISO 8601 datetime
to(string) Filter signals until this ISO 8601 datetime
page(int) Page number (default: 1)
resultsPerPage(int) Results per page (min: 10, max: 500, default: 100)
signals() List signals for this subscription (paginated)
signal(string) Get a specific signal's full details

Error Handling

The package throws typed exceptions for API failures. All exceptions extend KvkException and expose statusCode and responseBody properties.

Exception When
KvkAuthenticationException HTTP 401 or 403 — invalid or missing API key
KvkServerException HTTP 500+ — KVK API server error
KvkRequestException Any other non-successful HTTP response
use DIJ\Kvk\Exceptions\KvkAuthenticationException;
use DIJ\Kvk\Exceptions\KvkException;

try {
    $result = KVK::search()->kvkNumber('69599068')->get();
} catch (KvkAuthenticationException $e) {
    // Invalid API key — check KVK_API_KEY in .env
    $e->statusCode;    // 401
    $e->responseBody;  // raw response body
} catch (KvkException $e) {
    // Any other API error
    $e->statusCode;
    $e->responseBody;
}

Testing

The package is designed to be easily fakeable in your application tests.

Basic Usage

Use KVK::fake() to replace all KVK API calls with a fake that returns no results:

use DIJ\Kvk\Facades\KVK;

KVK::fake();

$result = KVK::search()->kvkNumber('69599068')->get();
// Returns a SearchResult with 0 items — no HTTP calls made

Faking Specific Responses

Pass response DTOs to KVK::fake() using named parameters to control what each endpoint returns:

use DIJ\Kvk\Data\Responses\BaseProfileResponse;
use DIJ\Kvk\Data\Responses\SearchResponse;
use DIJ\Kvk\Data\ValueObjects\SbiActivity;
use DIJ\Kvk\Facades\KVK;

// Search faking — use withSearchResponses() on the returned FakeKVK
KVK::fake()->withSearchResponses(
    SearchResponse::fake(kvkNumber: '69599068', name: 'Acme BV'),
    SearchResponse::fake(kvkNumber: '12345678', name: 'Other BV'),
);

$result = KVK::search()->get();
// $result->total === 2
// $result->items->first()->kvkNumber === '69599068'

// Customize nested data — e.g., test a specific SBI code
KVK::fake(
    baseProfile: BaseProfileResponse::fake(
        sbiActivities: [SbiActivity::fake(sbiCode: '86101')],
    ),
);

$profile = KVK::baseProfile('69599068')->get();
echo $profile->sbiActivities[0]->sbiCode; // '86101'

Available fake() parameters

Parameter Type Description
$baseProfile ?BaseProfileResponse Custom base profile response
$baseProfileOwner ?BaseProfileOwnerResponse Custom owner response
$baseProfileMainBranch ?BaseProfileMainBranchResponse Custom main branch response
$baseProfileBranches ?BaseProfileBranchesResult Custom branches result
$branchProfile ?BranchProfileResponse Custom branch profile response
$naming ?NamingResponse Custom naming response
$subscriptions ?SubscriptionsResult Custom subscriptions result
$signals ?SignalsResult Custom signals result
$signal ?SignalResponse Custom signal response
...$searchResponses SearchResponse Search responses — pass via withSearchResponses() on the returned FakeKVK

Fluent Builder Interface

You can also update a fake after it's been created using the fluent with* methods:

KVK::fake()
    ->withBaseProfile(BaseProfileResponse::fake(name: 'Updated Name'))
    ->withSearchResponses(SearchResponse::fake(kvkNumber: '11223344'));

DTO Fake Defaults

Every response DTO provides a fake() method with sensible defaults from the KVK test environment. Only specify the fields you need to override:

$response = SearchResponse::fake(
    kvkNumber: '69599068',
    name: 'Test BV Donald',
    type: 'hoofdvestiging',
);
Parameter Default
kvkNumber '69599068'
name 'Test BV Donald'
type 'hoofdvestiging'
active 'Ja'
branchNumber '000037178598'
rsin null
address null

Facade Cleanup

If you use KVK::fake() in tests that extend Laravel's TestCase, cleanup is automatic. For plain PHPUnit tests, add this to your test class:

protected function tearDown(): void
{
    \Illuminate\Support\Facades\Facade::clearResolvedInstances();
    parent::tearDown();
}

Advanced: HTTP-Level Faking

For lower-level tests that need to verify the full HTTP pipeline (including request parameters and response parsing), you can still use Http::fake() directly:

use Illuminate\Support\Facades\Http;

Http::fake([
    'api.kvk.nl/*' => Http::response([
        'pagina' => 1,
        'resultatenPerPagina' => 10,
        'totaal' => 1,
        'resultaten' => [
            [
                'kvkNummer' => '69599068',
                'naam' => 'Test BV',
                'type' => 'hoofdvestiging',
                'actief' => 'Ja',
            ],
        ],
    ]),
]);

$result = KVK::search()->kvkNumber('69599068')->get();

License

MIT