{ "woocommerce" :

Dutch company and VAT fields in WooCommerce checkout

Short answer

WooCommerce has no KVK or VAT registry field. You add one yourself: on classic checkout through the woocommerce_billing_fields filter, validated in woocommerce_after_checkout_validation and written to order meta. We do not ship a WooCommerce plugin. We ship a REST API and a widget that you call from those hooks.

cURL
# server-side check at checkout
curl ".../v1/lookup/68750110" \
  -H "Authorization: Bearer KEY"

{
  "name": "Acme B.V.",
  "isActive": true,
  "vat": {
    "number": "NL123456789B01",
    "valid": true
  }
}

Where the company data has to land

WooCommerce has no registry field. It has billing_company, which is free text. The moment you sell B2B in the Netherlands you need three places for the data to end up.

LocationWhat belongs thereWhy
Checkout fieldKVK number, typed or pickedInput and validation
Order metaKVK number, statutory name, VAT numberInvoice and bookkeeping
Customer metaThe same, as a defaultSecond order without retyping

The part teams skip: an order is a snapshot. If you store only the KVK number and re-fetch the name later, your 2024 invoice stops matching reality after the company renames in 2026. Write the name and address onto the order, not just the key.

Classic checkout: field, validation, storage

Three hooks, in this order. Put them in a site-specific plugin rather than in the functions.php of a theme you will replace later.

add_filter( 'woocommerce_billing_fields', function ( $fields ) {
    $fields['billing_kvk'] = [
        'label'    => 'KVK number',
        'required' => false,
        'class'    => [ 'form-row-wide' ],
        'priority' => 35,
    ];
    return $fields;
} );

add_action( 'woocommerce_after_checkout_validation', function ( $data, $errors ) {
    $kvk = trim( $data['billing_kvk'] ?? '' );
    if ( $kvk === '' ) {
        return;
    }
    if ( ! preg_match( '/^\d{8}$/', $kvk ) ) {
        $errors->add( 'billing_kvk', 'A KVK number is 8 digits.' );
        return;
    }

    $res = wp_remote_get( "https://api.kvkbase.nl/v1/lookup/{$kvk}", [
        'timeout' => 3,
        'headers' => [ 'Authorization' => 'Bearer ' . KVKBASE_KEY ],
    ] );

    if ( is_wp_error( $res ) || wp_remote_retrieve_response_code( $res ) >= 500 ) {
        return; // API unreachable: let the order through, flag it afterwards.
    }
    if ( wp_remote_retrieve_response_code( $res ) === 404 ) {
        $errors->add( 'billing_kvk', 'This KVK number is not in the trade register.' );
    }
}, 10, 2 );

add_action( 'woocommerce_checkout_create_order', function ( $order, $data ) {
    if ( ! empty( $data['billing_kvk'] ) ) {
        $order->update_meta_data( '_billing_kvk', wc_clean( $data['billing_kvk'] ) );
    }
}, 10, 2 );

The early return on a 5xx or a transport error is deliberate. A checkout that blocks because a third party is slow costs you revenue. Let the order through and attach an order note or a status flag for manual review.

The blocks checkout is a second implementation

Now that the blocks checkout is the default, the form runs on React and the classic filters never fire. There you register the field through the Additional Checkout Fields API on woocommerce_init, under your own namespace. Two things to know:

  • The value is stored under a namespaced meta key, not under _billing_kvk. Any report or invoice plugin reading your old key returns empty columns.
  • Server-side validation attaches to that API’s own validation hooks, not to woocommerce_after_checkout_validation.

If you run both checkouts side by side, write to the same custom meta key in both paths. Otherwise you build your reporting twice.

The widget as an input helper

If you want customers to search by company name instead of typing a number, attach the widget to the input. One script tag, with CSS selectors for the fields it may fill:

<script
  src="https://widget.kvkbase.nl/v1/widget.js"
  data-apikey="PUBLIC_KEY"
  data-target="#billing_kvk"
  data-autofill="true"
  data-name-target="#billing_company"
  data-city-target="#billing_city"
  data-postal-target="#billing_postcode"
  data-vat-target="#billing_vat"
></script>

The widget fills fields in the browser. That is convenience, not validation: anything a browser fills, a browser can edit. Do the check that matters server-side in the hook above, with a key that is not in your page source. The data-apikey is public by definition.

On classic checkout this works as-is. On the blocks checkout the input does not exist yet when the script loads, so a plain selector matches nothing. Attach the widget after the field renders, or call the API directly from your own block component.

What actually breaks in production

The customer pastes a branch number. Dutch invoices and confirmation mails often show the 12-digit vestigingsnummer instead of the 8-digit KVK number. Your regex catches it, but the error message has to say which number you want, or the customer gives up.

A real company returns 404. Deregistered entries drop out of the register. Treat NOT_FOUND as “no longer active” rather than “typo”, and let an existing customer continue with a manual review flag.

VIES is unreachable. VAT validation runs against the member state service and that service goes down regularly. You get UPSTREAM_ERROR and no valid: true. Never block checkout on it: charge VAT, process the order, and correct afterwards if the reverse charge turns out to apply. The mechanics are covered in VAT number validation with the VIES API.

A bulk backfill hits the rate limit. Enriching existing orders with a foreach over 3,000 customers exhausts the per-minute limit within seconds. Use POST /v1/lookup/batch and process in chunks from a cron action instead of one lookup per row.

Frequently asked

Do you have a WooCommerce plugin?
No. We ship a REST API and a JavaScript widget. You build the WooCommerce side yourself in a child theme or a small site-specific plugin using the standard checkout hooks. In practice that is thirty to fifty lines of PHP.
Should the KVK field be required?
Only if you need to separate business from consumer orders. On a store that serves both, keep it optional and reveal it when the customer fills in a company name. Making it required for everyone costs you conversion on consumer orders.
Does the same code work in the blocks checkout?
No. The blocks checkout runs on React and ignores woocommerce_billing_fields. There you register the field through the Additional Checkout Fields API, and it is stored under a different order meta key than the classic checkout writes.
Can I derive the VAT number from the KVK number?
For a BV or NV, yes. The VAT number is NL plus the KVK number plus B01, and we check that against VIES before returning it. That derivation does not hold for sole traders or partnerships, so leave the field empty there and ask the customer.

Updated:

One call gives you the whole company

KVK data, the derived VAT number and a live VIES check, in a single request. The free plan covers 50 lookups a month and needs no card.

50
free lookups a month
1
request instead of three
0
cards, contracts or sales calls