Classic checkout snippets don’t automatically work with Checkout Blocks. Here’s how to add, validate and retrieve a custom order field using the supported API.
You add a custom checkout field, refresh the checkout page and see nothing.
There’s no obvious PHP error. The snippet is active. Changing its priority doesn’t help.
Before investigating your theme or adding more CSS, check which checkout your store uses.
A field registered through woocommerce_checkout_fields belongs to the classic checkout approach. The Checkout block uses a different integration path.
That distinction is often the missing piece.
In this tutorial, we’ll add an optional purchase order reference to Checkout Blocks. We’ll also validate its format and retrieve the value from the resulting order.
No paid plugin, React build or theme modification is required.
First, identify your checkout
Open your assigned checkout page in the WordPress editor.
A classic checkout commonly uses:
[woocommerce_checkout] A block-based checkout contains the WooCommerce Checkout block, with inner blocks for its different sections.
If a page builder or another extension replaces your checkout, identify its integration requirements before using either approach.
Here’s the kind of snippet you may already have:
add_filter( 'woocommerce_checkout_fields', function ( $fields ) { $fields['billing']['purchase_order_reference'] = array( 'type' => 'text', 'label' => 'Purchase order reference', 'required' => false, ); return $fields; } ); That does not register the field with Checkout Blocks.
WooCommerce documents the differences in its checkout hook compatibility reference.
This doesn’t mean every WooCommerce PHP hook stops working with Blocks. It means you need the correct extension point for the part you’re changing.
What we’re building
Our field will:
- Appear in the checkout’s Order Information section.
- Be optional.
- Accept a reference such as
PO-104. - Reject values outside our example’s format rules.
- Be saved with the order, rather than as a reusable customer address field.
The format restriction is a business rule for this example: up to 40 ASCII letters, numbers or hyphens. Adjust it if your customers use spaces, slashes or other characters in their references.
Use a staging site and a payment gateway’s test mode while working through this tutorial.
WooCommerce’s guide specifies version 8.9.0 as the minimum for the Additional Checkout Fields API. Use a currently supported WooCommerce release compatible with your site, rather than treating that minimum as a recommendation.
See the official additional fields tutorial for the API requirements.
1. Put the code in a small plugin
Avoid adding checkout functionality directly to a parent theme’s functions.php.
Create this folder and file:
wp-content/plugins/checkout-blocks-order-reference/ └── checkout-blocks-order-reference.php Add the following code:
<?php /** * Plugin Name: Checkout Blocks Order Reference Example * Description: Add an optional purchase-order reference to Checkout Blocks. * Version: 1.0.0 * Requires Plugins: woocommerce * Text Domain: swift-commerce-checkout-example */ defined( 'ABSPATH' ) || exit; add_action( 'woocommerce_init', 'swift_commerce_example_register_order_reference' ); function swift_commerce_example_register_order_reference() { if ( ! function_exists( 'woocommerce_register_additional_checkout_field' ) ) { return; } woocommerce_register_additional_checkout_field( array( 'id' => 'swift-commerce-tutorial/order-reference', 'label' => __( 'Purchase order reference', 'swift-commerce-checkout-example' ), 'location' => 'order', 'type' => 'text', 'required' => false, 'sanitize_callback' => 'swift_commerce_example_sanitize_order_reference', 'validate_callback' => 'swift_commerce_example_validate_order_reference', ) ); } function swift_commerce_example_sanitize_order_reference( $value ) { return is_string( $value ) ? trim( sanitize_text_field( $value ) ) : ''; } function swift_commerce_example_validate_order_reference( $value ) { if ( '' === $value ) { return true; } if ( ! preg_match( '/\A[A-Za-z0-9-]{1,40}\z/', $value ) ) { return new WP_Error( 'swift_commerce_example_invalid_order_reference', __( 'Use up to 40 letters, numbers or hyphens for the purchase order reference.', 'swift-commerce-checkout-example' ) ); } return true; } Activate Checkout Blocks Order Reference Example from Plugins → Installed Plugins.
Add a product to your cart and open checkout. Look for the field in Order Information, not inside the billing address.
Why register on woocommerce_init?
The registration needs to happen after WooCommerce has initialized.
It also needs to be available beyond the visible checkout page. Checkout Blocks submits data through the Store API, so wrapping registration in if ( is_checkout() ) can prevent it from running during requests that process the field.
Register the definition consistently. Let WooCommerce decide where to render it.
2. Choose the field location deliberately
The location is not just a positioning preference.
An order reference belongs to one purchase, so we used:
'location' => 'order', For information that belongs to the customer or an address, the API also provides contact and address locations.
Be particularly careful with address: it applies to both billing and shipping, rather than acting as a billing-only field.
For this example, we don’t want the reference from an earlier purchase automatically becoming the reference for a new order. An order-level field fits that requirement.
3. Keep sanitization and validation separate
These callbacks have different jobs.
The sanitization callback normalizes a single-line string:
trim( sanitize_text_field( $value ) ) The validation callback checks whether the normalized value meets our format rule:
/\A[A-Za-z0-9-]{1,40}\z/ An empty value passes because the field is optional.
PO-104 passes. A reference containing spaces or more than 40 characters fails our example’s rule.
Do not rely exclusively on browser attributes or JavaScript for this check. Server-side validation still matters.
Also, this callback receives a value through WooCommerce’s field API. It is not reading raw $_POST, so don’t copy an old form handler into it and assume the request format is identical.
4. Let WooCommerce save the field
You don’t need a second handler that reads $_POST['purchase_order_reference'] and manually updates order metadata.
The field API handles persistence.
Our field’s registered identifier is:
swift-commerce-tutorial/order-reference That is different from the unnamespaced key in the earlier classic-checkout snippet.
Keep the identifier stable after launch. Changing it later creates a different field identity; it doesn’t automatically migrate previously saved values.
For storage and retrieval details, use the Additional Checkout Fields API reference.
5. Retrieve the value from an order
Inside an existing, authorized order-processing workflow, you can retrieve the reference like this:
use Automattic\WooCommerce\Blocks\Package; use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields; // $order_id comes from your existing, authorized order workflow. $order = wc_get_order( $order_id ); if ( $order instanceof \WC_Order ) { $checkout_fields = Package::container()->get( CheckoutFields::class ); $reference = $checkout_fields->get_field_from_object( 'swift-commerce-tutorial/order-reference', $order, 'other' ); if ( is_string( $reference ) && '' !== $reference ) { echo esc_html( $reference ); } } Place the use statements at the top level of the PHP file, not inside a function. Run the retrieval code after WooCommerce has initialized.
The other group is used here for our order-level field.
This example gets an order through wc_get_order() rather than assuming orders live in WordPress post tables. That matters for stores using High-Performance Order Storage.
The retrieval code does not grant permission to view an order. If you build a public endpoint or customer-facing page around it, enforce authorization and order ownership before exposing the value.
If the field still doesn’t appear
Check these before changing the code.
You’re testing the assigned checkout page
A store can have several pages named “Checkout.” Confirm the page assignment in WooCommerce settings.
The page actually uses Checkout Blocks
This example does not add the field to classic checkout or a separate checkout builder.
The Order Information inner block is present
Inspect the Checkout block’s structure in the editor if that section has been removed.
The plugin is active and registration runs
Check PHP errors on staging. If the registration function is unavailable, our guard returns without registering anything.
Another snippet isn’t registering the same identifier
Use one implementation for the field while debugging.
You aren’t expecting the old billing field’s position
Our field belongs to the order section.
Changing CSS or lowering a classic field’s priority won’t fix a registration problem.
Test more than field visibility
Seeing the input is the first check, not the last.
Run these cases with test orders:
- Leave it empty and confirm checkout succeeds.
- Submit
PO-104and verify the saved value. - Submit an invalid reference and confirm checkout is blocked with a useful message.
- Test an overlength value.
- Repeat as a logged-in customer and, if your store permits it, as a guest.
- Confirm a new checkout does not unexpectedly reuse an earlier order reference.
- Verify the value in the order workflow that will actually consume it.
If staff need the reference in an invoice, export or fulfilment integration, test that output too. Saving a field does not automatically connect it to every third-party system.
Prefer configuration over maintaining a snippet?
Disclosure: I build Swift Commerce.
Its Checkout Field Editor provides a settings-based route for supported additional order fields in Checkout Blocks. That does not mean every classic-checkout capability transfers to Blocks, so check the supported field types and limitations for your setup.
The standalone code above does not require Swift Commerce. Use it when a small, code-managed field suits the project. If a merchant needs to maintain fields themselves, a configuration interface may be more appropriate.
The important part is choosing the integration that matches the checkout you actually run.
A classic checkout field that disappears in Blocks may not be broken code. It may be code targeting a different checkout.
