Refunds and store credit

CreditKit Pro handles refunds three ways. A refund on a credit-paid order returns credits to the customer balance automatically. A refund on a money-paid credit bundle claws the granted credits back automatically. Store-credit issuance is a separate manual action, available on money-paid orders only. CreditKit Pro selects the mechanism from the order’s payment method, and the three mechanisms never run together on one order.

Which refund mechanism runs on my order?

OrderTriggerDirectionAutomatic or manualRoundingWebhook fired
Paid with credits (Pay with Credits gateway)woocommerce_create_refundCredits added to the customer balanceAutomaticround() to the nearest whole creditcredit.added, type = refund
Paid with money, contains a credit bundlewoocommerce_create_refundCredits removed from the customer balanceAutomaticfloor() to the whole credit belowcredit.spent, context = refund_clawback
Paid with money, any productsAdmin submits the Issue store credit box on the order screenCredits added to the customer balanceManualround() to the nearest whole creditrefund.restored, plus credit.added with type = refund_credit

Rows one and two fire wherever a WooCommerce refund is created: the order screen in wp-admin, POST /wc/v3/orders/{id}/refunds in the REST API, WP-CLI, and WooCommerce Subscriptions.

What happens when I refund an order the customer paid with credits?

CreditKit Pro returns credits to the customer balance in proportion to the money value refunded. The calculation is round( ( refund_amount / order_total ) × credits_spent ), where credits_spent is recorded on the order at checkout.

Restoration has exactly one owner. The handler is registered against woocommerce_create_refund at file scope, so credit return happens once no matter which surface created the refund. The Pay with Credits gateway’s own process_refund() deliberately performs no credit work and only acknowledges success to WooCommerce, because adding credits in the gateway as well double-refunded customers.

Each refund record is stamped with _pcs_refund_credited once credited, so a duplicate fire of the hook for the same refund is a no-op. The running total of credits returned across all partial refunds is kept on the parent order in _pcs_credits_refunded. CreditKit Pro writes an order note naming the credits returned, and a ledger entry of type refund appears in the customer’s credit history.

One webhook fires on this path: credit.added with type set to refund. The refund.restored event does not fire here, despite the name. Wire restoration listeners to credit.added and filter on the type field. See webhooks for the full payload.

What happens when I refund a credit bundle the customer paid for with money?

Refunding a money-paid order that granted credits removes those credits again, in proportion to the money value refunded. The claw-back is floored rather than rounded: floor( ( refund_amount / order_total ) × credits_granted ). The asymmetry with restoration is deliberate, because under-removing a fraction of a credit is preferable to taking a credit the customer arguably still owns.

A cumulative cap sits on top of the proportional figure. WooCommerce’s $order->get_total() returns the post-refund total, so the denominator shrinks after every partial refund. Successive partials each computed against a shrinking total would together claw back more credits than were granted. CreditKit Pro therefore tracks the running total in _pcs_credits_clawed_back_total on the parent order and clamps every claw-back to credits_granted − already_clawed_back. The cumulative amount removed can never exceed the amount granted.

The claw-back is clamped at zero by default, so a customer who has already spent the credits is not driven into a negative balance. The merchant absorbs the difference, an order note records the shortfall, and the pcs_refund_clawback_shortfall action fires for fraud tooling. Removal runs through the same atomic spend path as a checkout, which means expiry batches are consumed oldest-first, and a credit.spent webhook fires with context set to refund_clawback.

Each refund record carries a _pcs_refund_clawback_done stamp, so a replayed hook removes nothing a second time.

Integrator hooks on the claw-back

HookKindSignatureEffect
pcs_should_clawback_bundle_refundFilterbool $clawback = true, WC_Order_Refund $refund, WC_Order $order, WC_Product|null $bundle_productReturn false to skip the claw-back for this refund. The skip is recorded, so re-firing the hook stays a no-op.
pcs_clawback_clamp_at_zeroFilterbool $clamp = true, WC_Order_Refund $refund, WC_Order $order, int $user_idReturn false to allow the balance to go negative. Default true keeps the balance at or above zero.
pcs_refund_clawback_shortfallActionint $user_id, int $shortfall, WC_Order_Refund $refund, WC_Order $orderFires when clamping leaves credits that could not be removed. $shortfall is the number of credits the merchant absorbed.

How do I give a customer store credit as goodwill?

Open a money-paid order in WooCommerce and use the Issue store credit box on the order screen. Enter a value in store currency and an optional reason, then submit. CreditKit Pro converts the value with (int) round( amount × rate ) and adds the resulting credits to the customer’s balance.

The conversion rate is a store-wide setting, not a per-order field. Set it at Settings → General → Store credit from orders → Goodwill credit rate, stored under the key refund_credit_rate with a default of 1.0. The order screen displays the active rate read-only and offers no per-order override. Full field reference lives in settings.

Goodwill issuance moves no money. No WC_Order_Refund record is created, no payment gateway is contacted, and the order total is untouched. Goodwill issuance is an independent credit grant initiated from an order screen.

Each issuance is appended to a history record on the order, and an order note logs the credits and the reason. An idempotency key stops a resubmitted form from issuing twice. A per-issuance cap of 1,000,000.00 in order value applies. An amount that converts to fewer than one credit is rejected rather than silently rounded to zero. Guest orders are rejected with a guest_order error.

This is the only path in CreditKit Pro that fires refund.restored. The same issuance also fires credit.added with type set to refund_credit.

Why can’t I issue store credit on a credit-paid order?

An order paid through the Pay with Credits gateway is stamped with _paid_in_credits. On any order carrying that stamp, the Issue store credit box is not registered at all, so the box does not appear on the order screen. A direct call to the issuance service on the same order returns a credit_order WP_Error. The block exists in two independent places, so a custom integration cannot route around the missing admin UI.

The reason is double-refunding. A credit-paid order already returns credits automatically the moment a WooCommerce refund is created against it. A manual issuance on the same order would hand the customer a second set of credits for one refunded purchase. To return credits on a credit-paid order, create a normal WooCommerce refund on the line items and let restoration do the work.

Partial refunds

Each worked example below starts from an order with a 90.00 money total.

MechanismStarting stateRefund actionCalculationBalance change
RestorationOrder paid with 200 credits, money total 90.00Partial refund of 30.00round( 30.00 / 90.00 × 200 ) = round( 66.67 )67 credits added
Claw-backMoney order of 90.00 that granted 200 creditsPartial refund of 30.00floor( 30.00 / 90.00 × 200 ) = floor( 66.67 )66 credits removed
Goodwill issuanceMoney order of 90.00, Goodwill credit rate 1.5Admin issues 30.00 of valueround( 30.00 × 1.5 )45 credits added

The cumulative cap becomes visible on the second partial claw-back. Take a money order of 100.00 that granted 500 credits. A first refund of 40.00 removes 200 credits and drops the WooCommerce order total to 60.00. A second refund of 40.00 computes floor( 40.00 / 60.00 × 500 ) = 333, but only 300 credits remain under the cap, so 300 credits are removed. A third refund of 20.00 finds the cap exhausted and removes nothing. Total removed across all three refunds: 500 credits, exactly the number granted.

What this doesn’t do

  • No split credit-and-card tender. The Pay with Credits gateway is offered only when the entire cart will be paid in credits, so no order carries a part-credit, part-card payment for a refund to divide.
  • No FIFO restoration into expiry batches. Returned credits are written as a plain balance increase, and no expiry batch is reopened.
  • Restoration does not re-create an expiry date. Credits returned by a refund carry no expiry record, so returned credits do not inherit the expiry date of the batch originally spent.
  • Goodwill issuance moves no money. No refund reaches a payment gateway and no WooCommerce refund record is created, so reports that read WooCommerce refund totals will not show a goodwill issuance.
  • Guest orders get neither restoration nor claw-back. Both automatic mechanisms require a WordPress user ID on the order, and goodwill issuance rejects guest orders explicitly.
  • Claw-back does not create negative balances under default settings. A customer who already spent the granted credits keeps a zero balance, and the merchant absorbs the shortfall unless pcs_clawback_clamp_at_zero is filtered to false.

Related reading: the settings reference, the webhooks event payloads, and the features overview.