If you added a checkout field with woocommerce_checkout_fields or printed one on woocommerce_after_order_notes, and it's simply not there on a newer store, the store is probably using the Checkout block. Since WooCommerce 8.3 (November 2023), new installs get the Cart and Checkout blocks by default.
The Checkout block builds its form in React and talks to the Store API. Most classic checkout hooks never run there. WooCommerce's own hook compatibility table lists woocommerce_after_order_notes, woocommerce_before_order_notes and woocommerce_review_order_before_submit as not supported, and woocommerce_checkout_fields only partly. Validation on woocommerce_checkout_process doesn't fire either, so an old snippet fails silently: no field, no error, no saved meta.
The supported way is the Additional Checkout Fields API (stable since WooCommerce 8.9).
Register the field
Put this in a small plugin rather than functions.php, so the field survives a theme change.
<?php
/**
* Plugin Name: My Checkout Fields
* Requires Plugins: woocommerce
*/
add_action( 'woocommerce_init', function () {
if ( ! function_exists( 'woocommerce_register_additional_checkout_field' ) ) {
return;
}
woocommerce_register_additional_checkout_field( array(
'id' => 'my-plugin/po-number',
'label' => __( 'Purchase order number', 'my-plugin' ),
'optionalLabel' => __( 'Purchase order number (optional)', 'my-plugin' ),
'location' => 'order',
'type' => 'text',
'attributes' => array( 'maxLength' => 20, 'autocomplete' => 'off' ),
'sanitize_callback' => function ( $value ) {
return strtoupper( str_replace( ' ', '', $value ) );
},
'validate_callback' => function ( $value ) {
if ( ! preg_match( '/^[A-Z0-9-]{4,20}$/', $value ) ) {
return new WP_Error( 'my-plugin-invalid-po', __( 'Use 4 to 20 letters, numbers or dashes.', 'my-plugin' ) );
}
},
) );
} );
Things that trip people up:
-
The ID needs a namespace (
namespace/name). Without the slash, registration fails with a_doing_it_wrongnotice. -
locationdecides storage.contactsaves to the customer account,addressshows in both billing and shipping and saves two values,ordersaves to the order only. -
Types:
text,select,checkbox, anddatefrom WooCommerce 11.2. -
Register on
woocommerce_initor later. Since 11.0, registering beforeafter_setup_themetriggers a notice.
Validate from somewhere else
validate_callback runs after sanitize_callback. For checks added by another plugin, use the action, and add to the WP_Error you're given. Returning a new one does nothing.
add_action( 'woocommerce_validate_additional_field', function ( WP_Error $errors, $key, $value ) {
if ( 'my-plugin/po-number' === $key && 0 === strpos( (string) $value, 'TEST' ) ) {
$errors->add( 'my-plugin-test-po', __( 'Test PO numbers are not accepted.', 'my-plugin' ) );
}
}, 10, 3 );
One naming trap: the docs call the order-location hook woocommerce_blocks_validate_location_other_fields, but the 11.2.1 source fires woocommerce_blocks_validate_location_order_fields. If a cross-field rule never runs, check which name your version fires.
Read the value
The admin order screen, thank-you page and order emails show the field without extra code. In your own code, use the helper instead of raw meta keys:
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
add_action( 'woocommerce_store_api_checkout_order_processed', function ( WC_Order $order ) {
$fields = Package::container()->get( CheckoutFields::class );
$po = $fields->get_field_from_object( 'my-plugin/po-number', $order, 'other' );
if ( $po ) {
$order->add_order_note( sprintf( 'PO number: %s', $po ) );
}
} );
Use the group 'other' for contact and order fields, and 'billing' or 'shipping' for address fields. And don't wait for woocommerce_checkout_create_order: it doesn't fire on the block checkout.
When the classic checkout is still the right call
The API only adds fields. Keep the shortcode checkout if you need to change or remove core fields, use a type that isn't listed (file upload, radio, textarea), show an address field in only one address, or run an extension that only supports the classic form. Fields registered with this API don't appear on the shortcode checkout, so that route still needs classic hooks.
I wrote a longer version with a select and a checkbox example, the cross-field validation hook, the meta keys for export tools, and an FAQ on my blog: How to add a custom field to WooCommerce block checkout.
Top comments (1)
The silent failure is the most useful warning here. Testing the field on a staging order first, before touching a live checkout, would save a lot of store owners a painful surprise.