Request to "Waive Refund" for merchants who can't use Exchange API

Hi Shopify Community,

My question is about helping Merchants who use Returns Apps to facilitate Exchanges, but can’t use the Exchange API due to incompatibilities with downstream 3PLs.

Some context: I work at Frate Returns, we help merchants process returns & exchanges. We always recommend that merchants use the Exchange API (i.e., using the returnCreate API to pass exchangeLineItems), but it is quite common that throughout the testing process, we realize that their downstream 3PLs are not capable of ingesting new FulfillmentOrders on Orders which they’ve already seen, so this makes it not practical to use the Exchange API, and we end up creating exchanges via zero-cost-DraftOrders. This is not ideal, but it’s a necessary workaround given that not all 3PLs can work with the Exchange API.

So, back to my question: for Merchants who can’t use the Exchange API, here is the return lifecycle:

  1. We call returnCreate with returnLineItems
  2. We call draftOrderCreate with the exchange line items
  3. Later on, we call returnProcess with the returnLineItems, and NO financialTransfer because we gave the customer an exchange via the DraftOrder

The problem with this is that Shopify thinks that a refund is owed on the return because it is not aware of the fact that we handled the exchange, and this causes much confusion for our customers. For example, this UX will show, when really no refund is owed because we handled the exchange via the DraftOrder:

So, if you made it this far, my goal is to support exchange via the external DraftOrder method, AND be able to process the original return WITHOUT any financial transfer AND have Shopify avoid telling the merchant that they owe a refund. The practical problem (which we have already seen) is that CX agents are mindlessly refunding customers here while they also are shipping them an exchange, losing these merchants unnecessary money!

Let me know if you have any questions or want me to clarify anything I’m talking about here, I know it’s quite complex, but I think it’s worth solving because I’m sure other returns apps face the same issue.

Cheers,
Jake

1 Like

Thanks for those details @Jake_Goodman. I read until the end :slight_smile:

This is a really good call out, especially as merchants are losing money when the two systems (your return app and the 3pl) aren’t using the same process for managing returns and exchanges.

The blocker you are running in to is known, and mentioned in the migration guide here. And I see this is what you have implemented.

When you are running in to this, have you checked with the 3PL’s and other apps to see if it’s possible for them to migrate to supporting the new exchanges? Considering the merchant experience that seems like a better solution.

Although issuing refunds to the original payment method on a return will continue to work with returnRefund or refundCreate, returnRefund will be a legacy API, and using refundCreate for returns causes undesired effects for certain cases (example: risk of refunding the wrong item when there are multiple quantities of the same item on an order). The best practice is to integrate with returnProcess for this use case as well.

When we onboard new customers, we always check with their 3PL to see if they are willing to migrate to supporting the new exchange method. The reality is, since we onboard many customers per month, it’s simply not feasible to get buy in from EVERY 3PL to migrate their system to support the new method. In practice, I’ve found that only about 20-30% of 3PLs are willing to change, the rest are stuck in their ways and do not want to put in new work. I do my best at working with them, but there is only so much I can do.

So, I’ve basically accepted the reality that for the foreseeable future, returns apps have to accept the fact that not every 3PL will support exchange API, so we as the return app developers have to have an answer for these archaic 3PLs who only accept new Orders as the entry point for exchanges, which leaves us with this DraftOrder solution I described above.

What do you think about that? And is the answer to the merchants who experience this problem just: “hey, this is unfortunately what you are going to have to live with as long as you use this 3PL, if you want this issue fixed you will have to switch to a more modern 3PL who is updated on Shopify’s FulfillmentOrder flow?”

Thanks,
Jake

Thanks for that Jake. That is a tough situation. Ultimately, if the 3pls are using our API’s they should keep it up to date with the current methods, and on the other hand, that can often be costly and time consuming for them as well.

Do you have context on the API’s that the 3pl’s are using that aren’t leading to these issues?

On the other hand, have you looked in to any order editing or the orderCreate API to see if this would work better than draft orders?

I would like to set up an order and fulfillment service with this scenario to see for myself how this looks, to ensure I can pass on accurate feedback to our team, and see if we can find any alternatives that would work for both parties within the current restraints.

I completely agree that they should keep their systems up to date, the reality is many systems in the Shopify ecosystem don’t, and so the reality for apps like mine is that we need to support both the modern up-to-date players AND the legacy players with out-of-date systems :frowning: .

Fundamentally, it’s just a matter of how the 3PL ingests fulfillment requests:

  1. (Modern) New FulfillmentOrders (as described in this doc)
  2. (Legacy) New Orders, ignoring new FulfillmentOrders on existing Orders (this is why we would have to create a new Order to notify the 3PL about an exchange, and can’t rely on the exchange API because it creates a new FulfillmentOrder on an existing order, and such systems would ignore those!)

I have not explored order editing or orderCreate, because the problem is not the draftOrder itself. The problem is that when 3PL systems are not up to date, creating a return & exchange causes problems because have to put the exchange items on a different order, causing Shoipfy to think that a refund is owed on the return line items, because it doesn’t know that the refund is cancelled out by the exchange items, because they are on a different order.

The steps to replicate this are as described in my first post:

  1. We call returnCreate with returnLineItems
  2. We call draftOrderCreate with the exchange line items
  3. Later on, we call returnProcess with the returnLineItems, and NO financialTransfer because we gave the customer an exchange via the DraftOrder

I understand that this is not a bug per se, the API works exactly as expected. I’m just wondering if you have any recommendations for this workaround when we CAN’T use the exchange API as designed because of a system constraint on the 3PL.

To add to this, we have the exact same problem with issuing store credit in the form of a GiftCard.

I understand that the preferred way to do store credit returns would be to use Shopify native store credit via customer accounts. But, the reality is, many merchants do not use customer accounts or native store credit, and ask their returns apps to do store credit in the form of a Shopify GiftCard. This results in the exact same process of creating a return (with returnCreate), then processing the return (with returnProcess) and not including any financialTransfer (because it was handled separately by creating the gift card), which results in Shopify saying “You owe the customer a refund”, when in reality they do not because it was handled “outside” of the return.

Again, I know this problem is a result of us not doing things the “correct” way. However the main point of my post here is to call out that the “correct” way is simply not feasible to implement across most merchants today (i.e., using the exchange API for exchanges, and the store credit API for store credit), and returns app are generally also maintaining backup workflows (i.e., using draft orders for exchanges, and GiftCards for store credit), and this causes problems for merchants (specifically with Shopify saying “you owe the customer a refund” when they don’t).

Curious for your thoughts.

This is a great explanation and you’ve given me a lot to think about.

I’m going to dig in to this more with our product teams to ensure I get a more clear answer to you.

My initial (personal) thought here is that the level of support you are offering merchants and the empathy you have for the different scenarios within the reality of the ecosystem at the moment is really awesome. I can also imagine this is hard for your support teams as I’m guessing a lot of this is falling on you, when you are in fact trying to just make it work within their current setup. You can only really do it the “correct” way when all the other tools are using the correct methods as well.

Exploring alternative workarounds, I would consider testing by adding an edit order step to add a custom line item on the original order. Call it exchangeadjustment, or even call it the same name of the exchanged item, with the same value of the returned item. That would bring the original order total back to the initial value (no refund owed) without affecting the inventory since the actual exchange inventory will be accounted for in the newly created order.

Ok, thanks for the response @KyleG-Shopify , I will add that workaround idea to our backlog and let you know if/when we get to building it whether or not it works.

I would also love to hear back from you if the product team(s) has anything to add.

Appreciate you seeing the care we’re putting into this to solve merchant problems!

Cheers,
Jake

Hey Jake,

I’ve gone ahead and tested the workaround myself so I can confirm it works to resolve the “refund owed” issue when processing exchanges via draft orders or gift cards.

After processing a return without issuing a refund (which creates the “amount owed” state), you can add a tax-exempt custom line item matching the returned item’s value. This brings the order balance back to $0 and changes the status from showing a refund owed to “Paid.”

Here’s what this looks like in practice:

Before applying the workaround:


The order shows $75 owed to the customer, even though you’ve already provided the exchange through other means.

After applying the workaround:


The order now shows as “Paid” with a $0 balance, and the custom line item documents that an exchange was provided.

To implement this yourself, here are the mutations you’d use:

1. Begin the order edit session

mutation beginEdit($orderId: ID!) {
  orderEditBegin(id: $orderId) {
    calculatedOrder {
      id
    }
    userErrors {
      field
      message
    }
  }
}

Variables:

{
  "orderId": "gid://shopify/Order/1234567890"
}

2. Add the tax-exempt custom line item

mutation addExchangeItem($id: ID!, $title: String!, $price: MoneyInput!, $quantity: Int!) {
  orderEditAddCustomItem(
    id: $id
    title: $title
    price: $price
    quantity: $quantity
    taxable: false
  ) {
    calculatedOrder {
      id
      totalPriceSet {
        shopMoney {
          amount
        }
      }
    }
    userErrors {
      field
      message
    }
  }
}

Variables:

{
  "id": "gid://shopify/CalculatedOrder/123456787",
  "title": "exchanged item",
  "price": {
    "amount": 75.00,
    "currencyCode": "USD"
  },
  "quantity": 1
}

Key detail: Setting taxable: false is critical. Without it, tax gets added to the custom line item and you end up with a small outstanding balance instead of $0.

3. Commit the order edit

mutation commitEdit($id: ID!) {
  orderEditCommit(id: $id) {
    order {
      id
      totalOutstandingSet {
        shopMoney {
          amount
        }
      }
    }
    userErrors {
      field
      message
    }
  }
}

Variables:

{
  "id": "gid://shopify/CalculatedOrder/1234567890"
}

4. Add documentation to the order

mutation addNote($input: OrderInput!) {
  orderUpdate(input: $input) {
    order {
      id
      note
    }
    userErrors {
      field
      message
    }
  }
}

Variables:

{
  "input": {
    "id": "gid://shopify/Order/1234567890",
    "note": "EXCHANGE PROCESSED: Customer returned 1 Coffee Maker. Exchange item fulfilled via separate order #1590. No refund issued - exchange item added to this order to balance accounts."
  }
}

This approach works, but has some trade-offs:

  • You need to manually calculate and set the exact returned item amount
  • The taxable: false setting is required for a perfect $0 balance, which may not accurately reflect the tax treatment of the actual exchange
  • The custom line item doesn’t represent real inventory, just an accounting adjustment
  • Staff need clear documentation (via order notes) to understand what happened

For context, this workaround addresses the gap in the API where there’s no native way to mark that a return was satisfied through external means (draft order exchange or gift card). I’ve captured your feedback on this and passed it along to the team and will follow up once I’ve heard from them.

Cheers!