$0 / 100% discounted exchange items do not create a fulfillmentOrder after returnProcess

Summary

When calling returnProcess on a return that contains a fully discounted ($0.00) exchange item, no FulfillmentOrder is created. This means the exchange item gets stuck in an “Unreleased exchange items” state indefinitely and cannot be fulfilled programmatically.

Steps to reproduce

  1. Create two returns with an exchange, one priced at $10 and one at $0 (100% discount applied)
  2. Call returnProcess on the return
  3. Query fulfillment orders on the order

For the $10 exchange item:

  • :white_check_mark: A FulfillmentOrder is created as documented
  • :white_check_mark: We can call fulfillmentOrderReleaseHold to release it and proceed to fulfillment

For the $0 exchange item:

  • :cross_mark: No FulfillmentOrder is created after returnProcess
  • :cross_mark: There is nothing to call fulfillmentOrderReleaseHold on
  • :cross_mark: The item shows as “Unreleased exchange items” in the Shopify Admin UI indefinitely

Additional observation

If we manually click “Process return” in the Shopify Admin UI for the $0 exchange, it does release the fulfillment correctly. So the UI path works but the API path does not produce the same result.

Questions

  1. Is this intentional behaviour? The docs state that returnProcess creates FulfillmentOrder objects for exchange items, is a $0 item a known exception?
  2. Is there a workaround to programmatically release/fulfill a $0 exchange item via the API when no FulfillmentOrder is created?

This is causing real customer impact as orders with $0 exchange items are stuck and not being fulfilled automatically. Any guidance would be appreciated.

Hey @Rohan-Frate thanks for the clear write up, it made this easy for me to look into!

The returnProcess docs state that the mutation “creates FulfillmentOrder objects for exchange items” without any exception for zero-value items. The migration guide also draws a clear distinction between FulfillmentOrder creation and hold placement. Even or refundable exchanges should still get FulfillmentOrders created, they just won’t be placed on hold. So the expected behavior for your $0 exchange is that a FulfillmentOrder is created (with no hold), not that it’s skipped entirely.

I tested this on a dev store by creating two returns with exchanges via the API, one at full price and one with a 100% discount ($0). Both produced a FulfillmentOrder after calling returnProcess, which is the expected behavior. So I wasn’t able to reproduce the issue on my end, but that doesn’t rule out something specific to your store or flow.

To dig into why it’s behaving differently for you, could you reproduce the $0 exchange case and capture the x-request-id from the response headers for each step? Ideally I’d want the x-request-id from the returnCreate call, the returnProcess call, and the fulfillmentOrders query where you see no FulfillmentOrder.

With those I can trace the full sequence internally and get this in front of the right people if need be. Thanks!

Hey @Donal-Shopify!

Thanks for looking into this.

Sure thing, here are the request ID’s from the same flow recreated on my local development shop.

  1. returnCreate:

"x-request-id": "47a2923b-fcef-4fd5-bcb8-dbbf6ea11f1a-1776270151"

  1. returnProcess:

"x-request-id": "46a3dd58-1b43-4491-a0d6-20f459e9886e-1776270180"

  1. order.fulfillmentOrders query which returns no fulfillmentOrders for the unprocessed exchange item:

"x-request-id": "4391db3b-3cec-431a-9154-7aa84eae9387-1776270181"

Let me know what you find!

Hey @Donal-Shopify ,

To clarify, another nuance in this scenario is that the returned item is also a 100% discounted to begin with. So, there are no transactions. The transaction modal on the order page looks something like this:

Thanks for the request IDs, @Rohan-Frate. I traced through all three and can confirm what you’re seeing.

The returnCreate call correctly set up the exchange with the 100% discount and created reverse fulfillment orders as expected. The issue is in the returnProcess step. For your $0 exchange, the step that creates FulfillmentOrders for exchange items is not being triggered. The return is then auto-closed and the exchange item ends up stuck with no FulfillmentOrder to act on.

Your clarification about the original item also being 100% discounted (zero transactions on the order) was the key detail. That’s what distinguishes your scenario from mine when I tested earlier, and it appears to be why the FulfillmentOrder creation step gets skipped.

I’ve raised this with the relevant team internally along with the trace data from your request IDs. There’s no API workaround I can offer right now since the FulfillmentOrder simply isn’t being created, and you can’t fulfill what doesn’t exist. I’ll follow up here once I hear back from the team.

Thanks @Donal-Shopify ! Please do keep me updated as this is actively affecting merchants.

Hey @Rohan-Frate, I have an update. The team confirmed this is an issue with how returnProcess handles $0 exchanges. When there are no transactions on the order, the internal processing path that normally creates FulfillmentOrders for exchange items gets skipped entirely. Your detail about the original item also being 100% discounted was the key factor, since that’s what produces the zero-transaction condition.

They’ve logged this for a fix. I can’t give you a timeline on that, but there is a workaround you can use in the meantime. Instead of including the exchange items in the return flow, you can split it into two steps:

  1. Process the return via returnCreate and returnProcess without exchange items

  2. Add the exchange item separately using the order editing mutations (orderEditBegin, orderEditAddVariant, orderEditCommit). If the exchange item needs to be $0, apply the discount with orderEditAddLineItemDiscount before committing.

That order edit path creates FulfillmentOrders through a different flow that isn’t affected by the $0 transaction issue. Let me know if you run into anything with that approach. Thanks again for raising this with us!

Hey @Donal-Shopify , thanks for the update and confirmation from the team.

While we wait for the bug fix, I will take a look at the suggested workaround using orderEdit.

Out of curiosity, would this fix come in the next major graphql api version?

Thanks!

hey @Donal-Shopify , how is this handled from the UI?

it seems to be working if we manually click “return process” on the Order page.

Hey @Rohan-Frate, quick update on both.

On the API version question, fixes to existing behaviour like this typically ship to all supported API versions once they merge rather than being held for a new version.

On the UI question, good catch. The Admin “Process return” button invokes a different internal code path that commits the order edit directly, so it sidesteps the check that causes this.

Your best interim path is the orderEditBegin / orderEditAddVariant / orderEditCommit workaround. Process the return without exchange items, then add the exchange as a separate order edit.