Persisting Custom Field Data in WooCommerce Block Checkouts

Rendering a custom field in the WooCommerce Block Checkout is the easy half of the job. Getting that value onto the final order object is where most implementations quietly fail. This guide covers the persistence half: how a value typed into a React component survives the trip to a High-Performance Order Storage (HPOS) order record.

Why Persisting Checkout Data Matters

A checkout field that renders but never saves is worse than no field at all. The customer believes the information was captured, and the merchant never sees it. This data persistence failure can also create friction in the checkout conversion funnel. When delivery preferences, tax identifiers, or compliance details are lost during order processing, customers may face additional verification steps, delayed fulfilment, or follow-up requests, reducing the reliability of the overall checkout experience. The commercial stakes are concrete:

  • B2B fields such as VAT or tax ID numbers. Without persistence, invoices and reverse-charge VAT handling break, and finance reconciles by email.
  • Delivery date selectors. Fulfilment and courier scheduling depend on the chosen slot being on the order, not only in the browser.
  • Legal compliance checkboxes. Terms acceptance, age verification, and signature-on-delivery consent must be retained on the order as an auditable record.

The asymmetry is the real problem. Using @wordpress/blocks and the SlotFill system, you can render a custom component inside the checkout in an afternoon. Saving its data requires understanding a different transport layer entirely.

The Architecture Behind the Failure

General-purpose AI assistants regularly fail at this task. They produce a correct React component, then pair it with legacy PHP such as the woocommerce_checkout_create_order hook and $_POST field handling. That code belongs to the classic shortcode checkout, where an HTML form is submitted to the server. The Block Checkout does not submit a form. It talks to the server exclusively through asynchronous REST requests, so the classic form-submission assumptions do not hold and your custom data is silently lost when the order is created.

The actual flow looks like this:

  1. The customer interacts with your component, and its state lives in the browser, typically in a @wordpress/data store. This introduces a React state management challenge: custom checkout field values may exist in local component state through React hooks such as useState(), or in WooCommerce's @wordpress/data store, but neither automatically guarantees that the information is persisted to the backend order.
  2. The frontend communicates with the Store API over REST. Placing the order dispatches a POST request to /wc/store/v1/checkout. These RESTful endpoints also include /wc/store/v1/cart/extensions, which processes extension-specific updates sent through extensionCartUpdate before the final checkout request. Understanding the difference between cart updates and order submission is essential for reliable custom field persistence.
  3. The server builds the order from that request. Anything your code did not explicitly register for the Store API never reaches the order.

So the persistence problem becomes: how do you move a value from a React store to PHP, hold it safely, and write it to the order at the right moment?

The answer is a two-step pattern. The first step is the core of woocommerce_store_api_register_update_callback checkout block data persistence: register a namespaced callback that receives your data and parks it in the WooCommerce session. The second step commits that session value to the order once it exists.

The Store API also offers a direct route through endpoint data registration (woocommerce_store_api_register_endpoint_data) combined with the checkout extensions payload. The Store API schema defines the structure and data types exposed by WooCommerce's REST endpoints. Through ExtendSchema and woocommerce_store_api_register_endpoint_data, developers can register namespaced schema and data callbacks for supported endpoints, keeping custom extension data separate from WooCommerce's core fields. That route is worth evaluating for fields you want submitted atomically with the order request. The session pattern below is the more forgiving choice when a value changes interactively before the customer ever presses Place Order, which covers most compliance and delivery fields.

Flowchart illustrating how the WooCommerce Store API and woocommerce_store_api_register_update_callback safely route React frontend payload data into temporary session storage before committing to the HPOS database.


Step 1: Extract the Payload to the Session

First, the frontend must push the value to the server whenever it changes. The @woocommerce/blocks-checkout package exports extensionCartUpdate for this. It sends your data to the Store API under a namespace you choose:

import { extensionCartUpdate } from '@woocommerce/blocks-checkout';

function onSignatureToggle( checked ) {
    extensionCartUpdate( {
        namespace: 'compliance_data',
        data: { require_signature: checked },
    } );
}

On the PHP side, register a callback for that same namespace. Registration must happen on woocommerce_blocks_loaded, because the Store API functions are not guaranteed to exist earlier in the request lifecycle:

add_action( 'woocommerce_blocks_loaded', 'acme_register_checkout_persistence' );

function acme_register_checkout_persistence() {
    if ( ! function_exists( 'woocommerce_store_api_register_update_callback' ) ) {
        return;
    }

    woocommerce_store_api_register_update_callback(
        array(
            'namespace' => 'compliance_data',
            'callback'  => 'acme_extract_block_data_to_session',
        )
    );
}

/**
 * Receives the data array sent from the React frontend.
 *
 * @param array $data Payload under the 'compliance_data' namespace.
 */
function acme_extract_block_data_to_session( $data ) {
    if ( ! WC()->session || ! is_array( $data ) ) {
        return;
    }

    if ( isset( $data['require_signature'] ) ) {
        $value = rest_sanitize_boolean( $data['require_signature'] ) ? 'yes' : 'no';
        WC()->session->set( 'acme_require_signature', $value );
    }
}

Three details matter here:

  • Sanitize on entry. The payload is client-controlled input. Normalize it before it touches the session, as the example does with rest_sanitize_boolean().
  • Handle the unchecked state explicitly. Storing 'no' rather than skipping the write means a customer who checks and then unchecks the box does not leave a stale 'yes' in the session.
  • Namespace carefully. Use a vendor-prefixed namespace so your callback never collides with another extension.

Step 2: Commit the Session Data to the HPOS Order

The session is a staging area, not storage. The value becomes durable only when it is written to the order. The woocommerce_store_api_checkout_order_processed hook fires once the Store API has created the order, and it passes you the WC_Order object.

Because HPOS stores orders in dedicated tables rather than in wp_posts, never use update_post_meta() here. These dedicated database structures, known as WooCommerce custom order tables, include wp_wc_orders for core order records and wp_wc_orders_meta for custom metadata, assuming the default WordPress table prefix. Using WooCommerce's HPOS CRUD API ensures that custom checkout data is saved through the active order data store without directly querying or modifying these tables. Go through the WC_Order CRUD methods so the write lands in the correct tables regardless of storage configuration:

add_action( 'woocommerce_store_api_checkout_order_processed', 'acme_commit_session_to_order' );

/**
 * @param WC_Order $order The order created by the Store API checkout.
 */
function acme_commit_session_to_order( $order ) {
    if ( ! WC()->session ) {
        return;
    }

    $value = WC()->session->get( 'acme_require_signature' );

    if ( null === $value ) {
        return;
    }

    $order->update_meta_data( '_acme_require_signature', $value );
    $order->save();

    // Purge the staging value so it cannot leak into the next order.
    WC()->session->__unset( 'acme_require_signature' );
}

The underscore prefix on the meta key keeps it out of the default Custom Fields UI. To surface it for merchants, read it back with $order->get_meta( '_acme_require_signature' ) inside an admin display hook such as woocommerce_admin_order_data_after_billing_address.

Purging the session variable is not optional. WooCommerce sessions persist across page loads, and a returning customer could otherwise inherit the previous order's value.

Production Hardening

  • Enforce required fields server-side. A legal checkbox that is only validated in React can be bypassed by a direct API call. Validate during checkout processing, for example by hooking woocommerce_store_api_checkout_update_order_from_request and throwing a RouteException when a mandatory value is missing.
  • Test with HPOS enabled and with compatibility mode off. Any stray update_post_meta() call will surface immediately.
  • Log the session lifecycle during development. If WC()->session->get() returns null at order time, confirm that the frontend call fired and that the namespace strings match exactly on both sides.
  • Avoid storing sensitive data in the session unencrypted. For regulated identifiers, minimize retention and write to the order as early as possible.

Summary

The Block Checkout replaces form submission with REST transport, and persistence code has to follow. Push the value to the server through extensionCartUpdate, intercept it with a registered update callback, stage it in the WooCommerce session, then commit it to the HPOS order through the WC_Order object on woocommerce_store_api_checkout_order_processed, purging the session as you go. Implemented this way, B2B identifiers, delivery slots, and compliance consents survive from the browser to the order record intact.

0 Comments