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_wrong notice.
  • location decides storage. contact saves to the customer account, address shows in both billing and shipping and saves two values, order saves to the order only.
  • Types: text, select, checkbox, and date from WooCommerce 11.2.
  • Register on woocommerce_init or later. Since 11.0, registering before after_setup_theme triggers 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.