How to Process WooCommerce Refunds Through Stripe Automatically

Automate Stripe refund processing when WooCommerce orders are returned or cancelled.

What This Integration Does

When a customer-service agent issues a refund inside WooCommerce, the refund usually has to be reflected back in Stripe too - otherwise WooCommerce's ledger says "refunded" but the money never actually leaves your Stripe balance. Doing that by hand is slow and error-prone. This workflow listens for WooCommerce refund events and immediately issues the matching refund against the original Stripe charge, then confirms the round-trip back to the team.

The workflow runs per refund and is idempotent on the WooCommerce refund ID, so a webhook replay won't double-refund. It only acts on Stripe-paid orders (WooCommerce orders paid via other gateways are skipped with a no-op), and any failures route to a Slack channel so finance can intervene before the customer experience deteriorates.

Prerequisites

  • A WooCommerce connection with read access to orders.
  • A Stripe connection in live mode with permission to create refunds.
  • A Slack connection for failure alerts.
  • The WooCommerce webhook configured for the order.refunded topic, pointed at the Spojit trigger URL.
  • Confidence that WooCommerce orders store the Stripe payment intent ID or charge ID on the order (typically under _stripe_intent_id or _transaction_id).

Step 1: Webhook Trigger

Add a Trigger node and set its type to Webhook. Wire it into the WooCommerce order.refunded webhook. The payload contains the order ID and the refund amount but not always the Stripe references - those come from a follow-up order fetch.

Step 2: Fetch the WooCommerce Order

Add a Connector node on woocommerce and pick the get-order tool with {{ trigger.body.id }}. The response includes the order's meta_data array, which is where the Stripe plugin stashes the payment intent / charge ID under keys like _stripe_intent_id or _stripe_charge_id.

Step 3: Extract the Stripe Reference

Add a Transform node to pull the Stripe reference out of meta_data. A typical lookup:

{
  "stripeChargeId": "{{ order.meta_data | find key="_stripe_charge_id" | value }}",
  "stripeIntentId": "{{ order.meta_data | find key="_stripe_intent_id" | value }}",
  "refundAmount": "{{ trigger.body.refunds[0].total }}",
  "currency": "{{ order.currency }}"
}

Prefer stripeChargeId when present; fall back to looking up the latest charge on the payment intent if only the intent ID is on the order. The stripe list-charges tool filtered by payment_intent handles that.

Step 4: Condition - Was It Paid via Stripe?

Add a Condition node that branches on stripeChargeId being present. If the order was paid via PayPal, bank transfer, or another gateway, route the false branch to a Slack heads-up (so a human handles it manually) and end the workflow. The true branch proceeds to the refund.

Step 5: Issue the Stripe Refund

The stripe connector exposes refunds via the raw-api-request tool against the /v1/refunds endpoint. Add a Connector node pointing at stripe with raw-api-request configured:

  • method: POST
  • path: /v1/refunds
  • body: {"charge": "{{ stripeChargeId }}", "amount": {{ refundAmount * 100 }}, "reason": "requested_by_customer", "metadata": {"woocommerce_order_id": "{{ order.id }}", "woocommerce_refund_id": "{{ trigger.body.refunds[0].id }}"}}

Stripe expects amounts in the smallest currency unit (cents), hence the multiplication by 100. Stamping the WooCommerce IDs into metadata makes reconciliation trivial later.

Step 6: Confirm and Notify

Add a Condition node on the Stripe response. On success, run two notifications in Parallel:

  • A slack send-message call to your finance channel confirming the refund (amount, customer, order number).
  • A resend send-email call to the customer confirming the refund is on its way (typically 5-10 business days).

On failure, route to a slack alert in the ops channel with the Stripe error code so the team can either retry, switch to a partial refund, or process the refund manually in the Stripe dashboard. Optionally call woocommerce update-order with a note flagging the failure on the order.

Tips

  • Idempotency keys - pass the WooCommerce refund ID as Stripe's Idempotency-Key header in raw-api-request. If the workflow retries, Stripe returns the original refund rather than issuing a second.
  • Partial refunds - WooCommerce supports partial refunds; the same logic works as long as you read refunds[0].total rather than the order total.
  • Reconciliation - the metadata object you stamp on the Stripe refund is the single best tool for matching WooCommerce and Stripe later when finance asks "what was this refund for?"

Common Pitfalls

  • Amount in dollars vs cents - forgetting the * 100 on the refund amount under-refunds the customer by 99% and inflates the next argument with support.
  • Missing Stripe reference - older orders may not have _stripe_charge_id populated. Always route to the false branch in Step 4 rather than letting the workflow blow up.
  • Webhook replays - WooCommerce retries failed webhooks. Without an idempotency key, you can issue the same refund twice and have to claw it back.
  • Refunds against captured-but-disputed charges - if the customer has already opened a chargeback, Stripe rejects the refund. The Condition in Step 6 surfaces this error so finance handles it through the dispute, not as a duplicate refund.

Testing

Pick a small live WooCommerce order paid via Stripe and issue a partial refund (e.g. $1) through the WooCommerce admin. Watch the Stripe dashboard - a matching refund should appear within seconds, tagged with the WooCommerce IDs in metadata. Confirm the Slack and email confirmations land. Once that round-trips cleanly, the workflow is safe to leave subscribed.

Learn More

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.