IRIsuru Ratnayake

How to Add Custom Fields to the WooCommerce Block Checkout

Add custom fields to the WooCommerce block checkout with the Additional Checkout Fields API. Register, validate and read values, with working PHP examples.

Isuru Ratnayake

Isuru Ratnayake

· Updated · 5 min read

WooCommerce's block-based checkout is now the default checkout on new stores, and it breaks a lot of familiar advice. Most tutorials still show the classic checkout hooks, such as the woocommerce_checkout_fields filter or woocommerce_after_order_notes, and none of them affect the block checkout. If you added a field that way and it never appeared, that's why.

I ran into this on a client project that needed an extra reference number at checkout. This guide shows the approach that works: WooCommerce's Additional Checkout Fields API. It covers where fields can go, how to register them, how to validate them, and how to read the values back in your own code.

Where you can add custom fields

The block checkout lets you place a custom field in one of three locations:

Location value Where it appears Typical use
contact Contact information, under the email address Alternative email, customer reference, VAT number
address Inside the shipping and billing address forms Delivery instructions, building access code
order Additional order information, before the order note "How did you hear about us?", purchase-order number

One thing to keep in mind: an address field is shown in both the shipping and the billing address, and WooCommerce stores a separate value for each. You can't currently show it in only one of them.

Step 1: Register the field

Fields are registered in PHP with woocommerce_register_additional_checkout_field(). Call it on the woocommerce_init action, which is what the current WooCommerce docs recommend. Registering earlier can cause initialisation and translation problems.

Here is the field from my client project: an optional "Matter number" in the order section.

add_action( 'woocommerce_init', function () {
	if ( ! function_exists( 'woocommerce_register_additional_checkout_field' ) ) {
		return; // WooCommerce is too old or not active.
	}

	woocommerce_register_additional_checkout_field(
		array(
			'id'            => 'my-plugin/matter-number',
			'label'         => 'Matter number',
			'optionalLabel' => 'Matter number (only if you have one)',
			'location'      => 'order', // 'contact', 'address' or 'order'
			'required'      => false,
			'attributes'    => array(
				'autocomplete' => 'off',
				'title'        => 'Your matter number, if you already have one',
				'pattern'      => '[A-Za-z0-9]{5}', // optional: exactly 5 letters/numbers
				'data-custom'  => 'matter-number',
			),
		)
	);
} );

Tip: the id must be unique and namespaced as namespace/field-name. Use your plugin or theme slug as the namespace so the field never clashes with another extension.

To move the field to a different part of the checkout, change location to contact or address. No other change is needed.

When I first wrote this in May 2024, many examples used the woocommerce_blocks_loaded hook. Current WooCommerce versions recommend woocommerce_init, so use that for new code.

Field options you can use

Option Required What it does
id Yes Unique ID in namespace/field-name format
label Yes Field label, also used as the placeholder
optionalLabel No Label shown when the field is optional (default: label + "(optional)")
location Yes contact, address or order
type No text (default), select, checkbox or date
required No true/false, or a JSON Schema rule for conditional logic
hidden No false, or a JSON Schema rule to hide the field conditionally
attributes No Extra HTML attributes (data-*, aria-*, autocomplete, pattern, title, maxLength, readOnly, …). Not supported on select
sanitize_callback No Cleans the submitted value
validate_callback No Checks the value and returns a WP_Error if it's invalid

A dropdown (select) field

A select field needs an options array. Each option needs a unique value and a label. Only the value is sent to the server.

woocommerce_register_additional_checkout_field(
	array(
		'id'       => 'my-plugin/how-did-you-hear',
		'label'    => 'How did you hear about us?',
		'location' => 'order',
		'type'     => 'select',
		'options'  => array(
			array( 'value' => 'google',   'label' => 'Google' ),
			array( 'value' => 'referral', 'label' => 'A friend or colleague' ),
			array( 'value' => 'other',    'label' => 'Other' ),
		),
	)
);

A select field has no default value, so if you make it required the customer must actively pick an option.

Step 2: Sanitise and validate the value

Never trust checkout input. Both callbacks are set per field. The sanitize callback runs first, and validation runs on the cleaned value:

woocommerce_register_additional_checkout_field(
	array(
		'id'                => 'my-plugin/alt-email',
		'label'             => 'Alternative email for invoices',
		'location'          => 'contact',
		'required'          => false,
		'sanitize_callback' => function ( $field_value ) {
			return sanitize_email( $field_value );
		},
		'validate_callback' => function ( $field_value ) {
			if ( '' !== $field_value && ! is_email( $field_value ) ) {
				return new WP_Error( 'invalid_alt_email', 'Please enter a valid email address.' );
			}
		},
	)
);

When validation returns a WP_Error, WooCommerce shows the message on the checkout and stops the order from being placed. That's exactly what you want for bad input.

Step 3: Read the saved value in your code

You'll usually need the value later, for example in an order email, an admin column or an export. Use WooCommerce's CheckoutFields service rather than guessing meta keys.

Address fields have a billing and a shipping value:

use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;

$field_id        = 'my-plugin/delivery-notes';
$order           = wc_get_order( $order_id );
$checkout_fields = Package::container()->get( CheckoutFields::class );

$billing_value  = $checkout_fields->get_field_from_object( $field_id, $order, 'billing' );
$shipping_value = $checkout_fields->get_field_from_object( $field_id, $order, 'shipping' );

Contact and order fields are stored together in the "other" group. The simplest reliable way to read them is to get all of them at once:

$checkout_fields = Package::container()->get( CheckoutFields::class );
$other_fields    = $checkout_fields->get_all_fields_from_object( $order, 'other' );

$matter_number = $other_fields['my-plugin/matter-number'] ?? '';

Address field values can also be read from a customer object, for example wc()->customer, by passing it in place of the order. During checkout, use wc()->customer rather than new WC_Customer( $id ): a logged-in customer's saved data is only updated once the order is placed, so a fresh customer object can return old values.

Limitations to know before you start

  • The API is for the block checkout. If a store still uses the classic [woocommerce_checkout] shortcode, you need the classic hooks there (or switch the store to the block checkout).
  • You can't choose the meta key. WooCommerce stores the values under its own prefixed keys, so read them with the methods above.
  • Field types are limited to text, select, checkbox and date for now.
  • Address fields always appear in both billing and shipping.

Quick checklist

  1. Make sure the store's checkout page uses the Checkout block, not the shortcode.
  2. Register the field on woocommerce_init with a namespaced id.
  3. Pick the right location: contact, address or order.
  4. Add sanitize_callback and validate_callback for anything customers type.
  5. Read values with get_field_from_object() or get_all_fields_from_object().
  6. Test a guest checkout and a logged-in checkout, then check the order in wp-admin.

Further reading

Need custom checkout fields, conditional logic or a checkout integration built for your store? I build custom WooCommerce plugins for clients worldwide. Tell me about your project and I'll reply within a few hours.

  • WooCommerce
  • WordPress
  • PHP
  • Checkout
Isuru Ratnayake

Isuru Ratnayake

Senior WordPress & PHP Developer. Top Rated on Upwork with 250+ completed jobs and 15+ years of PHP & WordPress experience.