CheckoutWC’s abandoned cart recovery (ACR) tracks carts when customers enter their information at checkout, then sends follow-up emails if they leave without completing the purchase. This guide explains how to customize that process for your specific business needs.
How abandoned cart recovery works in CheckoutWC
Before diving into customization, it helps to understand the ACR lifecycle:
- Cart tracking — When a customer enters their email at checkout, ACR saves their cart contents, contact info, and any custom metadata to the database.
- Abandonment detection — A cron job runs every 5 minutes checking for carts that haven’t converted. If a cart sits idle past your configured threshold, it’s marked “abandoned.”
- Email sequence — Based on your email schedule, recovery emails are sent. Each email can include the cart contents, a checkout link, and optional coupon.
- Recovery or loss — If the customer clicks the link and completes checkout, the cart is marked “recovered.” If they don’t respond within your window, it’s marked “lost.”
Each stage has hooks you can use to customize behavior. The key is knowing which stage you need to modify.
Controlling which carts get tracked
Not every cart should trigger recovery emails. You might want to exclude staff orders, wholesale customers, or low-value carts that aren’t worth the email send.
Why exclude carts?
Common reasons to exclude carts from ACR tracking:
- Internal orders — Staff placing test orders or internal purchases don’t need recovery emails
- Wholesale/B2B customers — They may have different purchasing workflows or dedicated account managers
- Low-value carts — Recovery emails have a cost (time, email sends, potential discounts). A $5 cart may not justify the effort
- Recent purchasers — Someone who bought yesterday and is browsing again probably doesn’t need a “you forgot something” email
- Subscription renewals — Automated renewal carts shouldn’t trigger abandonment sequences
The cfw_acr_exclude_cart filter
This filter runs when ACR is about to save a cart. Return true to skip tracking entirely.
/**
* Exclude carts from ACR tracking.
*
* @param bool $exclude Whether to exclude this cart (default: false).
* @param string $email Customer email address.
* @param array $cart_contents Cart contents array.
* @param float $subtotal Cart subtotal.
* @param string $first_name Customer first name.
* @param string $last_name Customer last name.
* @param array $fields All checkout field values.
* @param array $meta Custom metadata array.
* @return bool
*/
add_filter( 'cfw_acr_exclude_cart', function( $exclude, $email, $cart_contents, $subtotal, $first_name, $last_name, $fields, $meta ) {
// Your logic here
return $exclude;
}, 10, 8 );
Example: Exclude orders under $50
If your average recovery email converts at 5% and includes a 10% discount, you’re giving away $5 on a $100 order to make the sale. On a $30 order, you’re giving away $3 to make a much smaller margin. At some point, the math doesn’t work.
add_filter( 'cfw_acr_exclude_cart', function( $exclude, $email, $cart_contents, $subtotal ) {
// Don't track carts under $50
if ( $subtotal < 50 ) {
return true;
}
return $exclude;
}, 10, 4 );
Example: Exclude wholesale customers
If you use a wholesale plugin that assigns customers to a “wholesale” role, those customers likely have a dedicated sales rep or different buying process. Generic recovery emails may feel impersonal or even annoying.
add_filter( 'cfw_acr_exclude_cart', function( $exclude, $email ) {
// Check if this email belongs to a wholesale user
$user = get_user_by( 'email', $email );
if ( $user && in_array( 'wholesale_customer', $user->roles, true ) ) {
return true;
}
return $exclude;
}, 10, 2 );
Example: Exclude recent purchasers
A customer who completed an order 3 days ago and is now browsing again isn’t “abandoning” in the traditional sense. They’re just shopping. Sending them a recovery email can feel pushy.
add_filter( 'cfw_acr_exclude_cart', function( $exclude, $email ) {
$recent_orders = wc_get_orders( array(
'customer' => $email,
'date_after' => '14 days ago',
'status' => array( 'completed', 'processing' ),
'limit' => 1,
) );
if ( ! empty( $recent_orders ) ) {
return true;
}
return $exclude;
}, 10, 2 );
Example: Exclude staff and internal orders
You don’t want to send recovery emails to your own team when they’re testing checkout or placing internal orders.
add_filter( 'cfw_acr_exclude_cart', function( $exclude, $email ) {
// Exclude your company domain
if ( strpos( $email, '@yourcompany.com' ) !== false ) {
return true;
}
// Or exclude specific email addresses
$excluded_emails = array(
'[email protected]',
'[email protected]',
);
if ( in_array( $email, $excluded_emails, true ) ) {
return true;
}
return $exclude;
}, 10, 2 );
Tracking additional data with abandoned carts
By default, ACR stores cart contents, customer name, email, and subtotal. But you might need more context to personalize recovery or analyze abandonment patterns.
Why track additional data?
- Attribution — Know which marketing channel brought the customer (referrer URL, UTM parameters)
- Segmentation — Tag carts by customer type, product category, or other criteria for targeted follow-up
- Analysis — Understand patterns like “customers from paid ads abandon at higher rates”
- Personalization — Use custom data in email templates or recovery flows
The cfw_acr_cart_meta filter
This filter lets you add custom metadata when a cart is tracked. The data is stored as JSON and available throughout the recovery process.
/**
* Add custom metadata to tracked carts.
*
* @param array $meta Existing cart metadata.
* @return array
*/
add_filter( 'cfw_acr_cart_meta', function( $meta ) {
// Add your custom data
return $meta;
} );
Example: Track marketing attribution
Knowing where abandoned carts come from helps you understand which channels have conversion problems. Maybe your Facebook ads drive traffic but those visitors abandon at higher rates than organic search.
add_filter( 'cfw_acr_cart_meta', function( $meta ) {
// Capture UTM parameters if present
$meta['utm_source'] = isset( $_GET['utm_source'] ) ? sanitize_text_field( $_GET['utm_source'] ) : '';
$meta['utm_medium'] = isset( $_GET['utm_medium'] ) ? sanitize_text_field( $_GET['utm_medium'] ) : '';
$meta['utm_campaign'] = isset( $_GET['utm_campaign'] ) ? sanitize_text_field( $_GET['utm_campaign'] ) : '';
// Track referrer
$meta['referrer'] = isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( $_SERVER['HTTP_REFERER'] ) : '';
return $meta;
} );
Example: Track the landing page
If you store the landing page in a session variable (set on first page load), you can capture it here to understand the customer journey.
add_filter( 'cfw_acr_cart_meta', function( $meta ) {
if ( isset( $_SESSION['landing_page'] ) ) {
$meta['landing_page'] = sanitize_url( $_SESSION['landing_page'] );
}
return $meta;
} );
Customizing email recipients
Sometimes the person who starts a checkout isn’t the right person to receive the recovery email.
Why change the recipient?
- B2B purchasing — An employee starts the order, but the purchasing department handles follow-up
- Shared accounts — Multiple people use the same customer account
- Testing and QA — Route test emails to a specific inbox
The cfw_acr_send_to_email filter
/**
* Change the recipient of recovery emails.
*
* @param string $send_to_email Original recipient email.
* @param object $cart The abandoned cart object.
* @param int $email_id The ACR email template ID.
* @return string
*/
add_filter( 'cfw_acr_send_to_email', function( $send_to_email, $cart, $email_id ) {
// Your logic here
return $send_to_email;
}, 10, 3 );
Example: Route corporate emails to purchasing
If someone from a corporate account starts an order, the purchasing department may be better equipped to follow up.
add_filter( 'cfw_acr_send_to_email', function( $send_to_email, $cart, $email_id ) {
// Check if this is from a known corporate domain
$corporate_domains = array(
'bigclient.com' => '[email protected]',
'enterprise.org' => '[email protected]',
);
foreach ( $corporate_domains as $domain => $redirect ) {
if ( strpos( $send_to_email, '@' . $domain ) !== false ) {
return $redirect;
}
}
return $send_to_email;
}, 10, 3 );
Styling recovery emails
Your recovery emails should match your brand. Generic-looking emails feel like spam; branded emails feel like a helpful reminder from a store the customer recognizes.
The cfw_acr_email_custom_css filter
ACR uses the Emogrifier library to inline CSS before sending, so your styles will be applied directly to elements rather than in a stylesheet (which many email clients ignore).
/**
* Add custom CSS to recovery emails.
*
* @return string CSS rules to apply.
*/
add_filter( 'cfw_acr_email_custom_css', function() {
return '
/* Your CSS here */
';
} );
Example: Match your brand colors
add_filter( 'cfw_acr_email_custom_css', function() {
return '
/* Primary button - match your site CTA color */
.btn-primary a {
background-color: #2563eb !important;
border-color: #2563eb !important;
border-radius: 6px !important;
}
/* Heading color */
h1 {
color: #1e293b;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* Subtle product highlight */
.content-block {
background: #f8fafc;
border-left: 4px solid #2563eb;
padding: 16px;
}
/* Footer styling */
.footer {
color: #64748b;
font-size: 12px;
}
';
} );
Available CSS classes
| Class | What it styles |
|---|---|
.body |
Outermost email wrapper |
.container |
Main content container (max-width: 580px) |
.main |
White content area background |
.content-block |
Padded content sections |
.btn-primary |
Primary CTA button |
.footer |
Footer area with unsubscribe link |
.preheader |
Preview text (hidden in body, shows in inbox) |
The cfw_cart_table_styles filter
If you need to specifically style the cart products table in emails:
add_filter( 'cfw_cart_table_styles', function( $styles ) {
$styles .= '
.acr-cart-table img {
border-radius: 8px;
}
.acr-cart-table td {
padding: 12px 8px;
}
';
return $styles;
} );
Customizing email headers
Email headers control things like the reply-to address, priority, and custom tracking headers.
Why customize headers?
- Reply-to address — Direct replies to your support team instead of a no-reply address
- Priority — Mark recovery emails as high priority (use sparingly)
- Custom headers — Add tracking IDs for your email analytics platform
The cfw_acr_email_headers filter
add_filter( 'cfw_acr_email_headers', function( $headers ) {
// Set a reply-to address so customers can respond
$headers[] = 'Reply-To: [email protected]';
// Add a custom header for tracking
$headers[] = 'X-ACR-Campaign: abandoned-cart-recovery';
return $headers;
} );
Modifying email content
Sometimes you need to modify the final email content after all placeholders have been replaced.
The cfw_acr_email_content filter
This filter receives the fully-rendered email HTML, after all merge tags like {{checkout_url}} have been replaced with actual values.
Example: Add UTM tracking to checkout links
You want to track which email in your sequence drives the most recoveries. Adding UTM parameters to the checkout URL lets you see this in Google Analytics.
add_filter( 'cfw_acr_email_content', function( $content ) {
// Find checkout URLs and append UTM parameters
$content = preg_replace_callback(
'/(\?cfw_acr_cart_id=[^"&\s]+)/',
function( $matches ) {
return $matches[1] . '&utm_source=checkoutwc&utm_medium=email&utm_campaign=cart_recovery';
},
$content
);
return $content;
} );
Example: Add a tracking pixel
Some email platforms use tracking pixels to measure open rates. You can inject one into your recovery emails.
add_filter( 'cfw_acr_email_content', function( $content ) {
$tracking_pixel = '
';
// Add before closing body tag
$content = str_replace( '', $tracking_pixel . '', $content );
return $content;
} );
Customizing the unsubscribe message
When customers click the unsubscribe link, they see a confirmation message. You might want to customize this to match your brand voice.
The cfw_unsubscribe_successful_message filter
add_filter( 'cfw_unsubscribe_successful_message', function( $message ) {
return "You've been unsubscribed from cart reminder emails. We respect your inbox — you won't hear from us about abandoned carts again.";
} );
Hooking into ACR events
These action hooks let you run code at specific points in the ACR lifecycle.
cfw_checkout_update_order_review
Fires when a cart is tracked. Use this to sync data to external systems like your CRM.
add_action( 'cfw_checkout_update_order_review', function() {
// Example: Log to your analytics platform
if ( function_exists( 'your_analytics_track' ) ) {
your_analytics_track( 'cart_tracked', array(
'email' => WC()->customer->get_billing_email(),
'cart_total' => WC()->cart->get_total( 'edit' ),
) );
}
} );
cfw_acr_send_email
Fires just before each recovery email is sent. Useful for logging or integrations.
add_action( 'cfw_acr_send_email', function() {
// Log email sends for debugging
if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
error_log( 'ACR: Recovery email sent at ' . current_time( 'mysql' ) );
}
} );
cfw_acr_mark_lost
Fires when a cart is marked as lost (recovery window expired). You might use this to trigger a final “we miss you” campaign through another platform.
add_action( 'cfw_acr_mark_lost', function() {
// Trigger a winback campaign in your email platform
} );
Database reference
For advanced integrations, you may need to query the ACR database directly. Abandoned carts are stored in wp_cfw_acr_carts.
| Column | Type | Description |
|---|---|---|
id |
int | Primary key |
email |
varchar | Customer email |
first_name |
varchar | Customer first name |
last_name |
varchar | Customer last name |
cart |
longtext | JSON-encoded cart contents |
subtotal |
decimal | Cart subtotal at time of tracking |
status |
varchar | active, abandoned, recovered, or lost |
created |
datetime | When the cart was first tracked |
emails_sent |
int | Number of recovery emails sent |
cart_hash |
varchar | Unique hash for secure cart restoration URLs |
Example: Query abandoned carts for reporting
global $wpdb;
$table = $wpdb->prefix . 'cfw_acr_carts';
// Get abandoned carts from the last 7 days
$abandoned = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$table}
WHERE status = %s
AND created > %s
ORDER BY subtotal DESC",
'abandoned',
gmdate( 'Y-m-d H:i:s', strtotime( '-7 days' ) )
)
);
// Now you can analyze patterns, export to CSV, etc.
Tracking carts without email addresses
By default, ACR only tracks carts when an email address has been entered. In some cases, you might want to track earlier — for example, if you’re using a logged-in customer’s email from their account.
The cfw_acr_track_cart_without_emails filter
// Track carts for logged-in users even before they enter email at checkout
add_filter( 'cfw_acr_track_cart_without_emails', function( $track ) {
if ( is_user_logged_in() ) {
return true;
}
return $track;
} );
Note: Use this carefully. Without an email, ACR can’t send recovery emails. This is mainly useful if you’re using the tracking data for analytics rather than email recovery.