Why Custom Checkout Fields Matter
Every business collects different information at the point of sale. An event ticketing platform needs event preferences, a furniture retailer wants delivery instructions, and a subscription box service requires dietary restrictions. Out of the box, WooCommerce provides standard fields like name, address, and email—but real-world checkout flows demand more. This guide on custom checkout fields in WooCommerce will walk you through two distinct approaches: the modern Block Checkout system and the legacy Classic Checkout, along with validation and persistent storage strategies.
Block Checkout — Use the Additional Checkout Fields API
If you're building on WooCommerce 7.0 or later with the checkout blocks, the Additional Checkout Fields API is the native path forward. This API lets you register custom fields declaratively, with built-in support for validation, conditional display, and integration with the checkout flow.
To register a custom field, you'll use the woocommerce_blocks_checkout_register_checkout_field_extension hook or the JavaScript-side API if you're working with a custom block. Here's how you'd add a gift message field:
<?php
add_action( 'woocommerce_blocks_checkout_register_checkout_field_extension', function( $registry ) {
$registry->register(
array(
'id' => 'gift-message',
'group' => 'contact-information',
'attributes' => array(
'legend' => 'Gift Message (Optional)',
'type' => 'textarea',
'required' => false,
'maxLength' => 500,
),
)
);
} );
?>Notice the group parameter. WooCommerce organizes fields into sections: contact-information, shipping-address, billing-address, and order-notes. Placing your field in the right group improves UX by keeping related inputs together.
When the checkout processes, the field value lands in the checkout context. To access it server-side, hook into woocommerce_store_api_checkout_update_order_from_request and read from the request payload. The Additional Checkout Fields API handles serialization automatically, so you receive clean, validated data.
For complex validation logic—say, checking that a referral code exists in your database—use client-side validation in a custom extension block, then confirm server-side when the order is placed. This hybrid approach gives users immediate feedback while preventing invalid orders.
Classic Checkout — woocommerce_checkout_fields Filter
If you support the legacy shortcode-based checkout or older WooCommerce versions, the woocommerce_checkout_fields filter is your entry point. This filter lets you add, modify, or remove fields from the checkout form programmatically.
Here's a real-world example: a B2B platform needs a company registration number and a delivery time preference:
<?php
add_filter( 'woocommerce_checkout_fields', function( $fields ) {
// Add company registration field to billing section
$fields['billing']['billing_registration_number'] = array(
'type' => 'text',
'label' => 'Company Registration Number',
'placeholder' => 'e.g., ABC123456',
'required' => true,
'class' => array( 'form-row-first' ),
'priority' => 25,
'description' => 'Your business registration ID',
'desc_tip' => true,
);
// Add delivery preference to order notes
$fields['order']['order_delivery_window'] = array(
'type' => 'select',
'label' => 'Preferred Delivery Window',
'required' => true,
'class' => array( 'form-row-full' ),
'options' => array(
'morning' => '8am - 12pm',
'afternoon' => '12pm - 5pm',
'evening' => '5pm - 9pm',
),
'priority' => 10,
);
return $fields;
} );
?>The class parameter controls layout. Use form-row-first and form-row-last to place fields side-by-side; form-row-full spans the entire width. The priority parameter determines field order within its section, with lower numbers appearing first.
You can conditionally show fields based on the cart or customer state. For instance, only show a gift-wrapping option if the customer is buying a specific product category:
<?php
add_filter( 'woocommerce_checkout_fields', function( $fields ) {
// Only show gift wrap for certain product categories
$has_giftable = false;
foreach ( WC()->cart->get_cart() as $item ) {
if ( has_term( 'gifts', 'product_cat', $item['product_id'] ) ) {
$has_giftable = true;
break;
}
}
if ( $has_giftable ) {
$fields['billing']['billing_gift_wrap'] = array(
'type' => 'checkbox',
'label' => 'Add gift wrap (+$5)',
'required' => false,
);
}
return $fields;
} );
?>On the frontend, WooCommerce automatically renders these fields using the checkout template. Your field definitions drive the HTML generation, reducing boilerplate.
Validate — Show Errors for Invalid Input
Custom fields need validation. If a customer leaves a required field blank or enters malformed data, you should reject the submission and display a clear error message. In both checkout systems, validation happens before the order is created.
For Classic Checkout, use the woocommerce_checkout_process hook to run validation logic. This hook fires after form submission but before payment processing:
<?php
add_action( 'woocommerce_checkout_process', function() {
// Validate company registration number format
if ( empty( $_POST['post_data'] ) ) {
return;
}
parse_str( $_POST['post_data'], $post_data );
if ( ! empty( $post_data['billing_registration_number'] ) ) {
$reg_num = sanitize_text_field( $post_data['billing_registration_number'] );
if ( ! preg_match( '/^[A-Z]{3}\d{6}$/', $reg_num ) ) {
wc_add_notice(
'Company registration number must be in format ABC123456.',
'error'
);
}
}
// Validate delivery window was selected
if ( empty( $post_data['order_delivery_window'] ) ) {
wc_add_notice(
'Please select a delivery window.',
'error'
);
}
} );
?>The wc_add_notice() function queues error messages. When validation fails, WooCommerce redisplays the checkout form with errors highlighted. Customers can then correct their input without losing other form data.
For Block Checkout, validation happens in the checkout context before order submission. If you've registered a field with required validation in the Additional Checkout Fields API, WooCommerce enforces it. For custom logic, use the woocommerce_blocks_checkout_update_order_meta hook or register a server-side validation step in your extension.
Always sanitize input. Use sanitize_text_field(), sanitize_email(), or intval() depending on the expected data type. Never trust $_POST directly—malicious requests can bypass client-side validation.
Save to Order — Store on the Order with CRUD Methods
Once validation passes, you need to persist custom field values to the order. WooCommerce provides two storage options: order meta (metadata attached to the order) and custom tables. For most use cases, order meta is simpler and sufficient.
In Classic Checkout, hook into woocommerce_checkout_create_order or woocommerce_checkout_update_order_meta to capture field data and attach it to the order:
<?php
add_action( 'woocommerce_checkout_create_order', function( $order, $data ) {
if ( ! empty( $_POST['post_data'] ) ) {
parse_str( $_POST['post_data'], $post_data );
if ( ! empty( $post_data['billing_registration_number'] ) ) {
$order->update_meta_data(
'_billing_registration_number',
sanitize_text_field( $post_data['billing_registration_number'] )
);
}
if ( ! empty( $post_data['order_delivery_window'] ) ) {
$order->update_meta_data(
'_delivery_window',
sanitize_text_field( $post_data['order_delivery_window'] )
);
}
}
}, 10, 2 );
?>Notice the underscore prefix: _billing_registration_number. This convention marks metadata as "protected" in WooCommerce, hiding it from the order details page by default. If you want the field visible in the admin order editor, omit the underscore or use a different approach.
After calling update_meta_data(), you don't need to save explicitly in this hook—WooCommerce handles persistence. However, if you're working outside the checkout flow, always call $order->save() after updating metadata.
To retrieve custom field data later, use the CRUD getter:
<?php
$order = wc_get_order( $order_id );
$reg_number = $order->get_meta( '_billing_registration_number' );
$delivery_window = $order->get_meta( '_delivery_window' );
echo 'Registration: ' . esc_html( $reg_number );
echo 'Delivery: ' . esc_html( $delivery_window );
?>For Block Checkout with the Additional Checkout Fields API, the field value is automatically serialized into the order if you've registered it correctly. You can still use get_meta() to retrieve it, though the meta key depends on how WooCommerce stores the field internally.
To display custom fields in the admin order details, add a custom section using the woocommerce_admin_order_data_after_order_details hook:
<?php
add_action( 'woocommerce_admin_order_data_after_order_details', function( $order ) {
$reg_number = $order->get_meta( '_billing_registration_number' );
$delivery_window = $order->get_meta( '_delivery_window' );
if ( $reg_number || $delivery_window ) {
echo '<div class="order-data-section">';
echo '<h3>Custom Details</h3>';
if ( $reg_number ) {
echo '<p><strong>Registration:</strong> ' . esc_html( $reg_number ) . '</p>';
}
if ( $delivery_window ) {
echo '<p><strong>Delivery Window:</strong> ' . esc_html( $delivery_window ) . '</p>';
}
echo '</div>';
}
} );
?>This displays your custom fields prominently where staff can see them when fulfilling orders.
Common Mistakes
Mixing Block and Classic Checkout approaches: Registering fields via woocommerce_checkout_fields doesn't work on Block Checkout, and vice versa. Detect which system is active and register fields accordingly, or use a plugin that abstracts the difference.
Forgetting to sanitize and escape: Custom checkout fields are entry points for injection attacks. Always sanitize on input with sanitize_text_field(), sanitize_email(), etc., and escape on output with esc_html() or esc_attr().
Storing sensitive data without encryption: If your custom fields collect payment info, tax IDs, or other sensitive data, don't store it in plain order meta. Use a dedicated secure storage solution or a third-party API.
Not testing with real cart contents: Some validation logic depends on the cart (e.g., conditional fields based on product categories). Test with actual products in the cart during checkout, not just empty carts.
Ignoring accessibility: Custom fields should have proper label elements, clear error messages, and keyboard navigation support. The desc_tip parameter adds helpful tooltips for required fields.
Key Takeaways
- Use the Additional Checkout Fields API for Block Checkout (WooCommerce 7.0+) and the
woocommerce_checkout_fieldsfilter for Classic Checkout. - Organize fields into logical groups and control layout with
classandpriorityparameters. - Validate input with
woocommerce_checkout_processand display errors usingwc_add_notice()before order creation. - Store custom field data as order meta using
update_meta_data(), following the underscore convention for protected fields. - Retrieve and display custom data in the admin and frontend using
get_meta()and appropriate hooks. - Always sanitize input and escape output; never trust raw POST data.






Comments
Loading comments…