
Returns Management
- 72 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Process returns end to end: generate prepaid labels, apply refund or exchange logic, update inventory, and notify customers automatically.
About
Automates the full returns flow including prepaid labels, refund or exchange decisioning, inventory updates, and customer notifications. A developer uses it to run returns without manual handling per order.
- Generates prepaid return labels and applies refund or exchange logic
- Auto-updates inventory and notifies customers
Returns Management by the numbers
- 72 all-time installs (skills.sh)
- Ranked #926 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill returns-managementAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 72 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Process returns end to end: generate prepaid labels, apply refund or exchange logic, update inventory, and notify customers automatically.
Files
Returns Management
Overview
A returns management system lets customers initiate returns self-service, generates prepaid shipping labels, processes refunds or exchanges when the item is received, and handles restocking decisions. A well-run returns process builds customer trust and can become a competitive advantage. Most platforms have solid native returns workflows or purpose-built apps that handle this without custom development.
When to Use This Skill
- When launching a self-service returns portal to reduce customer service workload
- When you need a structured RMA process with tracking numbers to prevent refund fraud
- When building logic that differentiates between "refund to original payment", "store credit", and "exchange" resolutions
- When integrating return label generation with carrier APIs (UPS, FedEx, USPS)
- When managing restocking — deciding whether returned items go back to sellable inventory or to a quarantine bin
Core Instructions
Step 1: Determine your platform and choose the right returns tool
| Platform | Recommended Tool | Why |
|---|---|---|
| Shopify | Loop Returns, Returnly, or AfterShip Returns | Loop Returns is the most popular; handles self-service portal, labels, exchanges, and store credit automatically |
| WooCommerce | WooCommerce Returns and Warranty Requests plugin or ReturnGo | WooCommerce's official extension handles RMA workflows; ReturnGo adds a branded self-service portal |
| BigCommerce | AfterShip Returns Center, Loop Returns, or ReturnGo | All three have native BigCommerce integrations with self-service portals |
| Custom / Headless | Build an RMA system + Shippo/EasyPost for return labels | Use Shippo's return label API; build the approval and restocking workflow on top |
Step 2: Set up the returns portal
Shopify
Option A: Loop Returns (most popular, starts free)
1. Install Loop Returns from the Shopify App Store 2. In Loop settings, configure your return window (e.g., 30 days from delivery), eligible resolutions (refund, exchange, store credit), and which products are returnable 3. Loop creates a branded returns portal at yourstore.com/returns — customers enter their order number and email to initiate 4. Configure return reasons (wrong size, changed mind, defective, etc.) in Loop → Policy → Return Reasons 5. Loop generates prepaid USPS, UPS, or FedEx return labels automatically based on your carrier settings 6. Enable Instant Exchange (Loop feature) so customers can place an exchange order immediately without waiting for the return to arrive 7. When a return is received and inspected, Loop can automatically process the refund and sync the inventory update back to Shopify
Option B: Shopify's built-in returns (good for simple workflows)
1. On an individual order: go to Orders → [Order] → Return 2. Select which items to return, the return reason, and whether to restock inventory 3. Generate a prepaid return label via Shopify Shipping and email it to the customer 4. When the package arrives: go back to the order → mark as received → issue refund 5. Limitation: Shopify's native returns do not have a self-service customer portal — customers must contact you to initiate
WooCommerce
Using WooCommerce Returns and Warranty Requests (official extension):
1. Install the plugin from WooCommerce.com (paid extension) 2. Go to WooCommerce → Returns to configure: return window, eligible order statuses, allowed reasons, and notification emails 3. Customers see a "Request a Return" button on their order detail page in My Account 4. Each return request creates an RMA record visible in WooCommerce → Returns 5. To issue a return label: integrate with ShipStation which has a "Create Return Label" button from the order view, or use Pirate Ship manually for low-volume 6. When the package arrives: go to the RMA in WooCommerce → Returns → mark as received → issue refund from the original order
Using ReturnGo (branded portal, free plan available):
1. Install ReturnGo from WordPress.org or their website 2. ReturnGo creates a branded self-service portal and handles email notifications automatically 3. Configure resolution options (refund, store credit, exchange) and carrier integrations in ReturnGo settings
BigCommerce
Using AfterShip Returns Center:
1. Install AfterShip Returns Center from the BigCommerce App Marketplace 2. Set up your returns portal URL (branded with your store name) 3. Configure return policy rules: return window, eligible products, required reasons 4. AfterShip generates prepaid return labels (USPS, UPS, FedEx) when you approve a return request 5. Returns syncs status back to BigCommerce orders automatically
Using Loop Returns: 1. Install from BigCommerce App Marketplace 2. Same configuration as the Shopify workflow above
Step 3: Configure refund and exchange resolution logic
Regardless of platform, define your resolution options clearly before going live:
1. Refund to original payment method — most straightforward; Stripe/PayPal refunds process in 5–10 business days 2. Store credit — faster for customers, better for your cash flow; Loop and AfterShip support automatic store credit issuance 3. Exchange — requires checking stock availability for the replacement item; Loop's Instant Exchange feature handles this automatically
Key settings to configure:
- Return window: Start the clock from delivery date, not order date (this matters — check your app's setting)
- Restocking: Decide whether returned items automatically go back to inventory or require manual inspection first. For used goods or high-value items, enable manual inspection before restocking
- Restocking fee: Configure this per product category if applicable (e.g., 15% for opened electronics)
- Final sale items: Tag final-sale products in your platform and configure the returns app to block returns on those tags
Step 4: Set up return label generation
Most returns apps generate labels automatically. For manual label creation:
Shopify
- Shopify Shipping → Create Return Label: goes directly to the carrier and creates a "return" label type (charged when the customer drops off, not when you create it)
- Available carriers: USPS, UPS, DHL (varies by region)
WooCommerce
- ShipStation: the most reliable option for return labels via WooCommerce; go to an order in ShipStation → Create Return Shipment
- Pirate Ship: manual process but the cheapest USPS rates; create a return label at pirateship.com and email it to the customer
Custom / Headless
import Shippo from 'shippo';
const shippo = Shippo(process.env.SHIPPO_API_KEY);
// Create a prepaid return label (customer sends back to warehouse)
async function createReturnLabel(params: {
customerName: string;
customerAddress: Address;
warehouseName: string;
warehouseAddress: Address;
estimatedWeightLbs: number;
}): Promise<{ trackingNumber: string; labelUrl: string }> {
const shipment = await shippo.shipment.create({
address_from: {
name: params.customerName,
street1: params.customerAddress.street1,
city: params.customerAddress.city,
state: params.customerAddress.state,
zip: params.customerAddress.zip,
country: 'US',
},
address_to: {
name: params.warehouseName,
street1: params.warehouseAddress.street1,
city: params.warehouseAddress.city,
state: params.warehouseAddress.state,
zip: params.warehouseAddress.zip,
country: 'US',
},
parcels: [{
weight: (params.estimatedWeightLbs * 16).toString(), // oz
mass_unit: 'oz',
length: '12', width: '10', height: '4',
distance_unit: 'in',
}],
async: false,
is_return: true, // marks this as a return label
});
const rate = shipment.rates.find(r => r.servicelevel.token === 'usps_priority')
?? shipment.rates[0];
const transaction = await shippo.transaction.create({
rate: rate.object_id,
label_file_type: 'PDF',
});
return {
trackingNumber: transaction.tracking_number,
labelUrl: transaction.label_url,
};
}Best Practices
- Require label from your system, not the customer's — prepaid labels let you control carrier choice, negotiate rates, and track packages; customer-supplied labels make tracking impossible
- Segregate received returns before restocking — create a physical "returned goods" bin and a logical "quarantine" status; never auto-restock without at least a visual inspection
- Issue refunds after receiving the package — only pre-authorize refunds if you offer "instant refund" as a deliberate premium feature
- Notify customers at every status change — send emails at "label created", "package received", and "refund/exchange processed" stages; lack of communication drives "where is my refund" contacts
- Start the return window from delivery date, not order date — this is more fair to customers and reduces disputes; most returns apps support this setting
- Track return reasons — aggregate return reasons monthly and share with buying/product teams; high return rates on specific SKUs indicate a product quality or description problem
Common Pitfalls
| Problem | Solution |
|---|---|
| Refund processed before return physically arrives | Configure your returns app to trigger refunds on "package received" status, not on "return requested"; check Loop/AfterShip workflow settings |
| Restocking creates inaccurate inventory counts | Use a quarantine step before restocking; only increment inventory after inspection confirms the item is resalable |
| Customer initiates return after window expires | Set your platform return window clearly and ensure the returns app checks order delivery date — some apps default to order date which gives customers less time |
| Return label goes unused but was purchased | Use "pay-on-scan" label types where available (USPS allows this via Shippo) — you're only charged when the customer drops off the package |
Related Skills
- @order-fulfillment-workflow
- @shipment-tracking
- @returns-refund-policy
- @stripe-integration
- @gift-cards
{
"context": "Tests whether the agent uses the Shippo library for label generation with the correct configuration (is_return: true, PDF format, USPS Priority preference), requires system-generated labels rather than customer-supplied ones, and sends the correct email notification upon label issuance.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Shippo package used",
"max_score": 12,
"description": "The implementation imports and uses the 'shippo' npm package (not a different carrier SDK or a generic HTTP client calling Shippo's REST API directly)"
},
{
"name": "Shippo API key from env",
"max_score": 6,
"description": "Shippo client is initialized with process.env.SHIPPO_API_KEY"
},
{
"name": "is_return: true",
"max_score": 12,
"description": "The shipment.create call includes is_return: true in the shipment parameters"
},
{
"name": "USPS Priority preference",
"max_score": 12,
"description": "Rate selection code attempts to find a rate with servicelevel.token === 'usps_priority' first, before falling back to another rate"
},
{
"name": "PDF label format",
"max_score": 10,
"description": "The transaction.create call specifies label_file_type: 'PDF'"
},
{
"name": "Warehouse env vars used",
"max_score": 8,
"description": "The address_to (destination/warehouse) in the shipment uses environment variables for warehouse name, street, city, state, and zip"
},
{
"name": "Shipment record persisted",
"max_score": 8,
"description": "tracking_number, carrier, and label_url are saved to the database after the Shippo transaction completes"
},
{
"name": "Status updated to label_issued",
"max_score": 8,
"description": "The return request status is updated to 'label_issued' after the label is generated"
},
{
"name": "Email sent with label URL",
"max_score": 8,
"description": "An email is sent to the customer after the label is generated, and includes the label URL in the email data payload"
},
{
"name": "Email uses return-label template",
"max_score": 8,
"description": "The email is sent using a template named 'return-label' (not a plain-text email or a different template name)"
},
{
"name": "shippo in package.json",
"max_score": 8,
"description": "package.json lists 'shippo' as a dependency"
}
]
}
Prepaid Return Label Generation Service
Problem/Feature Description
A direct-to-consumer brand has approved a batch of return requests that are ready to be processed by their warehouse team. The operations manager wants to ensure that all return labels are generated by their own system — this gives the company full visibility over which carrier is used, allows them to leverage their negotiated shipping rates, and makes package tracking reliable end-to-end. The previous ad hoc process of asking customers to ship packages any way they like has led to lost packages, disputed refunds, and no tracking data.
The engineering team needs to build the function that handles RMA approval: it should generate a prepaid return shipping label via a carrier integration, persist the shipment record, update the return request status, and immediately send the customer an email with their label so they can ship the item back.
Output Specification
Produce a TypeScript module `label-service.ts` that exports an approveReturnAndIssueLabel function. The function should:
- Accept a return request ID and a staff user ID
- Generate a prepaid return label using a carrier shipping API (the environment will have carrier API credentials and warehouse address details set as environment variables — use whatever carrier integration library fits best)
- Save the shipment details (tracking number, carrier, label URL) to the database
- Update the return request status
- Email the label to the customer
The code does not need to connect to a real database or run live — use placeholder db and emailService objects as if they are in scope, but the carrier API integration logic must be complete, correct, and use a real carrier API package. Include a package.json listing any runtime dependencies.
{
"context": "Tests whether the agent correctly implements the return processing workflow: segregating quarantine from restock items, gating inventory restocking behind an inspection status, issuing refunds only after physical receipt, storing restocking fees in cents as a deduction (not a negative line item), supporting three resolution types, and using current pricing for exchange orders.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Quarantine and restock tracked separately",
"max_score": 8,
"description": "The inspection results record both restock_quantity and quarantine_quantity as distinct fields on return_lines (not combined into one field)"
},
{
"name": "No auto-restock without inspection",
"max_score": 10,
"description": "Inventory is only incremented for units explicitly marked as restock_quantity > 0 (NOT quarantine_quantity); quarantine units are NOT added to sellable inventory"
},
{
"name": "Inspecting status gate",
"max_score": 8,
"description": "The code includes logic for or reference to an 'inspecting' status between 'received' and 'resolved' — does NOT jump directly from received to resolved without an inspection step"
},
{
"name": "Refund after receipt only",
"max_score": 10,
"description": "Resolution (refund, store credit, or exchange) is executed only after the return is confirmed received — the code does NOT issue refunds at label_issued or in_transit stage"
},
{
"name": "Restocking fee in cents (INTEGER)",
"max_score": 8,
"description": "Restocking fee is stored and passed as an integer value in cents (not dollars, not a float/decimal)"
},
{
"name": "Fee as refund deduction",
"max_score": 10,
"description": "Restocking fee is subtracted from the total refund amount (e.g., Math.max(0, totalRefund - restockingFee)), NOT recorded as a negative line item in the order"
},
{
"name": "Refund to original payment",
"max_score": 8,
"description": "When resolution is 'refund', the code calls a function that issues money back to the original payment method"
},
{
"name": "Store credit resolution",
"max_score": 8,
"description": "When resolution is 'store_credit', the code calls a separate store credit issuance function (distinct from the refund-to-payment path)"
},
{
"name": "Exchange order resolution",
"max_score": 8,
"description": "When resolution is 'exchange', the code creates a new exchange order (distinct from issuing a monetary refund)"
},
{
"name": "Exchange at current prices",
"max_score": 10,
"description": "DESIGN.md or code comments state that exchange orders use current/live prices plus a zero-cost adjustment for the return credit — NOT the original order's historical prices"
},
{
"name": "Transaction wraps multi-step writes",
"max_score": 8,
"description": "Multi-step database operations (updating return lines + updating inventory + updating RMA status) are wrapped in a database transaction"
},
{
"name": "resolved_at timestamp",
"max_score": 4,
"description": "The return request is updated with a resolved_at timestamp when its status changes to 'resolved'"
}
]
}
Warehouse Return Processing and Resolution
Problem/Feature Description
A warehouse team at a consumer electronics retailer receives dozens of returned packages each day. Currently, the process is error-prone: sometimes items are immediately put back on shelves before being checked for damage, other times refunds are issued automatically as soon as a label is scanned, leaving the business exposed to fraud when packages contain wrong or damaged goods. The finance team has also flagged inconsistencies in how restocking fees are recorded — some engineers subtract them as negative line items in the order system, which breaks financial reports.
The team needs a robust backend service to handle the full lifecycle from the moment a returned package arrives: recording the physical condition of each item, deciding what goes back to sellable stock versus to a damage bin, applying any applicable restocking fees, and then executing the appropriate resolution (full refund, store credit, or exchange for a replacement). The resolution should only happen after physical receipt is confirmed.
Output Specification
Produce a TypeScript module `receive-service.ts` that exports:
1. receiveAndResolveReturn(returnRequestId, inspectionResults, staffId) — marks a return as received, records item conditions, handles inventory, calculates any fees, and triggers resolution.
2. executeResolution(returnRequestId) — determines the resolution type and dispatches to the appropriate handler (refund to original payment, store credit, or exchange order).
3. calculateRefundAmount(rma) — computes the net refund after fees.
The code does not need to connect to a real database — use placeholder db calls and stub functions like issueRefundToOriginalPayment, issueStoreCredit, and createExchangeOrder as if they are defined elsewhere. The business logic, data flow, and structural decisions must be complete and correct. Include a brief `DESIGN.md` explaining the key design choices made for inventory management, refund timing, restocking fee accounting, and exchange order pricing.
{
"context": "Tests whether the agent designs the correct RMA schema (tables, fields, constraints), generates human-readable RMA numbers using a database sequence, implements configurable return window policies with correct category logic, validates return eligibility server-side, and returns the correct eligibility response structure.",
"type": "weighted_checklist",
"checklist": [
{
"name": "return_requests table",
"max_score": 6,
"description": "schema.sql includes a return_requests table with a UUID primary key and a rma_number column that is unique"
},
{
"name": "Status CHECK constraint",
"max_score": 8,
"description": "return_requests.status has a CHECK constraint that includes at minimum: 'requested', 'approved', 'label_issued', 'in_transit', 'received', 'inspecting', 'resolved', 'rejected'"
},
{
"name": "Resolution CHECK constraint",
"max_score": 7,
"description": "return_requests.resolution has a CHECK constraint with exactly the three values: 'refund', 'exchange', 'store_credit'"
},
{
"name": "Restocking fee in cents",
"max_score": 6,
"description": "return_requests includes a restocking_fee column defined as INTEGER (not DECIMAL/FLOAT), defaulting to 0"
},
{
"name": "return_lines condition CHECK",
"max_score": 6,
"description": "return_lines table includes a condition column with CHECK constraint containing: 'new', 'like_new', 'damaged', 'defective'"
},
{
"name": "Restock and quarantine columns",
"max_score": 6,
"description": "return_lines table includes both restock_quantity and quarantine_quantity columns (both INTEGER, default 0)"
},
{
"name": "return_shipments table",
"max_score": 6,
"description": "schema.sql includes a return_shipments table with tracking_number, carrier, label_url, and received_at columns"
},
{
"name": "RMA number format",
"max_score": 10,
"description": "RMA number generation produces format 'RMA-YYYY-NNNNN' (year + 5-digit zero-padded sequence number), using a database sequence (nextval) rather than random values"
},
{
"name": "Policy: default window",
"max_score": 5,
"description": "Return policy configuration sets the default window to 30 days"
},
{
"name": "Policy: electronics",
"max_score": 8,
"description": "Return policy configuration sets electronics window to 15 days with a restocking fee percentage of 15%"
},
{
"name": "Policy: final_sale",
"max_score": 8,
"description": "Return policy configuration includes a final_sale category with a window of 0 days (no returns allowed)"
},
{
"name": "Server-side window validation",
"max_score": 8,
"description": "The return request creation function computes daysSincePurchase from order.created_at on the server (not trusting any client-provided value) and throws/rejects if it exceeds the configured window"
},
{
"name": "Customer ownership check",
"max_score": 7,
"description": "The return request creation function validates that the order's customer_id matches the requesting customerId before proceeding"
},
{
"name": "Eligibility response shape",
"max_score": 9,
"description": "The eligibility endpoint returns an object with: eligible (boolean), daysRemaining (non-negative integer), and reason (null when eligible, or a string constant like 'WINDOW_EXPIRED' or 'ORDER_NOT_DELIVERED' when not eligible)"
}
]
}
E-Commerce Returns Backend
Problem/Feature Description
A mid-sized online retailer currently handles all product returns through manual customer-service emails, which creates delays, inconsistencies, and opportunities for fraud. The team wants to launch a self-service returns portal that lets customers submit return requests directly, reduces customer service overhead, and provides structured tracking of every return from initiation to resolution.
The engineering team has been asked to build the backend data layer and business logic for initiating return requests. The system must support multiple product categories with different return rules — different product categories (such as electronics or promotional items) have different return windows and restocking fees, and some items may not be returnable at all. The system must also assign each return a human-readable reference number that customer service agents can look up quickly over the phone.
Output Specification
Produce a TypeScript implementation with:
1. `schema.sql` — Database schema for the returns system. Include all tables needed to track return requests, the individual line items being returned, and any associated shipment information.
2. `returns.ts` — TypeScript module containing:
- The return policy configuration with category-based rules
- A function to determine how many return window days an order has
- A function to create a new return request (with all required validation)
- A function that generates human-readable RMA reference numbers
3. `eligibility.ts` — A REST API endpoint handler for checking whether a specific order is eligible for return, returning structured eligibility data to the client.
The code does not need to connect to a real database — use placeholder db calls (as if a db ORM object is in scope), but the logic and structure must be complete and correct.
{
"name": "finsi/returns-management",
"version": "0.1.0",
"summary": "RMA flow with return labels, refund/exchange logic, and restocking",
"skills": {
"returns-management": {
"path": "SKILL.md"
}
}
}