A Stripe refund can successfully reach a WooCommerce site while still triggering this PHP warning:
Warning: Undefined array key "refund_total"
wp-content/plugins/woocommerce/includes/wc-order-functions.php
When the stack leads through:
/wc-stripe/v1/webhook
↓
Payment Plugins for Stripe WooCommerce
↓
wc_stripe_process_create_refund()
↓
wc_create_refund()
the most likely issue is not that WooCommerce cannot process refunds.
It is that the Stripe integration is passing a partially populated line_items array into WooCommerce’s refund API.
The distinction matters because wc_create_refund() currently treats refund_total as effectively required for every line-item entry it receives.
What WooCommerce Expects
WooCommerce’s current wc_create_refund() implementation loops over the original order items and checks whether that item ID exists in the supplied refund data.
If it does, core reads:
$qty = isset( $args['line_items'][ $item_id ]['qty'] )
? $args['line_items'][ $item_id ]['qty']
: 0;
$refund_total =
$args['line_items'][ $item_id ]['refund_total'];
Notice the difference.
qty receives a fallback.
refund_total does not.
WooCommerce therefore assumes this structure:
'line_items' => [
$item_id => [
'qty' => 1,
'refund_total' => 25.00,
'refund_tax' => [...],
],
],
rather than:
'line_items' => [
$item_id => [
'qty' => 1,
],
],
or:
'line_items' => [
$item_id => [],
],
The latter two structures can trigger the warning.
Core WooCommerce Builds the Array With refund_total
WooCommerce’s own refund UI reinforces this expectation.
Its current AJAX refund handler initializes each refund line with:
array(
'qty' => 0,
'refund_total' => 0,
'refund_tax' => array(),
)
and subsequently replaces refund_total with the amount entered for that line.
So even when a line has no monetary refund yet, WooCommerce’s own caller gives it:
'refund_total' => 0
rather than leaving the key absent.
That is strong evidence that callers of wc_create_refund() are expected to provide the field whenever they provide a line-item entry.
WooCommerce Tests Follow the Same Contract
WooCommerce’s current tests likewise construct line refunds containing:
'qty' => 1,
'refund_total' => 10,
and similar combinations.
Other tests use:
'refund_total' => 49.99
or explicitly:
'refund_total' => 0
rather than omitting the key.
This makes the intended contract considerably clearer than the short wc_create_refund() function documentation itself.
Newer WooCommerce Refund Code Also Normalizes Missing Totals
WooCommerce’s newer v4 REST refund implementation is even more defensive.
When it converts refund request data into the structure expected by wc_create_refund(), it explicitly creates:
'qty' => $line_item['quantity'] ?? 0,
'refund_total' => $line_item['refund_total'] ?? 0,
before handing the data downstream.
That is important.
WooCommerce’s newer API layer recognizes that external input may omit a value and normalizes the data before calling the lower-level refund function.
This supports the conclusion that the gateway integration should do the same.
Why Stripe Dashboard Refunds Expose the Problem
Payment Plugins for Stripe WooCommerce specifically documents wc_stripe_process_create_refund() as the function that synchronizes a refund created directly in Stripe back into WooCommerce.
The data available from a Stripe-side refund is fundamentally different from a refund initiated from WooCommerce.
WooCommerce-side refund
WooCommerce already knows:
Item #123
Quantity refunded: 1
Line amount: 25.00
Tax refunded: 5.00
It can therefore build a complete line-item refund structure.
Stripe Dashboard refund
Stripe may primarily tell the integration:
Refund amount: 30.00
Charge/payment: ...
Refund ID: ...
It does not inherently know which WooCommerce product lines that merchant intended the refund to correspond to.
A $30 Stripe refund could mean:
1 × Product A
or:
half of Product B
or:
shipping only
or simply:
manual goodwill refund
There may be no valid item-level breakdown to reconstruct.
That makes an order-level refund more natural unless the integration has reliable per-line information.
The Problematic Structure
The warning implies that the gateway is eventually supplying something equivalent to:
$line_items = [
123 => [
'qty' => 0,
'refund_tax' => [],
],
];
and then:
wc_create_refund(
[
'amount' => $refund_amount,
'order_id' => $order_id,
'line_items' => $line_items,
]
);
WooCommerce finds item 123 in the supplied array and therefore proceeds into its negative-line-item logic.
Then it executes:
$args['line_items'][123]['refund_total']
and PHP reports:
Undefined array key "refund_total"
The warning is therefore consistent with an incomplete caller payload.
Is refund_total Supposed to Always Be Present?
The practical answer is:
Yes, if that item ID is included in the line_items argument passed to wc_create_refund().
WooCommerce’s current:
- implementation;
- admin refund builder;
- tests;
- newer REST normalization;
all support that interpretation.
That does not mean every refund must contain line items.
line_items itself can be empty.
The distinction is:
No line-level information
→ do not provide line-item entries
versus:
Line-item entry provided
→ provide a refund_total for that entry
Fix Option 1: Supply refund_total
If Payment Plugins intentionally wants to create WooCommerce refund line items, every entry should contain a value.
Conceptually:
$line_items[ $item_id ] = [
'qty' => $qty ?? 0,
'refund_total' => $refund_total ?? 0,
'refund_tax' => $refund_tax ?? [],
];
At minimum:
'refund_total' => 0
prevents an incomplete array.
This mirrors WooCommerce’s own refund builder.
However, blindly inserting zero is not always the best semantic solution.
If the overall Stripe refund is $40 but every line refund is stored as:
0
WooCommerce will have an order-level refunded amount without a meaningful product-line allocation.
That may be perfectly appropriate for a provider-side manual refund, but the developer should make that decision deliberately.
Fix Option 2: Do Not Supply line_items
If the Stripe webhook does not contain enough information to identify which WooCommerce products were refunded, a cleaner call may be:
wc_create_refund(
[
'amount' => $refund_amount,
'reason' => $reason,
'order_id' => $order_id,
'line_items' => [],
]
);
or simply omit line_items and allow WooCommerce’s default empty array.
WooCommerce’s function supports an overall refund amount without requiring negative refund line items.
This better represents:
Stripe says $40 was refunded
without pretending that Stripe told WooCommerce:
$25 came from product A
$15 came from product B
when it did not.
Which Fix Is Better?
It depends on what information Payment Plugins actually has at webhook time.
If reliable item-level allocation exists
Use:
[
'qty' => ...,
'refund_total' => ...,
'refund_tax' => ...,
]
for every relevant item.
If only the overall Stripe refund amount exists
Use an order-level refund:
amount = Stripe refund amount
line_items = empty
This avoids fabricating line-level accounting data.
Should WooCommerce Core Add a Guard Too?
WooCommerce could defensively change:
$refund_total =
$args['line_items'][ $item_id ]['refund_total'];
to:
$refund_total =
$args['line_items'][ $item_id ]['refund_total'] ?? 0;
That would eliminate the PHP warning.
And the newer WooCommerce v4 refund code already performs essentially that normalization before calling the lower-level refund routine.
So a defensive core change would not be unreasonable.
But it would not completely fix the integration problem.
Consider this malformed caller:
[
'qty' => 2,
]
Silently interpreting that as:
[
'qty' => 2,
'refund_total' => 0,
]
suppresses the warning but may hide a real accounting error.
Core therefore has a reasonable argument for requiring callers to provide complete line-item data.
The primary bug should be corrected where the malformed array originates.
Do Not Edit WooCommerce Core
Avoid modifying:
wp-content/plugins/woocommerce/includes/wc-order-functions.php
to add:
?? 0
as the permanent fix.
WooCommerce updates will overwrite the modification.
More importantly, it treats the symptom rather than correcting the caller.
The stack trace already identifies the higher-level code path that should be investigated:
Stripe webhook
↓
wc_stripe_process_create_refund()
↓
wc_create_refund()
The Stripe integration should normalize its refund data before calling WooCommerce.
A Temporary Gateway Patch
If the warning needs to be stopped immediately before an official fix is available, patching the gateway’s refund-array construction is preferable to modifying WooCommerce core.
Conceptually, locate where the Stripe webhook creates:
$line_items[ $item_id ]
and make sure the structure contains:
'refund_total' => 0,
when no line-specific refund amount is available.
For example:
$line_items[ $item_id ] = [
'qty' => $line_items[ $item_id ]['qty'] ?? 0,
'refund_total' => $line_items[ $item_id ]['refund_total'] ?? 0,
'refund_tax' => $line_items[ $item_id ]['refund_tax'] ?? [],
];
This is a conceptual patch, not a drop-in replacement for a specific current line in Payment Plugins.
Review the installed source before applying it.
Any direct plugin modification will also be overwritten by a plugin update, so keep a record of the change.
Better Still: Patch the Caller Semantically
Before simply adding a zero, inspect why the line entry exists.
If the code is effectively doing:
foreach ( $order->get_items() as $item_id => $item ) {
$line_items[ $item_id ] = [
'qty' => 0,
];
}
for a Stripe-side refund with no product breakdown, the better correction may be:
$line_items = [];
rather than creating zero-value records for every product.
The presence of a line item tells WooCommerce:
This refund contains information about this particular order item.
If no such information exists, leaving the collection empty is cleaner.
Verify Whether the Refund Still Synchronizes Correctly
The warning itself may not stop the refund from being recorded.
That needs to be tested separately.
For an affected order:
- Note the WooCommerce order total.
- Open the corresponding payment in Stripe.
- Create a controlled partial refund from the Stripe Dashboard.
- Allow the webhook to reach WordPress.
- Open the WooCommerce order.
- Check whether a refund object was created.
- Verify the refunded amount.
- Check the order’s remaining total.
- Inspect any refund line items.
- Compare WooCommerce’s refund total with Stripe.
You may find:
Stripe refund: $20
WooCommerce refund: $20
PHP warning: yes
In that case the issue is currently noisy but financially synchronized.
A more serious result would be:
Stripe refund: $20
WooCommerce refund: missing/wrong
PHP warning: yes
which means the incomplete line-item structure is affecting accounting as well as logging.
Compare a WooCommerce-Initiated Refund
Run the same test through:
WooCommerce → Order → Refund
rather than Stripe Dashboard.
If:
WooCommerce refund
→ no warning
while:
Stripe Dashboard refund
→ warning
that isolates the problem even further to the provider-to-WooCommerce webhook synchronization path.
WooCommerce’s own admin refund code initializes refund_total, which explains why its native path should not produce the same warning.
Check Webhook Logs
Payment Plugins specifically documents its refund webhook handler as synchronizing refunds created directly in Stripe back to WooCommerce.
For one controlled event, capture:
Stripe event ID
WooCommerce order ID
Stripe refund amount
PHP warning timestamp
WooCommerce refund ID
Do not post:
- API secret keys;
- webhook signing secrets;
- customer card data;
- full customer addresses.
The event/refund identifiers are usually enough for developers to trace the execution path.
Why It May Have Become More Visible on Newer PHP Versions
PHP 8 surfaces undefined array access prominently as:
Warning: Undefined array key "refund_total"
Code that previously relied on missing array offsets without explicit guards therefore becomes much more visible in logs.
That does not make PHP the root cause.
PHP is reporting that the integration supplied a structure WooCommerce was not written to receive.
Suppressing PHP warnings globally is not a good solution.
Keep Warnings Out of the Webhook Response
On production systems, use:
define( 'WP_DEBUG_DISPLAY', false );
when debugging is enabled.
Logging can remain active:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
for a controlled investigation.
You generally do not want PHP warnings rendered into REST or webhook responses.
Even when the HTTP status remains successful, unexpected diagnostic output can make API debugging harder and can expose server paths.
Current WooCommerce Still Uses the Unguarded Access
As of September 2026, WooCommerce 11.1.0 is the current WordPress.org release.
Its current code reference still shows wc_create_refund() reading the supplied line item’s refund_total directly after checking only that the line-item entry itself exists.
So this is not an old snippet from a long-retired WooCommerce branch.
The underlying contract remains relevant to current WooCommerce.
Current Payment Plugins Version
WordPress.org currently lists Payment Plugins for Stripe WooCommerce 4.0.13, released September 4, 2026.
The plugin’s documentation still identifies:
wc_stripe_process_create_refund()
as the mechanism used to create a WooCommerce refund when a refund originates in the Stripe Dashboard.
If the warning reproduces on 4.0.13, include that exact version in the support report.
Do not assume an older refund-related workaround applies unchanged to the 4.x codebase.
What Payment Plugins Should Test
A regression test should cover at least:
Full refund from WooCommerce
WooCommerce
→ Stripe
→ WooCommerce remains synchronized
Partial refund from WooCommerce
With line quantities and monetary amounts.
Full refund from Stripe Dashboard
Stripe
→ webhook
→ WooCommerce order-level refund
Partial refund from Stripe Dashboard
Especially where Stripe provides no WooCommerce item allocation.
For every test, assert that any array passed to:
wc_create_refund()
obeys:
If line_items[$id] exists,
refund_total also exists.
A simple regression assertion could catch this before runtime:
foreach ( $line_items as $line_item ) {
$this->assertArrayHasKey(
'refund_total',
$line_item
);
}
Recommended Bug Fix
The upstream fix belongs primarily in:
Payment Plugins for Stripe WooCommerce
→ wc_stripe_process_create_refund()
The developer should ensure one of these two valid states:
Complete line-level refund
'line_items' => [
$item_id => [
'qty' => 1,
'refund_total' => 25.00,
'refund_tax' => [],
],
],
No line-level information
'line_items' => [],
The invalid middle state is:
'line_items' => [
$item_id => [
'qty' => 0,
// refund_total missing.
],
],
That is what wc_create_refund() is not designed to receive.
Practical Conclusion
The warning is caused by a mismatch between the refund data supplied by the Stripe webhook integration and the structure expected by WooCommerce.
WooCommerce currently treats refund_total as mandatory once a particular order item is included in line_items. Its own AJAX refund builder initializes the field to zero, its tests provide it, and its newer v4 refund layer normalizes missing totals to zero before calling the underlying refund function.
Therefore, the most appropriate correction is in wc_stripe_process_create_refund().
If Payment Plugins has valid per-item refund information, it should always supply:
'refund_total'
for every supplied item.
If a refund created in Stripe has no WooCommerce product-level allocation, the integration should normally create an order-level refund without supplying incomplete line_items.
Adding:
?? 0
inside WooCommerce core could suppress the PHP warning, but it would also make core silently accept malformed caller data and would not address whether the gateway should have created those line entries in the first place.
So the warning is not merely harmless PHP noise. It identifies a real contract mismatch in the refund synchronization path, even if the overall refund amount currently still ends up correct.