Articles in this section

Shopify - NetSuite: Returns, exchanges, and refunds integration template

The Shopify - NetSuite Returns, Exchanges, and Refunds Template automates and synchronizes all post-purchase operations, including returns, exchanges, refunds, and related financial transactions, between Shopify (Online and POS) and NetSuite. It provides merchants with an end-to-end automated workflow that ensures accuracy, transparency, and efficiency across sales channels. This solution is offered in partnership with ZangaBee.

Shopify is a leading e-commerce platform that supports both online and in-store sales (POS), enabling merchants to manage orders, customers, and payments seamlessly. NetSuite, a cloud-based ERP system, handles core business functions such as accounting, inventory, and order management.

This specialized template focuses on post-purchase automation, specifically designed for managing returns, exchanges, and refunds from Shopify back into NetSuite.

By leveraging this template, businesses can automate the creation of Return authorizations, Credit memos, and Customer refunds, as well as manage exchange orders and inventory adjustments in real-time. The integration ensures that both Shopify and NetSuite remain in sync, reducing manual effort and improving the customer experience.

This template allows merchants to efficiently manage complex return workflows, such as multi-item exchanges, downsell refunds, and POS returns, all within a unified, automated framework powered by Celigo.

Supported integration flows

The following built-in flows sync from the specified source data to the corresponding destination.

Flows.png

Shopify returns to NetSuite RMA, exchange, and refund (Add)

When a return or exchange is initiated in Shopify,  either by a customer through the online store or at a physical POS location, the integration automates the end-to-end process by creating Return Authorizations (RMAs), exchange orders, and refund details in NetSuite. This flow manages both simple returns and complex exchanges. It supports even, downsell, and upsell exchanges, automatically generating necessary financial and fulfillment records.

POS returns: Automatically creates a return authorization, item receipt, and credit memo in NetSuite when the return is marked Closed in Shopify POS, eliminating manual intervention.

Web returns: Creates the RMA in NetSuite. If the return is processed in Shopify, the RMA item is marked received in NetSuite.

Exchange Scenarios:

  • Even exchange: Applies the credit memo to the new invoice, resulting in a zero balance.

  • Downsell exchange: Calculates the price difference and creates a customer refund record for the amount owed to the customer.

  • Upsell exchange: Automatically generates a customer payment record in NetSuite for the additional amount owed.

Unverified returns: Handles Shopify POS returns without an existing sales order by creating standalone RMAs, item receipts, and credit memos.

  • Flow direction: Shopify > NetSuite.

  • Creates: Return authorization, Credit memo, Exchange sales order.

  • Key benefits: Unifies online and in-store workflows, ensures accurate refund and return records, and streamlines exchange processing.

NetSuite credit memo to NetSuite customer refund (Add)

When a credit memo is created in NetSuite from the return flow, this integration automatically generates a corresponding customer refund record. It links the credit memo to the appropriate customer and refund account, ensuring that refunds initiated in Shopify are fully reconciled in NetSuite. It ensures that every refund issued in Shopify is synced in NetSuite with the correct credit memo and customer record.

Multi-gateway support: Maps multiple Shopify payment methods (Shopify Payments, PayPal, etc.) to their corresponding refund methods in NetSuite using static lookup tables.

Gift card refunds: Instead of creating a customer refund, the credit memo is updated to adjust the gift card balance.

Store credit refunds: For store credit transactions, the credit memo updates the customer’s account balance instead of generating a cash refund.

  • Flow direction: NetSuite > NetSuite.

  • Creates: Customer refund.

  • Key benefits: Automates refund creation, maintains financial accuracy, and improves accounting efficiency.

NetSuite sales order to Shopify fulfillment order status update

When a sales order in NetSuite is placed on hold or released, this flow updates the corresponding Shopify fulfillment order status. It keeps both systems in sync, ensuring accurate order tracking and fulfillment visibility.

  • Flow direction: NetSuite > Shopify.

  • Updates: Fulfillment Order status.

  • Key Benefits: Provides real-time visibility for fulfillment teams, minimizes order delays, and improves customer communication.

NetSuite saved searches

The integration flows use predefined saved searches in NetSuite to identify and extract relevant transaction data for processing. These saved searches ensure that only valid and actionable records are retrieved during each flow run.

All eTail sales orders

This saved search is used in the Shopify returns to NetSuite RMA, exchange, and refund add flow. This saved search identifies recent Shopify-related sales orders in NetSuite.

  • It includes only those orders created within the last 30 days and mapped to a Shopify order ID.

  • The retrieved data helps validate order relationships during return and exchange processing.

Filters:

All_Etail_Sales_Order_1.png

Columns:

All_Etail_Sales_Order.png

Script:

const salesOrderSearchFilters: SavedSearchFilters = [
    ['type', 'anyof', 'SalesOrd'],
    'AND',
    ['mainline', 'is', 'T'],
    'AND',
    ['taxline', 'is', 'F'],
    'AND',
    ['custbody_celigo_etail_order_id', 'isnotempty', ''],
    'AND',
    ['datecreated', 'onorafter', 'thirtydaysago'],
];


const salesOrderSearchColCustomerMainInternalId = search.createColumn({ name: 'internalid', join: 'customermain' });
const salesOrderSearchColLocation = search.createColumn({ name: 'location' });
const salesOrderSearchColInventoryLocationInternalId = search.createColumn({ name: 'internalid', join: 'location' });


const salesOrderSearch = search.create({
    type: 'salesorder',
    filters: salesOrderSearchFilters,
    columns: [
        salesOrderSearchColCustomerMainInternalId,
        salesOrderSearchColLocation,
        salesOrderSearchColInventoryLocationInternalId,
    ],
});
const salesOrderSearchPagedData = salesOrderSearch.runPaged({ pageSize: 1000 });
for (let i = 0; i < salesOrderSearchPagedData.pageRanges.length; i++) {
    const salesOrderSearchPage = salesOrderSearchPagedData.fetch({ index: i });
    salesOrderSearchPage.data.forEach((result: search.Result): void => {
        const customerMainInternalId = result.getValue(salesOrderSearchColCustomerMainInternalId);
        const location = result.getValue(salesOrderSearchColLocation);
        const inventoryLocationInternalId = result.getValue(salesOrderSearchColInventoryLocationInternalId);


        // ...
    });
}

Open credit memo

This saved search supports the Shopify returns to NetSuite RMA, exchange and refund add flow. It fetches all open credit memo records in NetSuite that are associated with Shopify orders.

  • The search ensures only credit memos with a valid Shopify order ID and an open status are included.

  • These results are used to match refund transactions and maintain financial reconciliation between the two systems.

Filters:

Open_Credit_Memo.png

Columns:

Open_Credit_Memo_2.png

Script:

const creditMemoSearchFilters: SavedSearchFilters = [
    ['type', 'anyof', 'CustCred'],
    'AND',
    ['mainline', 'is', 'T'],
    'AND',
    ['taxline', 'is', 'F'],
    'AND',
    ['status', 'anyof', 'CustCred:A'],
    'AND',
    ['custbody_celigo_etail_order_id', 'isnotempty', ''],
];


const creditMemoSearchColInternalId = search.createColumn({ name: 'internalid' });


const creditMemoSearch = search.create({
    type: 'creditmemo',
    filters: creditMemoSearchFilters,
    columns: [
        creditMemoSearchColInternalId,
    ],
});
onst creditMemoSearchPagedData = creditMemoSearch.runPaged({ pageSize: 1000 });
for (let i = 0; i < creditMemoSearchPagedData.pageRanges.length; i++) {
    const creditMemoSearchPage = creditMemoSearchPagedData.fetch({ index: i });
    creditMemoSearchPage.data.forEach((result: search.Result): void => {
        const internalId = result.getValue(creditMemoSearchColInternalId);


        // ...
    });
}

All eTail customers

This saved search is used in the Shopify returns to NetSuite RMA, exchange, and refund add flow. It retrieves all active NetSuite customers that are linked to Shopify through Celigo’s customer mapping records.

  • Filters include inactive customers being excluded and only those with a Shopify ID.

  • The saved search returns essential customer identifiers, such as internal IDs, to ensure accurate mapping between Shopify and NetSuite.

Filters

All_Etail_Customers.png

Columns:

All_Etail_Customers_2.png

Script:

const customerSearchFilters: SavedSearchFilters = [
    ['isinactive', 'is', 'F'],
    'AND',
    ['custrecord_celigo_shpf_scim_nsid.custrecord_celigo_shpf_scim_shpfid', 'isnotempty', ''],
];


const customerSearchColInternalId = search.createColumn({ name: 'internalid' });


const customerSearch = search.create({
    type: 'customer',
    filters: customerSearchFilters,
    columns: [
        customerSearchColInternalId,
    ],
});
const customerSearchPagedData = customerSearch.runPaged({ pageSize: 1000 });
for (let i = 0; i < customerSearchPagedData.pageRanges.length; i++) {
    const customerSearchPage = customerSearchPagedData.fetch({ index: i });
    customerSearchPage.data.forEach((result: search.Result): void => {
        const internalId = result.getValue(customerSearchColInternalId);


        // ...
    });
}

Get Shopify exchanged on-hold orders

This saved search supports the NetSuite sales order to Shopify fulfillment order status update flow. It identifies sales orders in NetSuite that are marked as on hold so that the flow can trigger the corresponding release update in Shopify.

  • Only sales orders in a “pending approval” status are included, ensuring that hold releases are applied correctly.

Filters:

Get_Shopify_exchanged_on-hold_orders_.png

Columns:

Get_Shopify_exchanged_on-hold_orders_2.png

Script:

const transactionSearchFilters: SavedSearchFilters = [
    ['mainline', 'is', 'T'],
    'AND',
    ['taxline', 'is', 'F'],
    'AND',
    ['cogs', 'is', 'F'],
    'AND',
    ['custbody_shopify_etail_onhold', 'is', 'T'],
    'AND',
    ['status', 'anyof', 'SalesOrd:B'],
];


const transactionSearchColETailOrderId = search.createColumn({ name: 'custbody_celigo_etail_order_id' });
const transactionSearchColPOCheckNumber = search.createColumn({ name: 'otherrefnum' });
const transactionSearchColTranId = search.createColumn({ name: 'tranid' });


const transactionSearch = search.create({
    type: 'transaction',
    filters: transactionSearchFilters,
    columns: [
        transactionSearchColETailOrderId,
        transactionSearchColPOCheckNumber,
        transactionSearchColTranId,
    ],
});


// // NOTE: Search.run() is limited to 4,000 results
// transactionSearch.run().each((result: search.Result): boolean => {
//     // ...
//
//     return true;
// });


const transactionSearchPagedData = transactionSearch.runPaged({ pageSize: 1000 });
for (let i = 0; i < transactionSearchPagedData.pageRanges.length; i++) {
    const transactionSearchPage = transactionSearchPagedData.fetch({ index: i });
    transactionSearchPage.data.forEach((result: search.Result): void => {
        const eTailOrderId = result.getValue(transactionSearchColETailOrderId);
        const poCheckNumber = result.getValue(transactionSearchColPOCheckNumber);
        const tranId = result.getValue(transactionSearchColTranId);


        // ...
    });
}

Get NetSuite sales orders

This saved search is used in the Shopify returns to NetSuite RMA, exchange, and refund add flow. It retrieves NetSuite sales orders that are associated with Shopify transactions.

  • The search includes only mainline records and excludes tax lines to ensure performance and data accuracy.

  • This saved search is used to validate and link Shopify order data with corresponding NetSuite sales orders during return and exchange processing.

Script:

Save Search for Flow DF1
step: Get NetSuite sales orders 



const salesOrderSearchFilters: SavedSearchFilters = [
    ['type', 'anyof', 'SalesOrd'],
    'AND',
    ['mainline', 'is', 'T'],
    'AND',
    ['taxline', 'is', 'F'],
    'AND',
    ['custbody_celigo_etail_order_id', 'isnotempty', ''],
];
const salesOrderSearchColInternalId = search.createColumn({ name: 'internalid' });
const salesOrderSearch = search.create({
    type: 'salesorder',
    filters: salesOrderSearchFilters,
    columns: [
        salesOrderSearchColInternalId,
    ],
});
// // NOTE: Search.run() is limited to 4,000 results
// salesOrderSearch.run().each((result: search.Result): boolean => {
//     // ...
//
//     return true;
// });
const salesOrderSearchPagedData = salesOrderSearch.runPaged({ pageSize: 1000 });
for (let i = 0; i < salesOrderSearchPagedData.pageRanges.length; i++) {
    const salesOrderSearchPage = salesOrderSearchPagedData.fetch({ index: i });
    salesOrderSearchPage.data.forEach((result: search.Result): void => {
        const internalId = result.getValue(salesOrderSearchColInternalId);
        // ...
    });
}
interface NestedArray extends Array> { }
type SavedSearchFilters = string | NestedArray;

Import exchange transactions

Payments for exchanges in Shopify are tied to the original order, and the integration is equipped to handle this behavior. The template captures and associates exchange-related payments with their corresponding NetSuite exchange orders, ensuring accurate financial reconciliation and complete visibility across systems, without requiring additional customization.

You can use the below script to automatically identify and link Shopify exchange orders to their corresponding transaction records. It ensures that each transaction is correctly reassigned from the original order to the appropriate exchange sales order, maintaining accurate financial tracking and auditability within NetSuite. Also, ensure the following custom fields exist and are configured:

  • Celigo eTail Order Transactions Record:

    • custrecord_etail_tran_processed This marks the transaction as processed once linked to an exchange order.

    • custrecord_etail_tran_timestamp This stores the timestamp of the transaction from Shopify. In the Celigo transaction import flow, add a mapping to populate custrecord_etail_tran_timestamp with the Shopify transaction timestamp when creating transaction records.

  • Sales Order form:

    • custbody_etail_ord_timestamp This stores the return creation time from Shopify for exchange sales orders. In the Celigo Return/Exchange flow, add a mapping to populate custbody_etail_ord_timestamp with Shopify’s return creation timestamp when an exchange sales order is created.

/**
 * @NApiVersion 2.1
 * @NScriptType UserEventScript
 */
define(['N/record', 'N/search', 'N/log'], (record, search, log) => {

  const CUSTOM_TRANSACTION_RECORD = 'customrecord_celigo_etail_odr_trnsaction';
  const FIELD_TRANSACTION_TIMESTAMP = 'custrecord_etail_tran_timestamp';
  const FIELD_LINKED_SO = 'custrecord_celigo_etail_orderid';
  const FIELD_PROCESSED_FLAG = 'custrecord_etail_tran_processed';
  const FIELD_ETAIL_ORDER_ID = 'custbody_celigo_etail_order_id';
  const FIELD_RETURN_CREATION_TS = 'custbody_etail_ord_timestamp';

  
  function parseDDMMYYYY(str) {
    if (!str) return null;
    try {
      const [datePart, timePart, ampmRaw] = str.trim().split(/\s+/);
      const [dd, mm, yyyy] = datePart.split('/').map(n => parseInt(n, 10));
      const [hh, min] = timePart.split(':').map(n => parseInt(n, 10));
      const ampm = (ampmRaw || '').toLowerCase();

      let hour = hh;
      if (ampm === 'pm' && hour < 12) hour += 12;
      if (ampm === 'am' && hour === 12) hour = 0;

      // Construct date with numbers only
      return new Date(yyyy, mm - 1, dd, hour, min, 0);
    } catch (e) {
      log.error('parseDDMMYYYY error', e);
      return null;
    }
  }

  /** Format → MM/DD/YYYY hh:mm am/pm for search filter */
  function fmt(date) {
    const pad = n => (n < 10 ? '0' + n : n);
    const M = pad(date.getMonth() + 1);
    const D = pad(date.getDate());
    const Y = date.getFullYear();
    let h = date.getHours();
    const m = pad(date.getMinutes());
    const ampm = h >= 12 ? 'pm' : 'am';
    h = h % 12 || 12;
    return `${D}/${M}/${Y} ${h}:${m} ${ampm}`;
  }

  function afterSubmit(context) {
    try {
      if (![context.UserEventType.CREATE, context.UserEventType.EDIT].includes(context.type)) return;

      const recId = context.newRecord.id;
      const txnRec = record.load({ type: CUSTOM_TRANSACTION_RECORD, id: recId });

      const tsText = txnRec.getText(FIELD_TRANSACTION_TIMESTAMP);
      const linkedSO = txnRec.getValue(FIELD_LINKED_SO);
      if (!tsText || !linkedSO) return log.debug('Skip', 'Missing timestamp or linked SO');

      const soRec = record.load({ type: record.Type.SALES_ORDER, id: linkedSO });
      const origEtail = soRec.getValue(FIELD_ETAIL_ORDER_ID);
      if (!origEtail) return log.debug('Skip', 'Missing eTail ID');

      const ts = parseDDMMYYYY(tsText);
      if (!ts) return log.error('Bad timestamp', tsText);

      const minus2 = new Date(ts.getTime() - 2 * 60000);
      const plus2  = new Date(ts.getTime() + 2 * 60000);
      const startStr = fmt(minus2);
      const endStr   = fmt(plus2);

      log.debug('Timestamp Window', `${startStr} - ${endStr}`);

      const soSearch = search.create({
        type: search.Type.SALES_ORDER,
        filters: [
          ['mainline','is','T'],
          'AND',
          [FIELD_ETAIL_ORDER_ID,'startswith', origEtail + '-'],
          'AND',
          [FIELD_RETURN_CREATION_TS,'within', startStr, endStr]
        ],
        columns:['internalid', FIELD_ETAIL_ORDER_ID, FIELD_RETURN_CREATION_TS]
      });

      const res = soSearch.run().getRange({ start: 0, end: 1 });
      if (!res || !res.length) return log.debug('No Match', 'No SO in window');

      const exchSO = res[0].getValue('internalid');
      record.submitFields({
        type: CUSTOM_TRANSACTION_RECORD,
        id: recId,
        values: {
          [FIELD_LINKED_SO]: exchSO,
          [FIELD_PROCESSED_FLAG]: true
        },
        options:{ enableSourcing:false, ignoreMandatoryFields:true }
      });

      log.audit('Updated', `Linked to Exchange SO ${exchSO}`);

    } catch (e) {
      log.error('Error in afterSubmit', e);
    }
  }

  return { afterSubmit };
});

Install the template

Keep your Shopify and NetSuite account credentials ready:

  1. Install the NetSuite integrator.io bundle (bundle ID: 81289).

    Note

    If you are using the Shopify-NetSuite Integration Application, this bundle will already be available in your NetSuite account.

    1. Sign in to your NetSuite account as an administrator.

    2. Navigate to Setup > Customization > SuiteBundler > Search and install bundles.

    3. Enter and search the Bundle ID (81289).

    4. Click Install and wait for the installation to complete successfully.

  2. Navigate to Celigo integrator.io > Marketplace.

  3. Search and select the Shopify – NetSuite Returns, Exchanges, and Refunds template.

  4. Click Install.

  5. Review the template’s contents and ReadMe, and click Install now to begin.

  6. Set up a connection to Celigo.

  7. Set up a connection to the Shopify GraphQL connection.

  8. Set up a connection to NetSuite.

  9. Install the Integrator Bundle.

  10. Install the Integrator SuiteApp.

  11. Install the resources from the template zip.

  12. Install the template.

Note that all of your flows are disabled when first installed. After configuring the flows, you can enable them in your Flows dashboard or in Flow Builder with the Off/On toggle button. Then run each flow.