The whole path, not just the field
Adding a field to the checkout is a quarter of the job. The value has to be validated, saved to the order, shown to staff in the admin, included in the customer's and the shop's emails, printed on the packing slip, and passed to whatever fulfilment or accounting system reads orders. A field that appears on the checkout and vanishes afterwards is the most common half-finished customisation on WooCommerce stores.
Here is the full path for the checkout block, using the Additional Checkout Fields API, with a worked example: a preferred delivery date and a gift message.
Register the fields
add_action( 'woocommerce_init', function () {
if ( ! function_exists( 'woocommerce_register_additional_checkout_field' ) ) return;
woocommerce_register_additional_checkout_field( [
'id' => 'acme/delivery-date',
'label' => 'Preferred delivery date',
'location' => 'order',
'type' => 'text',
'required' => false,
'attributes' => [
'placeholder' => 'YYYY-MM-DD',
'autocomplete' => 'off',
'pattern' => '\d{4}-\d{2}-\d{2}',
],
] );
woocommerce_register_additional_checkout_field( [
'id' => 'acme/gift-message',
'label' => 'Gift message (optional)',
'location' => 'order',
'type' => 'text',
'attributes' => [ 'maxLength' => 160 ],
] );
} );location => 'order' puts them in the "Additional information" step. contact places a field beside email; address adds it to billing and shipping addresses and saves it against the customer for reuse. Field IDs are namespaced (acme/...) so they cannot collide with core or other plugins.
The block has no native date picker type; a text field with a pattern and placeholder is the supported route. A real date picker is a JavaScript fill, covered in the checkout block customisation article.
Validate
add_action( 'woocommerce_validate_additional_field', function ( WP_Error $errors, $key, $value ) {
if ( 'acme/delivery-date' !== $key || '' === $value ) return;
$date = DateTime::createFromFormat( 'Y-m-d', $value );
if ( ! $date || $date->format( 'Y-m-d' ) !== $value ) {
$errors->add( 'acme_bad_date', 'Please enter the delivery date as YYYY-MM-DD.' );
return;
}
if ( $date < new DateTime( '+2 days' ) ) {
$errors->add( 'acme_too_soon', 'Delivery dates need at least two days\' notice.' );
}
if ( in_array( (int) $date->format( 'N' ), [ 6, 7 ], true ) ) {
$errors->add( 'acme_weekend', 'We do not deliver at weekends.' );
}
}, 10, 3 );
add_filter( 'woocommerce_sanitize_additional_field', function ( $value, $key ) {
return 'acme/gift-message' === $key ? sanitize_textarea_field( $value ) : $value;
}, 10, 2 );Errors surface inline in the block. Validation runs server-side on submit, so it holds even if the browser's pattern is bypassed.
Saving
Nothing to do. The API stores order and contact fields against the order and address fields against both the order address and the customer profile. Read them with:
$date = $order->get_additional_field_value( 'acme/delivery-date' );Under the hood they are order meta with a _wc_other/ or _wc_billing/ style prefix; use the accessor, not the meta key.
Showing in the admin
Also automatic: additional fields render in the order edit screen's "Additional fields" panel. If you want them more prominent, add an order meta box:
add_action( 'add_meta_boxes', function () {
$screen = wc_get_container()->get( \Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled()
? wc_get_page_screen_id( 'shop-order' ) : 'shop_order';
add_meta_box( 'acme-delivery', 'Delivery', function ( $post_or_order ) {
$order = $post_or_order instanceof WC_Order ? $post_or_order : wc_get_order( $post_or_order->ID );
echo '<p><strong>Date:</strong> ' . esc_html( $order->get_additional_field_value( 'acme/delivery-date' ) ?: 'Not specified' ) . '</p>';
echo '<p><strong>Gift message:</strong> ' . esc_html( $order->get_additional_field_value( 'acme/gift-message' ) ?: 'None' ) . '</p>';
}, $screen, 'side', 'high' );
} );The screen ID dance handles stores with and without High-Performance Order Storage. Also consider a column on the orders list for the delivery date, via manage_woocommerce_page_wc-orders_columns (HPOS) or manage_edit-shop_order_columns.
Emails
Customers should see what they entered; the shop needs it to fulfil. Add to every relevant email after the order details:
add_action( 'woocommerce_email_after_order_table', function ( $order, $sent_to_admin, $plain_text ) {
$date = $order->get_additional_field_value( 'acme/delivery-date' );
$msg = $order->get_additional_field_value( 'acme/gift-message' );
if ( ! $date && ! $msg ) return;
if ( $plain_text ) {
if ( $date ) echo "Preferred delivery date: $date\n";
if ( $msg ) echo "Gift message: $msg\n";
return;
}
echo '<h2>Delivery</h2><table cellspacing="0" cellpadding="6" style="width:100%;border:1px solid #eee">';
if ( $date ) echo '<tr><th style="text-align:left">Preferred date</th><td>' . esc_html( $date ) . '</td></tr>';
if ( $msg ) echo '<tr><th style="text-align:left">Gift message</th><td>' . esc_html( $msg ) . '</td></tr>';
echo '</table>';
}, 10, 3 );Test with a real order to each email type: new order (admin), processing (customer), completed.
Packing slips and invoices
PDF invoice plugins expose their own hooks. For WooCommerce PDF Invoices & Packing Slips: wpo_wcpdf_after_order_details, with $document_type to target packing slips only. Print the gift message on the slip so the picker includes it; print the delivery date so dispatch sees it.
Integrations
Anything reading orders via the REST API sees additional fields in the order's meta_data. Fulfilment and accounting connectors usually map custom meta by key; provide them the stored key (visible in the order's meta on the admin screen, or via wc_get_order( $id )->get_meta_data()). For webhooks you write yourself, read the accessor and include the values explicitly in the payload.
The "thank you" page and My Account
Customers should see their choices after ordering:
add_action( 'woocommerce_order_details_after_order_table', function ( $order ) {
$date = $order->get_additional_field_value( 'acme/delivery-date' );
if ( $date ) echo '<p><strong>Preferred delivery date:</strong> ' . esc_html( $date ) . '</p>';
} );The checklist
- Registered with a namespaced ID, in the right location.
- Server-side validation and sanitisation.
- Visible on the admin order screen, and in a list column if staff filter by it.
- Included in admin and customer emails, HTML and plain text.
- On the packing slip if fulfilment needs it.
- Mapped for any integration that reads orders.
- Shown on the thank-you page and in My Account.
- Tested with a real order on staging, end to end.
Where this sits
Following the value through to the packing slip is the difference between a field and a feature, and it is how custom fields are scoped in our WooCommerce work: not "add a field to the checkout" but "capture X at checkout and get it to everyone who needs it".