Tired of handling customer return requests through email? A dedicated return system can streamline the process, improve customer service, and keep all your requests organized. In this tutorial, I’ll show you how to build a complete, lightweight return system for your WooCommerce store using a Custom Post Type (CPT), the Bricks Builder form element, and just a few lines of PHP code.
If you’re looking for another way to streamline your WooCommerce workflow, be sure to check out my new plugin, Woo Quick Stock Adjust. It makes managing product stock a breeze, directly from your WooCommerce product list.

This method avoids bulky, expensive plugins and gives you full control. We’ll use the Advance Custom Fields (ACF) plugin to create the necessary fields for your return requests.
Features
This custom solution gives you a powerful, yet simple, returns management system:
- Dedicated Request Management: All return requests are saved as a custom post type called “Returns,” creating a central hub for all requests instead of getting lost in your inbox.
- User-Friendly Form: Customers submit their requests through a simple, custom-designed form built with Bricks Builder.
- Instant Request Status: The system automatically saves new requests with a default status of “In Progress,” keeping your team instantly informed.
- Intuitive Admin Interface: The system adds key information directly to the “Returns” list view in your WordPress admin dashboard. You’ll see the customer’s name, phone number, order ID, and the product they want to return at a glance.
- Visual Status Badges: Requests are visually color-coded with badges—for example, yellow for “In Progress” and blue for “Completed”—making it easy to see the status of each return.
- Quick Actions: A “Quick Complete” link appears on each return in the list view, allowing you to change a request’s status to “Completed” with a single click.
- Admin Menu Notifications: A red notification badge appears next to the “Returns” menu in the admin sidebar, showing you the number of open “In Progress” requests that need your attention.
Step 1: Create the ‘Returns’ Custom Post Type and Fields
Before adding any code, you must create the foundation for your system: the Custom Post Type (CPT) and its fields. This is where all the information from the return requests will be stored.
- Create the Custom Post Type: Using ACF, create a new CPT with the following details:
Slug: return (this is critical, it must match the code)
Plural Name: Returns
Singular Name: Return - Create the Custom Fields: Next, create a field group and link it to the ‘Returns’ CPT. Make sure the Attribute Name of each field matches exactly with the name referenced in the PHP code, as shown below:
Field Label | Attribute Name (for the form) |
---|---|
Order No. | return_order_id |
Order Email | return_order_email |
Full Name | return_name |
Phone | return_phone |
Reason for Return | return_reason |
Product for Return | return_product |
Product Details | return_product_info |
Product for Exchange | return_product_change |
Exchange Product SKU | return_product_change_sku |
Bank | return_bank |
IBAN | return_iban |
Step 2: Building the Return Form with Bricks Builder
Now that your custom post type and fields are ready, it’s time to build the form that customers will use to submit their requests.

- Create a New Page: Create a new page in WordPress, title it “Return Request” (or similar), and open it with Bricks Builder.
- Add the Form Element: Drag the Form element onto the canvas.
- Configure Form Actions: In the form settings, go to the Form Actions tab and select Custom Action. This is the critical step that links the form to the first PHP snippet you will add.
- Create the Form Fields: Delete the default fields and create new ones, making sure the Attribute Name of each field matches exactly with the names you defined in Step 1 and the PHP code.
Here’s a guide for the form fields and their corresponding Attribute Name you must enter:
Field Type | Field Label | Attribute Name | Description |
---|---|---|---|
Text | Order No. | return_order_id | For the customer’s order ID. |
Order Email | return_order_email | For the order’s email address. | |
Text | Full Name | return_name | For the customer’s name. |
Tel | Phone | return_phone | For the customer’s phone number. |
Select | Reason for Return | return_reason | A dropdown for the return reason. |
Text | Product for Return | return_product | The name or SKU of the product. |
Textarea | Product Details | return_product_info | Any additional info about the product. |
Text | Product for Exchange | return_product_change | The name or SKU of the exchange product. |
Text | Exchange Product SKU | return_product_change_sku | The SKU of the exchange product. |
Text | Bank | return_bank | The name of the customer’s bank. |
Text | IBAN | return_iban | The customer’s IBAN for the refund. |
Step 3: Adding the PHP Code Snippets
You’ll need to add these three PHP snippets to your website. The best practice is to add them to your child theme’s functions.php file or in WP Code or Code Snippets.



1. Bricks Form Action
This snippet tells your Bricks form what to do when a user submits it. It creates a new “Return” custom post and saves the form data as custom fields.
Important: You must change the form ID and ensure your form field names match the names in the code.
/**
* Custom action for Bricks form submission.
* This script creates a new 'return' post type and saves the form data.
*/
add_action( 'bricks/form/custom_action', function( $form ) {
// Get all fields from the form submission
$fields = $form->get_fields();
// --- Check for the correct form ID ---
$form_id = $fields['formId'] ?? '';
// CHANGE 'mlniuq' TO YOUR BRICKS FORM ID
if ( $form_id !== 'mlniuq' ) {
return;
}
// --- Sanitize and retrieve field values ---
// The names in $fields['name'] MUST match the names of your form fields
$order_id = sanitize_text_field( $fields['return_order_id'] ?? '' );
$order_email = sanitize_email( $fields['return_order_email'] ?? '' );
$name = sanitize_text_field( $fields['return_name'] ?? '' );
$phone = sanitize_text_field( $fields['return_phone'] ?? '' );
$reason = sanitize_text_field( $fields['return_reason'] ?? '' );
$product = sanitize_text_field( $fields['return_product'] ?? '' );
$product_info = sanitize_textarea_field( $fields['return_product_info'] ?? '' );
$product_change = sanitize_text_field( $fields['return_product_change'] ?? '' );
$product_change_sku = sanitize_text_field( $fields['return_product_change_sku'] ?? '' );
$bank = sanitize_text_field( $fields['return_bank'] ?? '' );
$iban = sanitize_text_field( $fields['return_iban'] ?? '' );
// --- Create the new post with a temporary title ---
$post_id = wp_insert_post([
'post_type' => 'return', // This must match your Custom Post Type slug
'post_title' => 'Temporary Return Request',
'post_status' => 'publish',
]);
// --- If the post was created, save the data ---
if ( $post_id && !is_wp_error( $post_id ) ) {
// --- GENERATE A UNIQUE TITLE ---
// Combine the current date with the newly created post's ID
$final_title = wp_date('dmY') . '-' . $post_id;
// --- UPDATE THE POST ---
// Update the post with the final title and the custom 'in_progress' status
wp_update_post([
'ID' => $post_id,
'post_title' => $final_title,
'post_status' => 'in_progress',
]);
// Save all the custom fields
update_post_meta( $post_id, 'return_order_id', $order_id );
update_post_meta( $post_id, 'return_order_email', $order_email );
update_post_meta( $post_id, 'return_name', $name );
update_post_meta( $post_id, 'return_phone', $phone );
update_post_meta( $post_id, 'return_reason', $reason );
update_post_meta( $post_id, 'return_product', $product );
update_post_meta( $post_id, 'return_product_info', $product_info );
update_post_meta( $post_id, 'return_product_change', $product_change );
update_post_meta( $post_id, 'return_product_change_sku', $product_change_sku );
update_post_meta( $post_id, 'return_bank', $bank );
update_post_meta( $post_id, 'return_iban', $iban );
// Send a success message back to the form
$form->set_result([
'action' => 'custom_action',
'type' => 'success',
'message' => '✅ Your return request has been submitted successfully!',
]);
} else {
// Send an error message if something went wrong
$form->set_result([
'action' => 'custom_action',
'type' => 'danger',
'message' => '❌ An error occurred while saving the request. Please try again.',
]);
}
}, 10, 1 );
2. Admin Columns & Badge
This snippet customizes the admin list view for your “Returns” CPT. It adds new columns, displays the custom field data within them, and shows a status badge. It also adds a notification badge to the “Returns” menu item.
/**
* Adds and customizes admin columns for the 'return' CPT.
*/
// 1. Add the new columns
add_filter( 'manage_return_posts_columns', 'set_custom_edit_return_columns' );
function set_custom_edit_return_columns($columns) {
$new_columns = [
'cb' => $columns['cb'],
'title' => __( 'Return No.', 'textdomain' ),
'status' => __( 'Status', 'textdomain' ),
'return_name' => __( 'Full Name', 'textdomain' ),
'return_phone' => __( 'Phone', 'textdomain' ),
'return_order_id' => __( 'Order No.', 'textdomain' ),
'return_product' => __( 'Product for Return', 'textdomain' ),
'date' => $columns['date']
];
return $new_columns;
}
// 2. Display the data in the new columns
add_action( 'manage_return_posts_custom_column' , 'custom_return_column_content', 10, 2 );
function custom_return_column_content( $column, $post_id ) {
switch ( $column ) {
case 'return_name' :
echo esc_html( get_post_meta( $post_id , 'return_name' , true ) );
break;
case 'return_phone' :
echo esc_html( get_post_meta( $post_id , 'return_phone' , true ) );
break;
case 'return_order_id' :
echo '<strong>' . esc_html( get_post_meta( $post_id , 'return_order_id' , true ) ) . '</strong>';
break;
case 'return_product' :
echo esc_html( get_post_meta( $post_id , 'return_product' , true ) );
break;
case 'status' :
$status = get_post_status( $post_id );
$status_label = '';
$background_color = '#e0e0e0'; // Default Grey
if ( $status === 'in_progress' ) {
$status_label = 'In Progress';
$background_color = '#ffc107'; // Yellow
} elseif ( $status === 'completed' ) {
$status_label = 'Completed';
$background_color = '#0d6efd'; // Blue
} else {
$status_obj = get_post_status_object( $status );
$status_label = $status_obj ? $status_obj->label : $status;
}
printf(
'<span style="background-color: %s; color: %s; padding: 4px 8px; border-radius: 4px; font-weight: bold; font-size: 12px; white-space: nowrap;">%s</span>',
esc_attr( $background_color ),
$background_color === '#ffc107' ? '#000' : '#fff',
esc_html( $status_label )
);
break;
}
}
/**
* Adds a notification badge to the 'Returns' submenu item.
*/
add_action( 'admin_menu', 'add_in_progress_returns_bubble_to_submenu', 99 );
function add_in_progress_returns_bubble_to_submenu() {
global $submenu;
// Define the CPT slug (always lowercase)
$cpt_slug = 'return';
// Define the parent menu slug (WooCommerce)
$parent_slug = 'woocommerce';
// If no WooCommerce submenu exists, stop.
if ( ! isset( $submenu[ $parent_slug ] ) ) {
return;
}
// 1. Get the count of posts with 'in_progress' status
$count_posts = wp_count_posts( $cpt_slug );
$in_progress_count = $count_posts->in_progress ?? 0;
// 2. If the count is greater than zero, proceed
if ( $in_progress_count > 0 ) {
// 3. Search for the CPT's submenu item within the parent menu
foreach ( $submenu[ $parent_slug ] as $key => $value ) {
// The URL we are looking for is for our CPT
$menu_url = 'edit.php?post_type=' . $cpt_slug;
// If the correct submenu item is found
if ( isset($value[2]) && $value[2] === $menu_url ) {
// 4. Add the notification badge
$submenu[ $parent_slug ][ $key ][0] .= ' <span class="awaiting-mod"><span class="pending-count">' . esc_html( $in_progress_count ) . '</span></span>';
// Stop the loop once found
break;
}
}
}
}
3. Custom Statuses & Quick Actions
This snippet creates the two custom statuses for your “Returns” CPT (“In Progress” and “Completed”) and adds a quick action link to the list view, allowing you to easily change a request’s status.
/**
* Creates custom post statuses for the 'return' CPT.
*/
// 1. Register the new statuses with WordPress
function register_custom_return_statuses() {
// Status: In Progress
register_post_status('in_progress', [
'label' => _x( 'In Progress', 'post' ),
'public' => true,
'exclude_from_search' => false,
'show_in_admin_all_list' => true,
'show_in_admin_status_list' => true,
'label_count' => _n_noop( 'In Progress <span class="count">(%s)</span>', 'In Progress <span class="count">(%s)</span>' ),
]);
// Status: Completed
register_post_status('completed', [
'label' => _x( 'Completed', 'post' ),
'public' => true,
'exclude_from_search' => false,
'show_in_admin_all_list' => true,
'show_in_admin_status_list' => true,
'label_count' => _n_noop( 'Completed <span class="count">(%s)</span>', 'Completed <span class="count">(%s)</span>' ),
]);
}
add_action( 'init', 'register_custom_return_statuses' );
// 2. Add the new statuses to the post editor dropdown
function add_custom_statuses_to_post_editor() {
global $post;
if ( ! $post || $post->post_type !== 'return' ) {
return;
}
// This script ensures the correct value is selected in the dropdown
$script = "
<script>
jQuery(document).ready(function($){
$('select[name=\"post_status\"]').append('<option value=\"in_progress\">In Progress</option>');
$('select[name=\"post_status\"]').append('<option value=\"completed\">Completed</option>');
// Set the current post's status as selected
var currentStatus = '{$post->post_status}';
if (currentStatus === 'in_progress' || currentStatus === 'completed') {
$('select[name=\"post_status\"]').val(currentStatus);
}
});
</script>
";
echo $script;
}
add_action( 'admin_footer-post.php', 'add_custom_statuses_to_post_editor' );
add_action( 'admin_footer-post-new.php', 'add_custom_statuses_to_post_editor' );
/**
* Adds a "Complete" quick action to the returns list.
*/
// Part A: Add the "Complete" link below each request
add_filter( 'post_row_actions', 'add_quick_complete_action_link', 10, 2 );
function add_quick_complete_action_link( $actions, $post ) {
// Check if we are on the 'return' CPT and if the request is NOT already completed
if ( $post->post_type === 'return' && $post->post_status !== 'completed' ) {
// Create the URL for the action with a security check (nonce)
$url = add_query_arg([
'action' => 'mark_return_as_completed',
'post_id' => $post->ID,
'_wpnonce' => wp_create_nonce('complete_return_nonce_' . $post->ID)
]);
// Add the link to the list of actions
$actions['mark_completed'] = sprintf( '<a href="%s" style="color:#0d6efd;">Completed</a>', esc_url( $url ) );
}
return $actions;
}
// Part B: The code that executes when the link is clicked
add_action( 'admin_action_mark_return_as_completed', 'handle_quick_complete_action' );
function handle_quick_complete_action() {
// Get the post ID from the URL
$post_id = isset($_GET['post_id']) ? intval($_GET['post_id']) : 0;
// Security check (nonce) to ensure the request is valid
if ( ! $post_id || ! isset($_GET['_wpnonce']) || ! wp_verify_nonce($_GET['_wpnonce'], 'complete_return_nonce_' . $post_id) ) {
wp_die('The request is not valid.');
}
// Change the post status to "Completed"
wp_update_post([
'ID' => $post_id,
'post_status' => 'completed',
]);
// Redirect back to the previous page (the list view)
wp_redirect( wp_get_referer() );
exit();
}
Keep in mind that this system is fully customizable. You can add or remove form fields and custom fields as you see fit. Just remember that for the system to work correctly, the Attribute Name you use in your Bricks form must exactly match the name of the custom field in ACF and the field name in the PHP code.