name: kvk-api-development description: "Search and query the Dutch KVK (Kamer van Koophandel) company registry. Activates when looking up companies, searching KVK numbers, querying trade register data, working with KVK API responses, or testing KVK API calls; or when the user mentions KVK, Kamer van Koophandel, company search, Dutch business registry, trade register, handelsregister, or company lookup." license: MIT metadata: author: hanwoolderink
KVK API Development
When to Apply
Activate this skill when:
- Searching the Dutch KVK company registry
- Looking up companies by KVK number, name, or address
- Working with KVK API responses (parsing results, iterating companies)
- Testing or mocking KVK API calls
- Configuring KVK API credentials
Configuration
Environment variables
Add to .env:
KVK_API_KEY=your-api-key
KVK_BASE_URL=https://api.kvk.nl
For the KVK sandbox/test environment:
KVK_API_KEY=l7xx1f2691f2520d487b902f4e0b57a0b197
KVK_BASE_URL=https://api.kvk.nl/test
Publishing config
php artisan vendor:publish --tag=kvk
This publishes config/kvk.php with two keys: base_url and api_key.
Search API
The search API wraps GET /api/v2/zoeken. There are two equivalent approaches:
Fluent builder (recommended)
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'
}
DTO approach
use DIJ\Kvk\Data\Parameters\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
| Fluent method | DTO property | Type | Description |
|---|---|---|---|
kvkNumber(string) |
kvkNumber |
?string |
8-digit KVK number |
rsin(string) |
RSIN |
?string |
9-digit RSIN number |
branchNumber(string) |
branchNumber |
?string |
12-digit branch number (vestigingsnummer) |
name(string) |
name |
?string |
Trade name or statutory name |
streetName(string) |
streetName |
?string |
Street name |
city(string) |
city |
?string |
City name |
postalCode(string) |
postalCode |
?string |
Postal code (requires houseNumber or poBoxNumber) |
houseNumber(int) |
houseNumber |
?int |
House number (requires postalCode) |
houseLetter(string) |
houseLetter |
?string |
House letter (requires houseNumber) |
poBoxNumber(int) |
poBoxNumber |
?int |
PO box number (requires postalCode) |
type(array) |
type |
?array |
Filter: 'hoofdvestiging', 'nevenvestiging', 'rechtspersoon' |
includeInactiveRegistrations(bool) |
includeInactiveRegistrations |
?bool |
Include dissolved companies (default: false) |
page(int) |
page |
?int |
Page number (default: 1) |
resultsPerPage(int) |
resultsPerPage |
?int |
Results per page (default: 100, max: 100) |
Response Handling
get() and search() return a SearchResult with:
$result = KVK::search()->name('Acme')->get();
$result->items; // SearchResponseCollection (extends Illuminate\Support\Collection)
$result->page; // int — current page number
$result->resultsPerPage; // int — results per page
$result->total; // int — total result count
$result->previous; // ?string — link to previous page
$result->next; // ?string — link to next page
Each item in $result->items is a SearchResponse:
foreach ($result->items as $item) {
$item->kvkNumber; // string — '69599068'
$item->name; // string — 'Test BV Donald'
$item->type; // string — 'hoofdvestiging', 'nevenvestiging', or 'rechtspersoon'
$item->active; // string — 'Ja' or 'Nee'
$item->rsin; // ?string
$item->branchNumber; // ?string — 12-digit vestigingsnummer
$item->address; // ?SearchResultAddress (with ->domesticAddress and ->foreignAddress sub-objects)
$item->expiredName; // ?string — expired trade name that matched the search
$item->links; // array<Link> — HATEOAS links to related resources
}
Result type values
| Type | Meaning |
|---|---|
hoofdvestiging |
Main branch — has basisprofiel and vestigingsprofiel links |
nevenvestiging |
Secondary branch — has both links |
rechtspersoon |
Legal entity — has basisprofiel link only, no branchNumber |
Error Handling
The package throws typed exceptions for API failures. All exceptions extend KvkException, which exposes ->statusCode (int) and ->responseBody (string).
| 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 (KvkRequestException, KvkServerException)
$e->statusCode; // HTTP status code
$e->responseBody; // raw response body
}
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 |
The API response uses Dutch field names (kvkNummer, naam, actief). The package DTOs translate these to English properties. When faking with Http::fake(), use the Dutch names in the response array.
Common Pitfalls
- Exact word matching only — the KVK search API matches complete words. Searching
koophandreturns 0 results;koophandelworks. - Inactive registrations excluded by default — call
->includeInactiveRegistrations()or set the parameter totrueto include dissolved companies. - Max 100 results per page —
resultsPerPagecaps at 100. Usepage()to paginate beyond that. - Dutch response fields — when faking HTTP responses, use the Dutch API field names (
kvkNummer,naam,plaats,actief) not the English DTO property names. - postalCode requires a partner —
postalCodeonly works combined withhouseNumberorpoBoxNumber. - Type values are Dutch — filter values are
hoofdvestiging,nevenvestiging,rechtspersoon(not English translations).
Base Profile API
Retrieve detailed company profiles using a KVK number. The API provides the main profile, owner information, main branch details, and a listing of all branches.
Usage
Use the baseProfile() method on the facade with an 8-digit KVK number:
use DIJ\Kvk\Facades\KVK;
// Get main company profile
$profile = KVK::baseProfile('69599068')->get();
echo $profile->kvkNumber; // '69599068'
echo $profile->name; // 'Test Stichting Bolderbast'
// Get owner information
$owner = KVK::baseProfile('69599068')->owner();
echo $owner->legalForm; // 'BesloteVennootschap'
// Get main branch details
$mainBranch = KVK::baseProfile('69599068')->mainBranch();
echo $mainBranch->branchNumber; // '000037178598'
// Get all branches listing
$branches = KVK::baseProfile('69599068')->branches();
echo $branches->totalBranchCount; // 1
// With geoData (adds GPS coordinates to addresses)
$owner = KVK::baseProfile('69599068')->geoData()->owner();
Response shapes
BaseProfileResponse (from get())
kvkNumber: string— 8-digit KVK numbernonMailingIndicator: string— 'Ja' or 'Nee'name: string— primary company nameformalRegistrationDate: ?string— YYYYMMDD formatmaterialRegistration: ?MaterialRegistration— withstartDateandendDatepropertiesstatutoryName: ?string— statutory name (statutaire naam)tradeNames: list<TradeName>— each withnameandordersbiActivities: list<SbiActivity>— each withsbiCode,sbiDescription,mainActivityIndicatorlinks: list<Link>— HATEOAS links
BaseProfileOwnerResponse (from owner())
rsin: ?string— 9-digit RSIN numberlegalForm: ?string— e.g. 'BesloteVennootschap'extendedLegalForm: ?string— full legal form descriptionaddresses: list<Address>— structured addresses (with optionalgeoData)websites: list<string>— list of website URLslinks: list<Link>
BaseProfileMainBranchResponse (from mainBranch())
branchNumber: string— 12-digit branch numberkvkNumber: string— 8-digit KVK numberrsin: ?string— 9-digit RSIN numbernonMailingIndicator: string— 'Ja' or 'Nee'firstTradeName: string— primary trade name for this branchmainBranchIndicator: string— 'Ja' or 'Nee'commercialBranchIndicator: string— 'Ja' or 'Nee'fullTimeEmployees: ?inttotalEmployees: ?intpartTimeEmployees: ?inttradeNames: list<TradeName>addresses: list<Address>websites: list<string>sbiActivities: list<SbiActivity>links: list<Link>
BaseProfileBranchesResult (from branches())
kvkNumber: string— 8-digit KVK numbercommercialBranchCount: int— number of commercial branchesnonCommercialBranchCount: int— number of non-commercial branchestotalBranchCount: int— total number of branchesbranches: BaseProfileBranchCollection— typed collection ofBaseProfileBranchResponseitemslinks: list<Link>
Each BaseProfileBranchResponse in the collection contains:
branchNumber: string— 12-digit branch numberfirstTradeName: string— primary trade namemainBranchIndicator: string— 'Ja' or 'Nee'commercialBranchIndicator: string— 'Ja' or 'Nee'fullAddress: ?string— flat address stringlinks: list<Link>
Faking Base Profile Calls
KVK::fake() covers all endpoints including the base profile. No separate configuration is required.
use DIJ\Kvk\Data\Responses\BaseProfileResponse;
use DIJ\Kvk\Facades\KVK;
// Default fake
KVK::fake();
$profile = KVK::baseProfile('69599068')->get();
// Returns BaseProfileResponse::fake() defaults
// Custom response
KVK::fake(
baseProfile: BaseProfileResponse::fake(name: 'Custom Company'),
);
$profile = KVK::baseProfile('69599068')->get();
echo $profile->name; // 'Custom Company'
Branch Profile API
Retrieve detailed branch information using a 12-digit branch number (vestigingsnummer).
Usage
use DIJ\Kvk\Facades\KVK;
// Get branch profile
$branch = KVK::branchProfile('000037178598')->get();
echo $branch->branchNumber; // '000037178598'
echo $branch->kvkNumber; // '68750110'
echo $branch->firstTradeName; // 'Test BV Donald'
// Include geo coordinates in address payloads
$branch = KVK::branchProfile('000037178598')->geoData()->get();
Response shape
branchNumber: string— 12-digit branch numberkvkNumber: string— 8-digit KVK numbernonMailingIndicator: string— 'Ja' or 'Nee'firstTradeName: stringmainBranchIndicator: string— 'Ja' or 'Nee'commercialBranchIndicator: string— 'Ja' or 'Nee'rsin: ?stringformalRegistrationDate: ?string— YYYYMMDDmaterialRegistration: ?MaterialRegistrationstatutoryName: ?stringfullTimeEmployees: ?inttotalEmployees: ?intpartTimeEmployees: ?inttradeNames: list<TradeName>addresses: list<Address>websites: list<string>sbiActivities: list<SbiActivity>links: list<Link>
Faking Branch Profile Calls
KVK::fake() also supports branch profile calls:
use DIJ\Kvk\Data\Responses\BranchProfileResponse;
use DIJ\Kvk\Facades\KVK;
// Default fake
KVK::fake();
$branch = KVK::branchProfile('000037178598')->get();
// Custom response
KVK::fake(
branchProfile: BranchProfileResponse::fake(firstTradeName: 'Custom Branch'),
);
$branch = KVK::branchProfile('000037178598')->get();
echo $branch->firstTradeName; // 'Custom Branch'
Naming API
Retrieve trade names using an 8-digit KVK number.
Usage
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;
echo $branch->firstTradeName; // commercial branches
echo $branch->name; // non-commercial branches
echo $branch->alsoKnownAs; // alias for non-commercial branches
}
Response shape
kvkNumber: string— 8-digit KVK numberstatutoryName: stringname: stringrsin: ?stringalsoKnownAs: ?stringstartDate: ?string— YYYYMMDDendDate: ?string— YYYYMMDD, nullablebranches: NamingBranchCollectionlinks: list<Link>
Each NamingBranchResponse item contains:
branchNumber: stringfirstTradeName: ?string— present for commercial branchestradeNames: list<TradeName>— present for commercial branchesname: ?string— present for non-commercial branchesalsoKnownAs: ?string— present for non-commercial brancheslinks: list<Link>
Faking Naming Calls
KVK::fake() also supports naming calls:
use DIJ\Kvk\Data\Responses\NamingResponse;
use DIJ\Kvk\Facades\KVK;
// Default fake
KVK::fake();
$naming = KVK::naming('69599068')->get();
// Custom response
KVK::fake(
naming: NamingResponse::fake(statutoryName: 'Custom Corp'),
);
$naming = KVK::naming('69599068')->get();
echo $naming->statutoryName; // 'Custom Corp'
Verification
- Run
php artisan tinkerand execute a search to verify credentials and connectivity - Check that
.envhasKVK_API_KEYset (the default1234is a placeholder and will return 401) - For sandbox testing, use base URL
https://api.kvk.nl/testwith test API keyl7xx1f2691f2520d487b902f4e0b57a0b197
Subscriptions API (Mutatieservice)
Monitor changes to KVK registry entries via the Mutatieservice. This API uses a scoped sub-object pattern: you first access the repository, then scope to a specific subscription to access signals.
Usage
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'
Response shapes
SubscriptionsResult (from get())
customerId: string— customer ID linked to the API keysubscriptions: SubscriptionCollection— typed collection ofSubscriptionResponse
Each SubscriptionResponse contains:
id: string— subscription identifiercontract: SubscriptionContract— withidpropertystartDate: string— ISO 8601 datetimeendDate: ?string— ISO 8601 datetime, null when activeactive: bool— whether the subscription is currently active
SignalsResult (from signals())
signals: SignalListItemCollection— typed collection ofSignalListItempage: int— current page numberresultsPerPage: int— results per pagetotal: int— total signal counttotalPages: int— total number of pagesprevious: ?string— link to previous pagenext: ?string— link to next page
Each SignalListItem contains:
id: string— signal identifiertimestamp: string— ISO 8601 datetimekvkNumber: string— 8-digit KVK numbersignalType: string— signal type enum (e.g.SignaalGewijzigdeInschrijving)branchNumber: ?string— 12-digit branch number, if applicable
SignalResponse (from signal())
messageId: string— unique message identifiersignalType: string— signal type enumregistrationId: string— registration identifierregistrationTimestamp: string— ISO 8601 datetimerelatesTo: array<string, mixed>— polymorphic payload (shape depends on signal type)
Signal types
| Signal type | Description |
|---|---|
SignaalGewijzigdeInschrijving |
Changed registration |
SignaalGewijzigdeVestiging |
Changed branch |
SignaalNieuweInschrijving |
New registration |
SignaalBeeindiging_2025_01 |
Termination |
SignaalRechtsvormwijziging_2025_01 |
Legal form change |
SignaalVoortzettingEnOverdracht_2025_01 |
Continuation/transfer |
SignaalAdreswijziging_2025_01 |
Address change |
SignaalNaamgeving_2025_01 |
Name change |
SignaalFusieSplitsing_2025_01 |
Merger/split |
SignaalActiviteitenWijziging_2025_01 |
Activity change |
Faking Subscription Calls
KVK::fake() also supports subscriptions and signals:
use DIJ\Kvk\Data\Responses\SignalResponse;
use DIJ\Kvk\Data\Results\SignalsResult;
use DIJ\Kvk\Data\Results\SubscriptionsResult;
use DIJ\Kvk\Facades\KVK;
// Default fake
KVK::fake();
$subscriptions = KVK::subscriptions()->get();
// Custom responses
KVK::fake(
subscriptions: SubscriptionsResult::fake(customerId: 'my-customer'),
signals: SignalsResult::fake(total: 42),
signal: SignalResponse::fake(messageId: 'custom-id'),
);
$subscriptions = KVK::subscriptions()->get();
echo $subscriptions->customerId; // 'my-customer'