In WooCommerce, giving customers a simple way to request a refund can save you time and improve their experience.
While many store owners handle refunds manually via emails or support tickets, adding a dedicated “Refund Request” button directly in the My Account page makes the process seamless for both customers and store admins. This approach keeps all refund requests tied to the original order, ensures proper logging via customer notes, and automatically notifies the admin when a request is submitted.
In this tutorial, we’ll create a lightweight solution using only PHP, JS and core HTML—no additional plugins or frameworks required.
You’ll learn how to display a new action on the My Account orders table, open a native HTML modal for customers to enter their reason, and handle the request by adding a customer note and sending an admin email. By the end, your WooCommerce store will have a professional, fully functional refund request workflow ready to go.

PHP Snippet: Enable WooCommerce Refund Requests in My Account
This WooCommerce snippet adds a customer-facing refund request system directly within the My Account area.
Action Button Filter: The first section adds a “Ask for a Refund” button to order action lists (orders page, single order view). It only appears for completed or processing orders within 60 days. If a refund has already been requested, the button changes to “Pending Refund” and becomes non-clickable.
CSS Styling: A small inline style greys out the pending refund button to visually indicate it’s disabled.
Modal Dialog & JavaScript: This outputs an HTML dialog element with a textarea for the refund reason. JavaScript handlers open the modal when clicking the refund button (extracting the order ID from the URL hash), handle cancel/submit actions, and make an AJAX call to WordPress. After successful submission, the page reloads to show the updated button state.
AJAX Handler: Processes the refund request by verifying the nonce and user authorization, then stores the request timestamp and reason as order meta. It adds a customer note to the order and sends an email notification to the site admin with order details and a direct link.
/**
* @snippet Refund Button @ WooCommerce My Account Order Actions
* @tutorial https://businessbloomer.com/woocommerce-customization
* @author Rodolfo Melogli, Business Bloomer
* @compatible WooCommerce 10
* @community Join https://businessbloomer.com/club/
*/
/**
* Add "Request Refund" action on My Account > Orders
*/
add_filter( 'woocommerce_my_account_my_orders_actions', function( $actions, $order ) {
// Don't show on thank you page
if ( is_order_received_page() ) {
return $actions;
}
// Show only for completed or processing orders (customize as needed)
if ( in_array( $order->get_status(), ['completed', 'processing'] ) ) {
// Check if refund already requested
$refund_requested = $order->get_meta( '_bb_refund_requested' );
if ( $refund_requested ) {
// Show greyed out button
$actions['bb-refund-pending'] = [
'url' => '#',
'name' => '✓ Pending Refund',
];
return $actions;
}
// Check if order is within 60 days
$order_date = $order->get_date_created();
if ( $order_date ) {
$days_since_order = ( time() - $order_date->getTimestamp() ) / DAY_IN_SECONDS;
if ( $days_since_order <= 60 ) {
$actions['bb_request_refund'] = [
'url' => '#refund-' . $order->get_id(),
'name' => 'Ask for a Refund',
'aria-label' => 'Ask for a Refund',
];
}
}
}
return $actions;
}, 10, 2 );
/**
* Add CSS to grey out pending refund button
*/
add_action( 'wp_head', function() {
?>
<style>
a.bb-refund-pending {
opacity: 0.5;
cursor: not-allowed !important;
pointer-events: none;
}
</style>
<?php
});
/**
* Output the HTML dialog modal + small JS handler
*/
add_action( 'woocommerce_view_order', 'bb_output_refund_modal' );
add_action( 'woocommerce_after_account_orders', 'bb_output_refund_modal' );
function bb_output_refund_modal() {
?>
<dialog id="bb-refund-dialog">
<form method="dialog" id="bb-refund-form">
<h3>Request a Refund</h3>
<input type="hidden" name="order_id" id="bb-order-id" value="">
<label for="bb-reason">Reason for refund:</label>
<textarea id="bb-reason" name="reason" required style="width:100%;height:120px;"></textarea>
<div style="margin-top:1rem;">
<button id="bb-refund-cancel">Cancel</button>
<button id="bb-refund-submit">Submit</button>
</div>
</form>
</dialog>
<script>
// Open dialog when clicking the custom action
document.querySelectorAll('a.bb_request_refund').forEach(function(btn){
btn.addEventListener('click', function(e){
e.preventDefault();
const orderID = this.getAttribute('href').replace('#refund-', '');
document.getElementById('bb-order-id').value = orderID;
document.getElementById('bb-reason').value = '';
document.getElementById('bb-refund-dialog').showModal();
});
});
// Cancel button
document.getElementById('bb-refund-cancel').addEventListener('click', function(e){
e.preventDefault();
document.getElementById('bb-refund-dialog').close();
});
// Submit button
document.getElementById('bb-refund-submit').addEventListener('click', function(e){
e.preventDefault();
const orderID = document.getElementById('bb-order-id').value;
const reason = document.getElementById('bb-reason').value;
if( !orderID || reason.trim() === '' ) return;
fetch('<?php echo admin_url( "admin-ajax.php" ); ?>', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
action: 'bb_submit_refund_request',
order_id: orderID,
reason: reason,
nonce: '<?php echo wp_create_nonce( "bb_refund_nonce" ); ?>'
})
})
.then(r => r.json())
.then(data => {
document.getElementById('bb-refund-dialog').close();
if(data.success) {
alert('Your refund request has been submitted.');
location.reload();
} else {
alert('Error: ' + (data.message || 'Something went wrong'));
}
})
.catch(() => {
alert('Network error. Please try again.');
});
});
</script>
<?php
}
/**
* AJAX handler: add customer note + send admin email
*/
add_action( 'wp_ajax_bb_submit_refund_request', 'bb_submit_refund_request' );
add_action( 'wp_ajax_nopriv_bb_submit_refund_request', 'bb_submit_refund_request' );
function bb_submit_refund_request() {
// Verify nonce
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'bb_refund_nonce' ) ) {
wp_send_json_error( ['message' => 'Security check failed'] );
}
if ( empty( $_POST['order_id'] ) || empty( $_POST['reason'] ) ) {
wp_send_json_error( ['message' => 'Missing required fields'] );
}
$order_id = absint( $_POST['order_id'] );
$reason = sanitize_textarea_field( $_POST['reason'] );
$order = wc_get_order( $order_id );
if ( ! $order ) {
wp_send_json_error( ['message' => 'Invalid order'] );
}
// Mark as refund requested
$order->update_meta_data( '_bb_refund_requested', current_time( 'mysql' ) );
$order->update_meta_data( '_bb_refund_reason', $reason );
$order->save();
// 1) Add customer note
$order->add_order_note( 'Customer requested a refund: ' . $reason, true );
// 2) Notify admin
wp_mail(
get_option( 'admin_email' ),
'Refund Request for Order #' . $order_id,
"A customer has requested a refund.\n\nOrder: #{$order_id}\nReason:\n{$reason}\n\nView order: " . admin_url( 'post.php?post=' . $order_id . '&action=edit' )
);
wp_send_json_success( ['message' => 'Refund request submitted'] );
}









Great idea – better than mine where I used a CF7 form on a specified page but didn’t prevent duplicate requests (https://www.damiencarbery.com/2016/11/refund-requests-for-woocommerce-customers-using-contact-form-7/)
Aside: I suggest removing the nopriv ajax handler as the user will have to be logged in to see the button and no non-logged in users should be able to submit a refund request. I know you have nonce protection; this would just be an extra layer of security.
Thank you and good catch, will do!