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


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-01or 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:
- Shopify Webhook (
refunds/create) fires when a refund is initiated in Shopify admin or via API - Data Validation checks that required fields exist and the refund amount is above zero
- Stripe Refund API call issues the actual money movement
- Conditional Branch handles Stripe success vs. failure
- SendGrid Email notifies the customer
- Airtable logs the completed transaction for accounting reconciliation
- 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');23const shopifySecret = 'YOUR_WEBHOOK_SECRET';4const hmacHeader = $input.first().headers['x-shopify-hmac-sha256'];5const rawBody = JSON.stringify($input.first().body);67const generatedHmac = crypto8 .createHmac('sha256', shopifySecret)9 .update(rawBody, 'utf8')10 .digest('base64');1112if (generatedHmac !== hmacHeader) {13 throw new Error('HMAC validation failed — request rejected');14}1516return $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:
| n8n Field Name | Source Expression |
|---|---|
| `shopify_refund_id` | `{{ $json.body.id }}` |
| `shopify_order_id` | `{{ $json.body.order_id }}` |
| `refund_amount` | `{{ $json.body.transactions[0].amount }}` |
| `currency` | `{{ $json.body.transactions[0].currency }}` |
| `gateway` | `{{ $json.body.transactions[0].gateway }}` |
| `stripe_charge_id` | `{{ $json.body.transactions[0].authorization }}` |
| `customer_email` | `{{ $json.body.transactions[0].receipt.email }}` |
| `refund_reason` | `{{ $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 than0 - 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: POST2URL: https://api.stripe.com/v1/refunds3Auth: Header Auth4 Name: Authorization5 Value: Bearer sk_live_YOUR_STRIPE_SECRET_KEY6Body: Form-Data (URL Encoded)
Body parameters:
1charge = {{ $json.stripe_charge_id }}2amount = {{ Math.round($json.refund_amount * 100) }}3reason = requested_by_customer4metadata[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 }}equalssucceeded
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;34return [{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 = Completed7Gateway = stripe8Customer 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:
- Click the three dots on the canvas → Settings
- Enable Error Workflow → point to a separate
Refund Error Handlerworkflow - In the error workflow, use the Error Trigger node — it receives
$execution.errorautomatically
Error handler function node:
1const error = $input.first().json;23return [{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, notorders/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().rawBodyif 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_idalready exists in your log table. If it does, stop execution.
1// Deduplication check Function node2const existingRecords = $input.first().json.records;34if (existingRecords && existingRecords.length > 0) {5 // Already processed — halt gracefully6 return [];7}89return $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_refundablefield — 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 Sendpermission 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 Datathat routesgateway == 'paypal'to the PayPal Refunds API (POST /v2/payments/captures/{captureId}/refund) andgateway == 'stripe'to the flow above - Partial refund support: Map
$json.body.refund_line_itemsto 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.