Introduction

Stateset One provides a comprehensive REST and GraphQL API for Returns Management, enabling businesses to automate their entire returns process from initiation to refund. This guide will walk you through building a complete returns management system.

The Returns Management module in Stateset includes various objects that facilitate the returns processes. These objects typically encompass:

  • Return
  • Return Line Items
  • Orders
  • Refunds

Returns Management Overview

Returns are an inevitable part of the eCommerce business. Customers may request returns for a variety of reasons, such as receiving a damaged or defective item, or simply changing their mind about a purchase. Regardless of the reason, it is important for businesses to have a streamlined process in place to manage returns efficiently and effectively. This helps in enhancing customer experience and building brand loyalty.

Challenges with Returns Management

Returns management can be a complex and time-consuming process. It involves multiple steps, such as generating return labels, creating returns in the system, and processing refunds. These steps are often manual and require significant time and effort. This can lead to delays in processing returns, resulting in customer dissatisfaction and loss of revenue.

Stateset’s Returns Management Solution

Stateset’s Returns Management solution automates the entire returns process, from generating return labels to processing refunds. This helps in streamlining the process and reducing the time and effort required to manage returns. The solution leverages the Temporal workflow orchestration framework to automate the various steps involved in returns management. This includes generating and emailing return labels, creating returns in Stateset, providing search capabilities for efficient record retrieval, updating customer support platforms with tracking information, and processing instant refunds. By automating the returns process, Stateset enables businesses to effectively manage and track returns, thereby enhancing customer experience and improving operational efficiency.

Prerequisites

Before you begin, ensure you have:

  • A Stateset account (Sign up here)
  • API credentials from the Stateset Cloud Console
  • Node.js 16+ installed (for SDK examples)
  • Basic understanding of REST APIs
  • Your eCommerce platform credentials (Shopify, WooCommerce, etc.)

Quickstart Guide

1

Install SDK

Install the stateset-node SDK in your project:

npm install stateset-node
# or
yarn add stateset-node
2

Initialize Client

Set up your Stateset client with proper error handling:

import { StateSetClient } from 'stateset-node';

const client = new StateSetClient({
  apiKey: process.env.STATESET_API_KEY,
  environment: process.env.NODE_ENV || 'production'
});
3

Create a Return

Create a new return with validation:

async function createReturn(orderData) {
  try {
    // Validate input data
    if (!orderData.order_id || !orderData.items) {
      throw new Error('Missing required fields');
    }
    
    const returnData = {
      order_id: orderData.order_id,
      customer_email: orderData.customer_email,
      items: orderData.items.map(item => ({
        sku: item.sku,
        quantity: item.quantity,
        reason: item.reason,
        condition: item.condition || 'A'
      })),
      status: 'NEW',
      type: orderData.return_type || 'REPLACEMENT',
      notes: orderData.customer_notes
    };
    
    const return = await client.returns.create(returnData);
    
    // Generate RMA number
    const rma = `RMA-${return.id}`;
    await client.returns.update(return.id, { rma });
    
    return { success: true, return, rma };
    
  } catch (error) {
    console.error('Failed to create return:', error);
    throw new Error(`Return creation failed: ${error.message}`);
  }
}
4

Generate Return Label

Automatically generate and email return shipping labels:

async function generateReturnLabel(returnId, shippingAddress) {
  try {
    const label = await client.shipping.createReturnLabel({
      return_id: returnId,
      carrier: 'fedex',
      service: 'FEDEX_GROUND',
      from_address: shippingAddress,
      to_address: {
        name: 'Returns Center',
        company: 'Your Company',
        street1: '123 Warehouse St',
        city: 'Los Angeles',
        state: 'CA',
        zip: '90001',
        country: 'US'
      }
    });
    
    // Email label to customer
    await client.notifications.send({
      type: 'return_label',
      return_id: returnId,
      attachments: [{
        filename: `return-label-${returnId}.pdf`,
        content: label.label_pdf_base64,
        encoding: 'base64'
      }]
    });
    
    return label;
    
  } catch (error) {
    console.error('Label generation failed:', error);
    throw error;
  }
}

Return Initiation

When a customer requests a return, the Returns Management workflow in Stateset is triggered. The workflow captures the necessary information from the customer, such as order details, reason for return, and any supporting documentation.

Return Label Generation and Emailing

Using the captured information, the workflow generates a return label that includes the shipping address and other relevant details. The label is then emailed to the customer, providing clear instructions on how to return the item.

Creation of a Return in Stateset

Simultaneously, the workflow creates a new return in Stateset, associating it with the corresponding order and customer information. This allows for streamlined tracking and management of the return process within the Stateset platform.

Stateset Workflow using Temporal

Temporal is an open source programming model that can simplify your code, make your applications more reliable, and help you deliver more features faster. The temporal framework allows activities to be defined and workflows to execute them based on the state or specific signals. Temporal provides the workflow management engine to combine the power of serverless API calls with a deterministic scheduler for execution. By leveraging the Temporal workflow orchestration framework, Stateset automates the entire Returns Management (RMA) process. This includes generating and emailing return labels, creating returns in Stateset, providing search capabilities for efficient record retrieval, updating customer support platforms with tracking information, and processing instant refunds. This automation streamlines the return process, enhances customer experience, and enables businesses to effectively manage and track returns.

Here is an example of Stateset’s Return API using Temporal Worker & Workflow:

javascript

import { Worker } from '@temporalio/worker';
import { URL } from 'url';
import * as activities from './activities.js';

async function run() {

  const worker = await Worker.create({
    workflowsPath: new URL('./workflows/return-ticket-approved.js', import.meta.url).pathname,
    activities,
    taskQueue: 'stateset-returns-automation',
    namespace: 'stateset'
  });

  await worker.run();

}

run().catch((err) => {
  console.error(err);
  process.exit(1);
});

javascript
import { proxyActivities } from '@temporalio/workflow';
import * as wf from '@temporalio/workflow';

// Proxy Activities
const { generateResponse, createZendeskComment, createReturnRecord, generateUSLabel, generateCALabel, updateWorkflowId, updateMatch, cancelSubscription } = proxyActivities({
    startToCloseTimeout: '1 minute',
});

/** A workflow that simply calls an activity */
export async function returnApprovedWorkflow(body, ticket_id_int) {
    
    let workflow_state = [];
    var cancel_subscription = body.cancel_subscription;
    var condition = body.condition;
    var match = body.match;
    var country = body.country;

    // Generate Response
    await generateResponse(body);

    // Generate Label
    if (country = "US") {

        await generateUSLabel(ticket_id_int);

    } else {

        await generateCALabel(ticket_id_int);
    }

    // Create Return
    var return_id = await createReturnRecord(ticket_id_int);

    // Update Workflow Id
    await updateWorkflowId(return_id, wf.workflowInfo().workflowId);

    // Cance Subscription
    if (cancel_subscription) {

        await cancelSubscription(customer_email, ticket_id_int);

    };

    // Sleep
    await wf.sleep('3 days');

    // If Instant Refund and Conditon = A
    if (condition == "A" && match == true) {

        const matched_return = await updateMatch(return_id, wf.workflowInfo().workflowId);

        await refundOrder(order_id);
        
        await createZendeskComment(ticket_id_int, condition);

        return 'return_and_refund_processed';

    }

}

Search and Lookup Capabilities

The Returns Management workflow in Stateset incorporates search functionality powered by Algolia that enables easy retrieval of return information. Users can search for returns based on various criteria, such as serial number, RMA number, or order ID. This helps in quickly locating specific return records and accessing relevant details.

javascript
// Save the Return Record to the Algolia Index
    index.saveObject({
        objectID: return_record.insert_returns.returning[0].id,
        order_id: return_record.insert_returns.returning[0].order_id,
        description: return_record.insert_returns.returning[0].description,
        reported_condition: return_record.insert_returns.returning[0].reported_condition,
        status: return_record.insert_returns.returning[0].status,
        serial_number: return_record.insert_returns.returning[0].serial_number,
        tracking_number: return_record.insert_returns.returning[0].tracking_number,
        rma: return_record.insert_returns.returning[0].rma,
        country: return_record.insert_returns.returning[0].country
    })

Instant Refunds Processing

For eligible returns, the Returns Management workflow includes instant refund processing. Once the returned item is received and inspected, the workflow automatically initiates the refund process, facilitating timely and efficient reimbursement to the customer. This eliminates delays and enhances customer satisfaction by ensuring prompt resolution of return requests.

Integration with Zendesk or Gorgias

The workflow integrates with customer support platforms like Zendesk or Gorgias to streamline communication and updates. Upon generating the return label and initiating the return process, the workflow updates the respective ticketing system with tracking information. This ensures that customer support representatives have real-time visibility into the status of returns and can provide accurate updates to customers when needed.

Aftership Tracking

javascript

export default async function (req, res) {

  var newReturn = req.body.event.data.new;
  var timestamp = req.body.created_at;
  var event_id = newReturn.id;
  var order_id = newReturn.order_id;
  var serial_number = newReturn.serial_number;
  var rma = newReturn.rma;
  var email = newReturn.customerEmail;
  var tracking_number = newReturn.tracking_number;
  var token = process.env.AFTERSHIP_TOKEN

    try {

      // Notification
      let aftership_tracking_response = await fetch('https://api.aftership.com/v4/trackings', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'aftership-api-key': token
        },
        body: JSON.stringify({
          "tracking" : {
              "tracking_number": tracking_number,
              "slug": "fedex",
              "title": rma,
              "emails": [
                  "customer@gmail.com"
              ],
              "order_id": order_id,
              "custom_fields": {
                  "label_created_date": timestamp,
                  "customer_email": email,
                  "serial_number": serial_number,
                  "workflow_event_id": event_id,
              }
          }
      })
    })

      console.log(aftership_tracking_response);

      return res.status(200).json({ status: "AfterShip Tracking API Event Successfully Executed" });

  } catch (error) {
    return res.status(500).json({
      "error_code": "internal_error",
      "error_message": error.toString ? error.toString() : error
    })
  };
};

Refunds

Refunds describes the general data about the goods or services being refunded to your customers. For example, you might have multiple line items on an order, each would be a separate Refund Line Item. The Refund is corresponding to the metadata regarding Refunde Items. The Refund resource has two major components:

  • Transaction records of money returned to the customer
  • The line items included in the refund, along with restocking instructions

Before you create a refund, use the calculate endpoint to generate accurate refund transactions. Specify the line items that are being refunded, their quantity and restock instructions, and whether you’re refunding shipping costs. You can then use the response of the calculate endpoint to create the actual refund. When you create a refund using the response from the calculate endpoint, you can set additional options, such as whether to notify the customer of the refund. You can refund less than the calculated amount for either shipping or the line items by setting a custom value for the amount property.

Shopify refunds

Stateset Returns Automation integrates with Shopify’s native Return APIs. For more information see the Stateset Returns Automation App on the Shopify App Store: https://apps.shopify.com/stateset-returns-automation

Here is an example of how to process a Refund using the Shopify Returns API:

javascript

    try {
        const suggestedRefund = await getSuggestedRefund(
            returnId,
            returnLineItems,
            shop,
            accessToken
        );

        const returnQuery = gql`
            mutation returnRefundMutation(
                $returnLineItems: [ReturnRefundLineItemInput!]!, 
                $refundShipping: RefundShippingInput,
                $orderTransactions: [ReturnRefundOrderTransactionInput!]
                ) {
                returnRefund(
                    returnRefundInput: {
                        returnId: "${returnId}",
                        returnRefundLineItems: $returnLineItems,
                        refundShipping: $refundShipping,
                        orderTransactions: $orderTransactions
                    }
                )
                {
                    refund {
                    id
                    }
                    userErrors {
                    field
                    message
                    }
                }
            }`;

        const returnResponseJson: any = await ShopifyClient(
            shop,
            accessToken,
            returnQuery,
            {
                returnLineItems,
                refundShipping: {
                    fullRefund: true,
                    shippingRefundAmount: {
                        amount:
                            suggestedRefund?.shipping?.maximumRefundableSet
                                ?.shopMoney?.amount || 0,
                        currencyCode:
                            suggestedRefund?.shipping?.maximumRefundableSet
                                ?.shopMoney?.currencyCode || 'USD'
                    }
                },
                orderTransactions:
                    suggestedRefund?.suggestedTransactions?.map((item: any) => {
                        return {
                            transactionAmount: {
                                amount: item.amountSet?.shopMoney?.amount || 0,
                                currencyCode:
                                    item.amountSet?.shopMoney?.currencyCode ||
                                    'USD'
                            },
                            parentId: item.parentTransaction.id
                        };
                    }) || []
            }
        );

        console.log(
            returnResponseJson.returnRefund.userErrors,
            returnResponseJson.returnRefund
        );

        if (returnResponseJson.returnRefund.userErrors?.length) {
            const message =
                returnResponseJson.returnRefund.userErrors[0].message;

            res.status(500).send(message);
            return;
        }

        if (returnResponseJson?.returnRefund?.refund) {
            await updateReturn(id, {
                status: 'REFUNDED'
            });

            return res.status(200).json({
                ...returnResponseJson.returnRefund.refund,
                msg: 'Successful Refund Return'
            });
        }

        res.status(500).send('There is no refund Transaction');
    } catch (error: any) {
        console.log(error);
        res.status(500).send(error.message);
    }
});

If a refund includes shipping costs, or if you choose to refund line items for less than their calculated amount, then an order adjustment is created automatically to account for the discrepancy in the store’s financial reports.

WooCommerce Refunds

The return record in Stateset will include customer information such as the order id which is used to query the total order amount and tax amount. These values are used to calculate the total refund amount depending on the restocking fee. A refund can be placed via the Return record which will trigger a refund in WooCommerce and then Stripe. It may take a few days for the refund to appear on the customers debit card. The refund can be verified in Stateset by going to the Stripe returns list view. The condition and rationale from the return record can also be sent back to Zendesk from Stateset as a private message for the CSR.

javascript

// refund for the item being returned
var refund_line_item = {
   "id": item_id,
   "quantity": 1,
   "refund_total": item_total - restocking_fee,
   "refund_tax": refund_tax_line_item_items
 };
 
// Process Refund Automatically
// Automated Refund Data
const data = {
    "api_refund": true,
    "reason": reason_category,
    "line_items": refund_line_items
};


// POST Request Options
var requestOptions = {
    method: 'POST',
    headers: {
            'Content-Type': 'application/json'
        },
    body: JSON.stringify(data)
};

// Process Refund
await fetch(`https://ecommerce.com/wp-json/wc/v3/orders/${order_id_int}/refunds`, requestOptions)
        .then(response => response.json())
        .then(json => {})

Complete Returns Workflow Implementation

Here’s a production-ready implementation of a complete returns management system:

import { StateSetClient } from 'stateset-node';
import { TemporalClient } from '@temporalio/client';

class ReturnsManagementService {
  constructor(apiKey) {
    this.client = new StateSetClient({ apiKey });
    this.temporal = new TemporalClient();
  }

  /**
   * Complete returns workflow with all steps automated
   */
  async processReturnRequest(request) {
    const workflow = {
      steps: [],
      errors: [],
      returnId: null
    };

    try {
      // Step 1: Validate return eligibility
      const eligibility = await this.checkReturnEligibility(request.order_id);
      if (!eligibility.eligible) {
        throw new Error(`Return not eligible: ${eligibility.reason}`);
      }
      workflow.steps.push({ step: 'eligibility_check', status: 'completed' });

      // Step 2: Create return record
      const return = await this.createReturnWithValidation(request);
      workflow.returnId = return.id;
      workflow.steps.push({ step: 'return_created', status: 'completed', returnId: return.id });

      // Step 3: Generate return label
      const label = await this.generateAndSendLabel(return.id, request.shipping_address);
      workflow.steps.push({ step: 'label_generated', status: 'completed', trackingNumber: label.tracking_number });

      // Step 4: Start Temporal workflow for automation
      const workflowHandle = await this.startReturnWorkflow(return.id);
      workflow.steps.push({ step: 'workflow_started', status: 'completed', workflowId: workflowHandle.workflowId });

      // Step 5: Update support systems
      await this.updateSupportSystems(return.id, request.ticket_id);
      workflow.steps.push({ step: 'support_updated', status: 'completed' });

      // Step 6: Set up tracking
      await this.setupReturnTracking(return.id, label.tracking_number);
      workflow.steps.push({ step: 'tracking_setup', status: 'completed' });

      return {
        success: true,
        returnId: return.id,
        rma: return.rma,
        trackingNumber: label.tracking_number,
        workflow: workflow
      };

    } catch (error) {
      workflow.errors.push({
        step: workflow.steps.length,
        error: error.message,
        timestamp: new Date()
      });

      // Attempt to clean up partial return
      if (workflow.returnId) {
        await this.client.returns.update(workflow.returnId, { 
          status: 'ERROR',
          error_message: error.message 
        });
      }

      throw error;
    }
  }

  async checkReturnEligibility(orderId) {
    const order = await this.client.orders.get(orderId);
    const daysSincePurchase = Math.floor((Date.now() - new Date(order.created_at)) / (1000 * 60 * 60 * 24));
    
    if (daysSincePurchase > 30) {
      return { eligible: false, reason: 'Outside 30-day return window' };
    }
    
    if (order.status === 'RETURNED') {
      return { eligible: false, reason: 'Order already returned' };
    }
    
    return { eligible: true };
  }

  async createReturnWithValidation(request) {
    // Validate all items are from the same order
    const order = await this.client.orders.get(request.order_id);
    const validSkus = order.items.map(item => item.sku);
    
    for (const item of request.items) {
      if (!validSkus.includes(item.sku)) {
        throw new Error(`SKU ${item.sku} not found in order ${request.order_id}`);
      }
    }

    // Create return with enriched data
    const returnData = {
      order_id: request.order_id,
      customer_email: order.customer_email,
      items: request.items,
      status: 'NEW',
      type: request.return_type,
      reason_code: request.reason_code,
      customer_notes: request.notes,
      metadata: {
        ip_address: request.ip_address,
        user_agent: request.user_agent,
        created_via: 'api'
      }
    };

    return await this.client.returns.create(returnData);
  }

  async generateAndSendLabel(returnId, customerAddress) {
    const label = await this.client.shipping.createReturnLabel({
      return_id: returnId,
      from_address: customerAddress,
      carrier: this.selectOptimalCarrier(customerAddress),
      service: 'GROUND',
      insurance: true,
      reference_1: returnId
    });

    // Send label via multiple channels
    await Promise.all([
      this.client.notifications.email({
        return_id: returnId,
        template: 'return_label',
        attachments: [label]
      }),
      this.client.notifications.sms({
        return_id: returnId,
        message: `Your return label is ready. Tracking: ${label.tracking_number}`
      })
    ]);

    return label;
  }

  async startReturnWorkflow(returnId) {
    return await this.temporal.workflow.start('returnApprovedWorkflow', {
      taskQueue: 'returns-automation',
      workflowId: `return-${returnId}`,
      args: [{ returnId }]
    });
  }

  async updateSupportSystems(returnId, ticketId) {
    if (ticketId) {
      // Update Zendesk/Gorgias
      await this.client.support.updateTicket(ticketId, {
        status: 'pending',
        tags: ['return_initiated'],
        custom_fields: {
          return_id: returnId
        }
      });
    }
  }

  async setupReturnTracking(returnId, trackingNumber) {
    // Set up webhook for tracking updates
    await this.client.webhooks.create({
      url: `${process.env.WEBHOOK_URL}/returns/${returnId}/tracking`,
      events: ['tracking.updated'],
      filters: {
        tracking_number: trackingNumber
      }
    });

    // Register with Aftership or similar
    await this.client.tracking.create({
      tracking_number: trackingNumber,
      carrier: 'auto-detect',
      reference: returnId,
      notify_customer: true
    });
  }

  selectOptimalCarrier(address) {
    // Logic to select best carrier based on location
    if (address.country === 'US') {
      return address.state === 'HI' || address.state === 'AK' ? 'usps' : 'fedex';
    }
    return 'ups';
  }
}

// Usage example
const returnsService = new ReturnsManagementService(process.env.STATESET_API_KEY);

// Express.js endpoint
app.post('/api/returns', async (req, res) => {
  try {
    const result = await returnsService.processReturnRequest(req.body);
    res.json(result);
  } catch (error) {
    res.status(400).json({ 
      error: error.message,
      code: error.code || 'RETURN_FAILED' 
    });
  }
});

Error Handling and Recovery

class ReturnErrorHandler {
  static async handleReturnError(error, context) {
    const errorHandlers = {
      'RATE_LIMIT': this.handleRateLimit,
      'INVALID_ORDER': this.handleInvalidOrder,
      'SHIPPING_FAILED': this.handleShippingFailure,
      'REFUND_FAILED': this.handleRefundFailure
    };

    const handler = errorHandlers[error.code] || this.handleGenericError;
    return await handler(error, context);
  }

  static async handleRateLimit(error, context) {
    // Implement exponential backoff
    const retryAfter = error.retryAfter || 60;
    await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
    return { retry: true, delay: retryAfter };
  }

  static async handleShippingFailure(error, context) {
    // Try alternative carrier
    const alternativeCarriers = ['ups', 'usps', 'fedex'];
    const failedCarrier = context.carrier;
    const nextCarrier = alternativeCarriers.find(c => c !== failedCarrier);
    
    return { 
      retry: true, 
      overrides: { carrier: nextCarrier } 
    };
  }

  static async handleRefundFailure(error, context) {
    // Queue for manual review
    await context.client.tasks.create({
      type: 'manual_refund_review',
      priority: 'high',
      data: {
        return_id: context.returnId,
        error: error.message,
        attempted_at: new Date()
      }
    });
    
    return { retry: false, escalate: true };
  }
}

Monitoring and Analytics

class ReturnsAnalytics {
  constructor(client) {
    this.client = client;
  }

  async trackReturnMetrics(returnId, event, metadata = {}) {
    await this.client.analytics.track({
      event: `return.${event}`,
      properties: {
        return_id: returnId,
        timestamp: new Date(),
        ...metadata
      }
    });
  }

  async getReturnInsights(timeframe = '30d') {
    const metrics = await this.client.analytics.query({
      metrics: [
        'returns.total',
        'returns.by_reason',
        'returns.processing_time',
        'returns.refund_amount'
      ],
      timeframe,
      groupBy: ['reason_code', 'product_category']
    });

    return this.generateInsights(metrics);
  }

  generateInsights(metrics) {
    return {
      summary: {
        total_returns: metrics.returns.total,
        avg_processing_time: metrics.returns.processing_time.avg,
        total_refunded: metrics.returns.refund_amount.sum
      },
      top_reasons: metrics.returns.by_reason.slice(0, 5),
      recommendations: this.generateRecommendations(metrics)
    };
  }
}

Best Practices

  1. Always validate return eligibility before creating a return
  2. Use idempotency keys to prevent duplicate returns
  3. Implement proper error handling with retry logic
  4. Track all return events for analytics and debugging
  5. Automate refunds for qualified returns to improve customer satisfaction
  6. Integrate with your support platform for seamless customer service
  7. Monitor return patterns to identify product issues
  8. Use webhooks for real-time status updates

Troubleshooting

Support

For additional help with returns management: