Branch8

How to Automate Refund Flows with n8n: End-to-End Shopify to Payment Gateway Tutorial

Matt Li
Matt Li
March 14, 2026
14 mins read
Technology
n8n logo on a dark workflow automation background representing automated refund processing pipelines

Key Takeaways

  • A Shopify `refunds/create` webhook combined with n8n's HTTP Request node and Stripe's `POST /v1/refunds` endpoint handles end-to-end refund processing without human intervention.
  • Always validate Shopify HMAC signatures in a Function node before processing — unverified webhooks are a fraud risk for cross-border merchants.
  • Stripe expects amounts in the smallest currency unit; multiply HKD/SGD/USD by 100, but do NOT multiply zero-decimal currencies like JPY.
  • Implement Airtable-based deduplication before the Stripe API call to prevent double-refunds caused by Shopify's automatic webhook retry logic.
  • For Asia-Pacific merchants, add a parallel branch for WeChat Pay or Alipay refunds using their respective APIs — settlement timelines differ significantly from card networks.
  • n8n's workflow-level Error Workflow setting ensures any silent node failure triggers an ops alert rather than leaving a refund stuck mid-process.

Quick Answer

To automate refund flows with n8n, create a workflow that listens for a Shopify refunds/create webhook, validates the refund payload, calls the Stripe POST /v1/refunds endpoint with the original charge ID, then sends a confirmation email via SendGrid. The full workflow takes under 2 hours to configure and eliminates manual processing across every sales channel.

Manual refund processing is one of the highest-cost, highest-error operational tasks for any Asia-based e-commerce operator running multi-channel stores. A single refund can require a support agent to log into Shopify, cross-reference an order in Stripe or PayPal, issue the gateway refund, update a CRM record, notify the customer in their local language, and log the transaction in an accounting sheet — all manually, across potentially five different tabs.

This tutorial walks you through building a production-ready, end-to-end refund automation workflow in n8n (v1.x) that handles all of that automatically. We'll use Shopify as the storefront trigger, Stripe as the payment gateway, SendGrid for transactional email, and Airtable as a lightweight audit log — a stack common across Hong Kong, Singapore, and Southeast Asian DTC brands.

Prerequisites

Before starting, ensure you have:

  • n8n v1.30 or later (self-hosted via Docker or n8n Cloud)
  • A Shopify store with API access (Admin API version 2024-01 or later)
  • A Stripe account with a live or test secret key
  • A SendGrid account with a verified sender domain
  • An Airtable base with a table called Refund Log
  • Basic familiarity with n8n's canvas and node configuration
  • An HTTPS endpoint for your n8n instance (required for Shopify webhooks; use ngrok for local testing)

Architecture Overview

The workflow follows this sequence:

  1. Shopify Webhook (refunds/create) fires when a refund is initiated in Shopify admin or via API
  2. Data Validation checks that required fields exist and the refund amount is above zero
  3. Stripe Refund API call issues the actual money movement
  4. Conditional Branch handles Stripe success vs. failure
  5. SendGrid Email notifies the customer
  6. Airtable logs the completed transaction for accounting reconciliation
  7. Slack Alert (optional) notifies the ops team of failures

Ready to Transform Your Ecommerce Operations?

Branch8 specializes in ecommerce platform implementation and AI-powered automation solutions. Contact us today to discuss your ecommerce automation strategy.

Step 1 — Set Up the Shopify Webhook Trigger

Expected outcome: n8n receives a live JSON payload whenever a refund is created in Shopify.

1.1 Create the Webhook Node

In your n8n canvas, add a Webhook node (not the Shopify trigger node — we'll use a raw webhook for maximum payload control).

Configure it as follows:

  • HTTP Method: POST
  • Path: /shopify-refund
  • Authentication: Header Auth
  • Header Name: X-Shopify-Hmac-Sha256

Your webhook URL will look like:

1https://your-n8n-instance.com/webhook/shopify-refund

1.2 Register the Webhook in Shopify

Use the Shopify Admin API to register the webhook programmatically:

1curl -X POST \
2 https://your-store.myshopify.com/admin/api/2024-01/webhooks.json \
3 -H "X-Shopify-Access-Token: shpat_YOUR_ACCESS_TOKEN" \
4 -H "Content-Type: application/json" \
5 -d '{
6 "webhook": {
7 "topic": "refunds/create",
8 "address": "https://your-n8n-instance.com/webhook/shopify-refund",
9 "format": "json"
10 }
11 }'

Shopify returns a webhook.id confirming registration. Save it.

1.3 Validate the HMAC Signature

After the Webhook node, add a Function node called Validate HMAC to verify the request is genuinely from Shopify:

1const crypto = require('crypto');
2
3const shopifySecret = 'YOUR_WEBHOOK_SECRET';
4const hmacHeader = $input.first().headers['x-shopify-hmac-sha256'];
5const rawBody = JSON.stringify($input.first().body);
6
7const generatedHmac = crypto
8 .createHmac('sha256', shopifySecret)
9 .update(rawBody, 'utf8')
10 .digest('base64');
11
12if (generatedHmac !== hmacHeader) {
13 throw new Error('HMAC validation failed — request rejected');
14}
15
16return $input.all();
Security note: Never skip HMAC validation in production. Unverified webhooks are a common attack vector for fraudulent refund triggers, especially relevant for merchants processing cross-border transactions in HK and SG.

Step 2 — Parse and Validate the Refund Payload

Expected outcome: A clean, validated object containing order_id, refund_amount, currency, customer_email, and stripe_charge_id.

Add a Set node called Parse Refund Data. Map the following fields from the Shopify webhook body:

`shopify_refund_id`
Source Expression`{{ $json.body.id }}`
`shopify_order_id`
Source Expression`{{ $json.body.order_id }}`
`refund_amount`
Source Expression`{{ $json.body.transactions[0].amount }}`
`currency`
Source Expression`{{ $json.body.transactions[0].currency }}`
`gateway`
Source Expression`{{ $json.body.transactions[0].gateway }}`
`stripe_charge_id`
Source Expression`{{ $json.body.transactions[0].authorization }}`
`customer_email`
Source Expression`{{ $json.body.transactions[0].receipt.email }}`
`refund_reason`
Source Expression`{{ $json.body.note }}`

Next, add an IF node called Validate Required Fields:

  • Condition 1: {{ $json.refund_amount }} is not empty
  • Condition 2: {{ $json.stripe_charge_id }} is not empty
  • Condition 3: {{ $json.refund_amount }} greater than 0
  • Combine: ALL conditions must be true

Route the false branch to a Slack or HTTP Request node that logs the malformed payload to your ops channel before stopping.

Ready to Transform Your Ecommerce Operations?

Branch8 specializes in ecommerce platform implementation and AI-powered automation solutions. Contact us today to discuss your ecommerce automation strategy.

Step 3 — Issue the Refund via Stripe API

Expected outcome: Stripe processes the refund and returns a refund object with status: succeeded.

Add an HTTP Request node called Stripe Create Refund.

1Method: POST
2URL: https://api.stripe.com/v1/refunds
3Auth: Header Auth
4 Name: Authorization
5 Value: Bearer sk_live_YOUR_STRIPE_SECRET_KEY
6Body: Form-Data (URL Encoded)

Body parameters:

1charge = {{ $json.stripe_charge_id }}
2amount = {{ Math.round($json.refund_amount * 100) }}
3reason = requested_by_customer
4metadata[shopify_order_id] = {{ $json.shopify_order_id }}
5metadata[shopify_refund_id] = {{ $json.shopify_refund_id }}
Important: Stripe expects amounts in the smallest currency unit. Multiply HKD, SGD, or USD amounts by 100. For JPY (a zero-decimal currency), do not multiply.

The Stripe response payload will look like:

1{
2 "id": "re_3OxYz2ABC123",
3 "object": "refund",
4 "amount": 4500,
5 "charge": "ch_3OxYz2ABC123",
6 "created": 1711084800,
7 "currency": "hkd",
8 "status": "succeeded",
9 "reason": "requested_by_customer"
10}

Step 4 — Handle Stripe Success and Failure Branches

Expected outcome: Clean routing so successful refunds proceed to notification; failures are escalated immediately.

Add an IF node called Check Stripe Status:

  • Condition: {{ $json.status }} equals succeeded

4.1 Failure Branch — Stripe Error Handling

On the false branch, add a Set node to format the error:

1// In a Function node called "Format Stripe Error"
2const stripeError = $input.first().json;
3
4return [{
5 json: {
6 error_code: stripeError.error?.code || 'unknown',
7 error_message: stripeError.error?.message || 'Stripe refund failed',
8 decline_code: stripeError.error?.decline_code || null,
9 shopify_order_id: $('Parse Refund Data').first().json.shopify_order_id,
10 timestamp: new Date().toISOString()
11 }
12}];

Then route to an HTTP Request node that posts to your Slack #ops-alerts channel:

1{
2 "text": "🚨 Refund Failed",
3 "blocks": [
4 {
5 "type": "section",
6 "text": {
7 "type": "mrkdwn",
8 "text": "*Stripe Refund Failed*\nOrder: `{{ $json.shopify_order_id }}`\nError: `{{ $json.error_message }}`\nCode: `{{ $json.error_code }}`"
9 }
10 }
11 ]
12}

Ready to Transform Your Ecommerce Operations?

Branch8 specializes in ecommerce platform implementation and AI-powered automation solutions. Contact us today to discuss your ecommerce automation strategy.

Step 5 — Send Customer Notification via SendGrid

Expected outcome: Customer receives a branded refund confirmation email within seconds of the Stripe refund succeeding.

On the true (success) branch from Step 4, add a SendGrid node called Send Refund Confirmation.

Configure the node:

  • Operation: Send Email
  • To Email: {{ $('Parse Refund Data').first().json.customer_email }}
  • From Email: [email protected]
  • Subject: Your refund of {{ $('Parse Refund Data').first().json.currency.toUpperCase() }} {{ $('Parse Refund Data').first().json.refund_amount }} is on its way
  • Template ID: d-YOUR_SENDGRID_DYNAMIC_TEMPLATE_ID

Dynamic template data (SendGrid v3 format):

1{
2 "customer_name": "{{ $('Parse Refund Data').first().json.customer_email.split('@')[0] }}",
3 "refund_amount": "{{ $('Parse Refund Data').first().json.refund_amount }}",
4 "currency": "{{ $('Parse Refund Data').first().json.currency.toUpperCase() }}",
5 "order_id": "{{ $('Parse Refund Data').first().json.shopify_order_id }}",
6 "stripe_refund_id": "{{ $json.id }}",
7 "processing_days": "5-7",
8 "support_email": "[email protected]"
9}
Cross-border tip: For stores serving mainland China customers via WeChat Pay or Alipay (common for HK/SG merchants), add a second branch here that calls the WeChat Pay Refund API (POST https://api.mch.weixin.qq.com/v3/refund/domestic/refunds) in parallel. Refund timelines differ: WeChat Pay settles same-day while Stripe card refunds take 5-10 days — communicate this clearly in your email template.

Step 6 — Log to Airtable for Audit Trail

Expected outcome: Every processed refund is recorded in Airtable with full traceability for accounting reconciliation.

Add an Airtable node called Log Refund to Airtable:

  • Operation: Create Record
  • Base ID: appYOUR_BASE_ID
  • Table Name: Refund Log

Fields to map:

1Shopify Order ID = {{ $('Parse Refund Data').first().json.shopify_order_id }}
2Shopify Refund ID = {{ $('Parse Refund Data').first().json.shopify_refund_id }}
3Stripe Refund ID = {{ $('Stripe Create Refund').first().json.id }}
4Amount = {{ $('Parse Refund Data').first().json.refund_amount }}
5Currency = {{ $('Parse Refund Data').first().json.currency }}
6Status = Completed
7Gateway = stripe
8Customer Email = {{ $('Parse Refund Data').first().json.customer_email }}
9Reason = {{ $('Parse Refund Data').first().json.refund_reason }}
10Processed At = {{ new Date().toISOString() }}

Ready to Transform Your Ecommerce Operations?

Branch8 specializes in ecommerce platform implementation and AI-powered automation solutions. Contact us today to discuss your ecommerce automation strategy.

Step 7 — Add Global Error Handling

Expected outcome: Any unhandled node failure triggers an alert rather than silently dying.

In n8n v1.x, configure workflow-level error handling:

  1. Click the three dots on the canvas → Settings
  2. Enable Error Workflow → point to a separate Refund Error Handler workflow
  3. In the error workflow, use the Error Trigger node — it receives $execution.error automatically

Error handler function node:

1const error = $input.first().json;
2
3return [{
4 json: {
5 workflow_name: error.workflow.name,
6 node_name: error.node.name,
7 error_message: error.message,
8 execution_id: error.execution.id,
9 timestamp: new Date().toISOString(),
10 environment: 'production'
11 }
12}];

Route this to PagerDuty, Slack, or your internal alerting system.

Step 8 — Test the Complete Workflow

Expected outcome: A test refund in Shopify flows through all nodes with green execution status.

8.1 Create a Test Refund via Shopify API

1curl -X POST \
2 https://your-store.myshopify.com/admin/api/2024-01/orders/ORDER_ID/refunds.json \
3 -H "X-Shopify-Access-Token: shpat_YOUR_TOKEN" \
4 -H "Content-Type: application/json" \
5 -d '{
6 "refund": {
7 "currency": "HKD",
8 "notify": false,
9 "note": "Test refund via n8n automation",
10 "transactions": [
11 {
12 "parent_id": TRANSACTION_ID,
13 "amount": "100.00",
14 "kind": "refund",
15 "gateway": "stripe"
16 }
17 ]
18 }
19 }'

8.2 Verify Each Node Output

In n8n, open Executions and check each node:

  • Webhook: Should show raw Shopify payload
  • Validate HMAC: Should pass without throwing
  • Parse Refund Data: Should show clean mapped fields
  • Stripe Create Refund: Should show status: succeeded
  • Send Refund Confirmation: Should show SendGrid 202 Accepted
  • Log Refund to Airtable: Should show new record ID

Ready to Transform Your Ecommerce Operations?

Branch8 specializes in ecommerce platform implementation and AI-powered automation solutions. Contact us today to discuss your ecommerce automation strategy.

Troubleshooting

Webhook Not Receiving Payloads

  • Confirm your n8n URL is publicly accessible over HTTPS
  • Check Shopify's webhook delivery log under Settings → Notifications → Webhooks
  • Verify the webhook is registered to the correct topic (refunds/create, not orders/updated)
  • For local development, ensure ngrok tunnel is active: ngrok http 5678

HMAC Validation Always Failing

  • Shopify signs the raw request body. If n8n's Webhook node parses JSON before you access it, the signature won't match. Use $input.first().rawBody if available, or disable body parsing in the webhook node settings.
  • Double-check you're using the webhook secret from Shopify → Settings → Notifications, not the API secret key.

Stripe Returns charge_already_refunded

  • Your workflow may be executing twice due to Shopify's webhook retry logic (Shopify retries up to 19 times on non-2xx responses)
  • Fix: Add a Airtable lookup node before the Stripe call that checks if shopify_refund_id already exists in your log table. If it does, stop execution.
1// Deduplication check Function node
2const existingRecords = $input.first().json.records;
3
4if (existingRecords && existingRecords.length > 0) {
5 // Already processed — halt gracefully
6 return [];
7}
8
9return $input.all();

Stripe Amount Mismatch Error

  • Ensure you're converting to smallest currency unit: Math.round(amount * 100) for HKD/SGD/USD
  • For currencies with no decimals (JPY, KRW), pass the integer directly without multiplication
  • Check the Stripe charge's amount_refundable field — you cannot refund more than the original charge minus previous refunds

SendGrid 403 Forbidden

  • Verify your sender email is authenticated under SendGrid → Settings → Sender Authentication
  • Confirm your API key has Mail Send permission scope
  • For Asia-Pacific deliverability, ensure your SendGrid account is on a dedicated IP with proper PTR records

n8n Execution Times Out

  • Default n8n execution timeout is 60 seconds. For high-volume stores, Stripe API calls can stack.
  • In n8n/config/default.json, increase: "executionTimeout": 300
  • Consider splitting the Airtable logging into a separate async workflow triggered via n8n's internal HTTP call

Production Deployment Checklist

Before going live with this workflow:

  • [ ] Replace all test API keys with live credentials stored in n8n Credentials (never hardcoded)
  • [ ] Enable workflow-level error handling pointing to your error handler workflow
  • [ ] Set up Airtable deduplication to prevent double-refunds on webhook retries
  • [ ] Configure n8n execution log retention (minimum 30 days for audit compliance in HK/SG)
  • [ ] Test with a real HKD/SGD transaction in Stripe test mode
  • [ ] Verify SendGrid template renders correctly in Gmail, Apple Mail, and local clients
  • [ ] Document the workflow in your ops runbook with the Shopify webhook ID for easy rotation

Ready to Transform Your Ecommerce Operations?

Branch8 specializes in ecommerce platform implementation and AI-powered automation solutions. Contact us today to discuss your ecommerce automation strategy.

What to Build Next

Once this baseline workflow is stable, extend it in these directions:

  • Multi-gateway branching: Add an IF node after Parse Refund Data that routes gateway == 'paypal' to the PayPal Refunds API (POST /v2/payments/captures/{captureId}/refund) and gateway == 'stripe' to the flow above
  • Partial refund support: Map $json.body.refund_line_items to calculate partial amounts per SKU
  • CRM sync: After the Airtable log, call HubSpot or Salesforce to update the contact's refund count — useful for identifying serial returners across your Asia market
  • Accounting automation: Pipe the Airtable log to Xero via n8n's Xero node to auto-create credit notes

Branch8 builds and maintains production n8n workflows for e-commerce operators across Hong Kong, Singapore, and Southeast Asia. If you need this refund automation deployed, customized for multiple payment gateways, or integrated with your ERP — reach out to the Branch8 team for a scoping call.

FAQ

Yes. Add an IF node after parsing the Shopify payload that checks the `gateway` field. Route `stripe` to the Stripe Refunds API, `paypal` to `POST /v2/payments/captures/{captureId}/refund`, and `alipay` or `wechatpay` to their respective gateway APIs. Each branch runs independently with its own error handling.