header blog

WooCommerce Checkout Hooks: Where to Add Custom Fields and Logic

Modifying an existing checkout field is a job for the woocommerce_checkout_fields filter. Adding a genuinely new field, one that doesn’t exist in WooCommerce’s default set, saving it, and validating it, is a different job that needs three separate hooks working together.

The Code

Here’s a complete example: a “Gift message” field added after order notes, saved to the order, and validated so it can’t exceed 200 characters.

// 1. Display the field
add_action( 'woocommerce_after_order_notes', 'fixelar_add_gift_message_field' );

function fixelar_add_gift_message_field( $checkout ) {
    echo '<div id="fixelar_gift_message"><h3>' . esc_html__( 'Gift Message', 'woocommerce' ) . '</h3>';

    woocommerce_form_field( 'fixelar_gift_message', array(
        'type'        => 'textarea',
        'class'       => array( 'form-row-wide' ),
        'label'       => __( 'Add a gift message (optional)', 'woocommerce' ),
        'required'    => false,
    ), $checkout->get_value( 'fixelar_gift_message' ) );

    echo '</div>';
}

// 2. Validate the field
add_action( 'woocommerce_after_checkout_validation', 'fixelar_validate_gift_message', 10, 2 );

function fixelar_validate_gift_message( $fields, $errors ) {
    if ( ! empty( $fields['fixelar_gift_message'] ) && strlen( $fields['fixelar_gift_message'] ) > 200 ) {
        $errors->add( 'validation', __( 'Gift message must be under 200 characters.', 'woocommerce' ) );
    }
}

// 3. Save the field to the order
add_action( 'woocommerce_checkout_update_order_meta', 'fixelar_save_gift_message' );

function fixelar_save_gift_message( $order_id ) {
    if ( ! empty( $_POST['fixelar_gift_message'] ) ) {
        update_post_meta( $order_id, '_fixelar_gift_message', sanitize_textarea_field( $_POST['fixelar_gift_message'] ) );
    }
}

Where to Put This Code

All three functions go in your child theme’s functions.php, or in a snippets plugin like Code Snippets. They need to stay together, since removing one breaks the chain: without step 2, invalid data gets saved; without step 3, the field displays but the value disappears after checkout.

This is written for the classic (shortcode-based) checkout. If your store runs the block-based Checkout Block, adding custom fields uses a different system entirely, the Checkout Blocks API with woocommerce_register_additional_checkout_field, not these action hooks. Confirm which checkout your store is actually running before implementing this.

How Each Hook Works

woocommerce_after_order_notes is a placement hook, one of several spots on the checkout form where you can inject a field. Others include woocommerce_before_checkout_billing_form, woocommerce_after_checkout_billing_form, and woocommerce_before_checkout_shipping_form. Which one you use just determines where on the page the field visually appears.

woocommerce_form_field() is a WooCommerce helper function that renders a properly formatted form field, matching the styling of the rest of checkout, so you don’t have to hand-write the HTML.

woocommerce_after_checkout_validation runs after WooCommerce’s own field validation and gives you two parameters: $fields (the posted checkout data) and $errors (a WP_Error object). Calling $errors->add() inside this hook stops the order from being placed and shows the customer your message.

woocommerce_checkout_process is an older alternative for validation that some tutorials still use. Both work, but woocommerce_after_checkout_validation is newer and gives cleaner access to the submitted field data, so it’s the better default for new code.

woocommerce_checkout_update_order_meta fires after the order is created and gives you $order_id. This is where you actually persist the custom field’s value using update_post_meta(), since nothing before this point writes anything to the database.

Real Use Cases

A gift message or delivery instructions field. The exact pattern above: not a modification of an existing field, but a genuinely new one tied to custom order logic.

A required terms checkbox for regulated products. Combining a placement hook with woocommerce_after_checkout_validation lets you block checkout entirely if a specific box isn’t checked, with a clear error message explaining why.

A tax ID or business registration field for B2B stores. Placed with woocommerce_after_checkout_billing_form, validated for format, and saved to order meta so it’s visible on the admin order screen.

Conditional shipping logic based on cart contents. woocommerce_after_checkout_validation isn’t limited to field format checks. It can inspect WC()->cart and block checkout if, for example, two incompatible products are both present.

Common Issues

The field shows up but the value disappears after checkout. This means step 3 (woocommerce_checkout_update_order_meta) is missing. Displaying a field and saving it are two separate jobs.

Validation isn’t blocking checkout even when the condition is met. Confirm you’re using $errors->add() and not just returning a value. woocommerce_after_checkout_validation is an action hook, not a filter, so nothing is expected back from your function; the error object is how you communicate the failure.

The custom field works on the classic checkout page but not after switching to the Checkout Block. As noted above, this entire approach is built for the shortcode checkout. Block-based checkout customization requires the newer Checkout Blocks API.

Field data is getting saved without sanitization. Always run submitted data through an appropriate sanitization function (sanitize_text_field(), sanitize_textarea_field(), etc.) before saving it with update_post_meta(). Skipping this is a real security gap, not just a style preference.

If you need custom checkout fields and logic built and tested against both classic and block-based checkout, or migrated from one to the other, that’s exactly the kind of work Fixelar handles as part of our WooCommerce custom development services. We test the full flow: display, validation, and order data, before it touches a live store.