Customer Review Request
Overview
The Customer Review Request feature emails customers a few days after their order is marked completed, asking them to review the products they bought. The email links to a dedicated tokenized landing page where the customer rates each eligible product item, optionally writes a review, and submits everything in a single form.
The feature lives in the Automattic\WooCommerce\Internal\OrderReviews namespace and registers its WordPress hooks only when the feature flag is on.
Feature flag
The feature ships off by default behind the customer_review_request feature flag (beta). Merchants enable it under WooCommerce → Settings → Advanced → Features. While the flag is off, none of the feature's hooks run, the review-request email is not registered with WC_Emails, and no host page is created. Enabling it registers the hooks and seeds the host page (see "Host page" below).
How it works
When the feature is enabled, these pieces register their hooks:
- Scheduler — on order completion, schedules the delayed email via Action Scheduler, and cancels it if the order later leaves the eligible set (cancelled, refunded, processing, on-hold, pending, failed, trashed, deleted). It also skips scheduling when nothing on the order is reviewable (see "Skip when nothing is reviewable" below).
- Endpoint — routes
/review-order/{id}/?key={order_key}to the WC-managed Review Order page and renders it through the[woocommerce_review_order]shortcode. Access requires a matching order key and an eligible status; for an order that has a customer id, a logged-in visitor must be that customer (guests, and anyone holding the key for a customer-less guest order, still pass). - SubmissionHandler — the AJAX endpoint that stores submitted reviews, one per
(product, variation)slot, honoring thecomment_moderationoption and updating a review in place when the customer resubmits the same row. - ItemEligibility — decides which line items are reviewable, excludes fully-refunded items, and keeps each variation's row independent.
StarRating and Meta are stateless view helpers the templates call directly.
Host page
When the feature is enabled, WooCommerce creates and maintains a dedicated Review Order page (slug review-order, embedding the [woocommerce_review_order] shortcode). It is seeded by any "create default WooCommerce pages" run — fresh install, activation, database upgrade, and Status → Tools → Create default WooCommerce pages — and is self-healing: if the page is unpublished or the stored page option is re-pointed, it is restored on the next load. The page is hidden from nav menus and wp_list_pages() / core/page-list output, and is labeled "— Review Order Page" in the admin Pages list (matching how Cart / Checkout / Shop / My account are labeled). Its theme-rendered page title is suppressed so it does not duplicate the body <h1>.
Skip when nothing is reviewable
If an order would land on the empty "nothing to review" page anyway, no email is scheduled or sent. At schedule time the Scheduler skips the order and logs the reason no reviewable items; at send time the email re-checks and is silently dropped if every item has since been reviewed or had reviews disabled (for example, the admin turns off reviews during the delay window).
A customer can still reach the empty-state page directly — by finishing or losing their reviewable items after the email went out, or by opening a bookmarked or admin-shared link — so customer-review-order-empty.php keeps rendering that fallback.
Per-variation reviews
The Review Order page renders one row per reviewable slot — a distinct (product, variation) pair — rather than one per raw line item. customer-review-order.php passes the items through ItemEligibility::unique_slot_items() first, so two line items of the same variation collapse into a single row. Two different variations of one variable product (e.g. a T-shirt purchased in Small + Medium) still get two distinct rows, each with its own image and attribute summary, and each can carry an independent rating + text review.
Storage is on the parent product post — WC reviews don't live on variations — but the line item context is captured via two pieces of comment meta written by SubmissionHandler alongside _review_order_id:
_review_variation_id— the line item'svariation_id(0for simple products, kept for symmetry)._review_variation_summary— the attribute summary at the moment the review was written (e.g."Size: Small, Colour: Red"). Captured as a snapshot so historical reviews stay readable even if the variation is later retired or its attribute terms are renamed.
ItemEligibility::find_existing_review() keys by (order_id, product_id, variation_id, email), so each variation row pre-fills against its own review on revisit. The Review Order row template renders the variation's attribute summary via ItemEligibility::format_variation_summary(), which restricts the attributes to the live variation's whitelisted slugs before passing them to wc_get_formatted_variation() — third-party item meta (engraving, gift-wrap, custom add-ons) is intentionally excluded so private personalisation data can't leak into the public review snapshot.
On the parent product's single-product Reviews tab, the variation summary is prepended above each comment body through the existing woocommerce_review_before_comment_text action; reviews without the summary meta render unchanged. The theming class is .woocommerce-review__variation-summary.
Tokenized URL helper
$url = wc_get_review_order_url( $order );
Returns the tokenized review-order URL for a given WC_Order. Use this rather than constructing the URL manually so the helper's filter and pretty/plain-permalink fallback both apply.
Filter and action reference
Filters
-
woocommerce_should_send_review_request(bool,WC_Order)
Returnfalseto skip scheduling the email for a specific order. -
woocommerce_review_request_delay_seconds(int)
Override the delay (in seconds) before the email fires. Defaults to the value configured on the email settings screen. -
woocommerce_review_order_url(string,WC_Order)
Replace the URL emitted bywc_get_review_order_url(). -
woocommerce_review_order_eligible_statuses(string[],WC_Order|null)
The order statuses treated as eligible. Defaults to[ 'completed' ]. The same list is consulted at four points: the route-level page gate, the AJAX submission handler, the send-time re-check, and the scheduler's cancellation (a queued email is unscheduled only when the order leaves this set). Initial scheduling stays tied to thecompletedtransition, so widening this filter does not queue a request when an order enters the added status; it only keeps a queued request alive through transitions inside the expanded set and lets the page and send pass. The second argument is the order, ornullwhen the scheduler evaluates a status change without a resolved order. -
woocommerce_review_order_eligible_items(WC_Order_Item[],WC_Order)
The order items considered reviewable. The default callback excludes fully-refunded items. Beyond choosing which rows the page renders, it feeds the actionable-items check at both schedule time and send time, so emptying it for an order suppresses scheduling and sending too. -
woocommerce_review_order_rating_labels(array<int,string>)
Customize the 1-5 star labels surfaced beside the control. Defaults toVery poor / Not that bad / Average / Good / Perfect.
Actions
Send pipeline
-
woocommerce_send_review_request(int $order_id)
Action Scheduler hook.Schedulerenqueues this for each eligible order when the order moves tocompleted, with the delay configured on the email settings screen. When the delay elapses, Action Scheduler fires the hook and WC's transactional-email pipeline re-dispatches it aswoocommerce_send_review_request_notification(the hook is registered with the pipeline inWC_Emails::init_transactional_emails()). Useful for plugins that need to observe the scheduled send without owning the email itself. It is a plaindo_action, so a callback's return value cannot cancel the send — to prevent a send, use thewoocommerce_should_send_review_requestfilter (evaluated at schedule time) instead. -
woocommerce_send_review_request_notification(int $order_id)
Transactional email pipeline action, fired byWC_Emailsafterwoocommerce_send_review_request.WC_Email_Customer_Review_Request::trigger()listens here and builds + sends the email. Useful when you need to add a second listener (e.g. analytics) alongside the built-in mailer.
Form
-
woocommerce_review_order_form_fields(WC_Order_Item_Product,WC_Product,WC_Order,int $row_index)
Fires after each row's image / rating / review columns, as a sibling of.woocommerce-review-order__item-rowand immediately before</li>. Echo extra fields directly; they render below the row's columns so injected UI doesn't disturb the three-column layout. -
woocommerce_review_order_submitted(WC_Order $order,array $results)
Fires after the form has been processed, even when some rows ended inerror.$resultsis the per-row outcome map keyed by row index, withproduct_id,variation_id,status(ok | pending_moderation | error), and (on success)comment_id.
Post-submit thank-you flow
After a successful AJAX submission the thank-you view is shown in place of the form, re-using the empty-state copy. If any row failed, the form stays visible so the per-row error notes remain readable.
Refreshing the page or re-clicking the email link re-renders the form for any remaining items — server-side routing is unchanged, so there is no URL hop and no server-side flag.
Theme overrides
The page renders through wc_get_template(), so themes can override any of:
templates/order/customer-review-order.php— the page wrapper. Branches between the form view and the empty-state thank-you view, and pre-computes the per-row decisions consumed by the row template.templates/order/customer-review-order-row.php— one form row per item. Receivesexisting_ratingandexisting_textso the row pre-fills when the customer already submitted a review for this order.templates/order/customer-review-order-empty.php— thank-you view rendered when no actionable rows remain (every item already reviewed for this order, or every item skipped because reviews are disabled).templates/order/star-rating.php— the accessible star-rating control partial.
To override, copy a template into your theme while preserving the relative path:
templates/order/customer-review-order.php→yourtheme/woocommerce/order/customer-review-order.phptemplates/order/customer-review-order-row.php→yourtheme/woocommerce/order/customer-review-order-row.phptemplates/order/customer-review-order-empty.php→yourtheme/woocommerce/order/customer-review-order-empty.phptemplates/order/star-rating.php→yourtheme/woocommerce/order/star-rating.php
Theme-aware classes used by the page
The templates lean on built-in WC/WP classes so the active theme drives the styling instead of opinionated SCSS:
- The disabled-products notice composes with
.woocommerce-info— themes that already restyle classic WC notices automatically restyle this one too. - The customer/order meta line uses
.woocommerce-breadcrumbso it adopts the theme's small-grey-secondary-text treatment. - The Submit button picks up
wc_wp_theme_get_element_class_name( 'button' )(i.e.wp-element-buttonon block themes), so block themes paint it with their button styling.
If you do override customer-review-order.php, preserve those class names so theme integrators don't have to re-style the page from scratch.
Email template overrides
The HTML and plain-text bodies of the email itself follow standard WC conventions:
templates/emails/customer-review-request.phptemplates/emails/plain/customer-review-request.phptemplates/emails/block/customer-review-request.php(block-based email editor)
Tests
PHPUnit tests live under plugins/woocommerce/tests/php/src/Internal/OrderReviews/ (plus the email-class test under plugins/woocommerce/tests/php/includes/emails/); the Playwright specs are in plugins/woocommerce/tests/e2e/tests/order/review-order-page.spec.ts.
pnpm --filter=@woocommerce/plugin-woocommerce test:php:env -- --filter "OrderReviews|Customer_Review_Request"
pnpm --filter=@woocommerce/plugin-woocommerce test:e2e:default --grep "Customer Review Request — Review Order page"
Accessibility notes
- The star-rating control is a group of five native
<input type="radio">elements inside a wrapper that carries the ARIArole="radiogroup"andaria-labelledbypointing at the rating label. The visual stars are SVG; the inputs themselves are visually hidden but remain in the accessibility tree. - Keyboard navigation is supported via Arrow keys and Home/End.
- A live caption (
aria-live="polite") announces the selected rating's label. - The "Required" indicator on the rating label is exposed to screen readers via a
.screen-reader-textspan alongside the visual asterisk. - Inline mandatory-rating errors render with
role="alert"so assistive tech announces them on submission. - The host-page heading suppression keeps the chrome
<h1>from duplicating the body<h1>, avoiding the "page announced twice" issue with screen readers.