SaleAgreements, how does a restocking fee or return shipping fee present?

hey,

in SaleAgreements, how does a return with restocking fee and returnShippingFee present on an order?

Hey @ChrisBradley - thanks for reaching out.

In Admin GraphQL 2026-07, a return appears in Order.agreements as a ReturnAgreement. Its sales connection includes reversal sales for the returned items and one FeeSale for each persisted fee. A return with one restocking fee and one return-shipping fee has two FeeSale records. These fees appear as FeeSale records rather than AdjustmentSale records in this flow.

When a fee is charged, its sale has lineType: FEE, actionType: ORDER, and quantity: null. You can distinguish a live fee through fee.__typename, which is RestockingFee or ReturnShippingFee, and read the charge from totalAmount. Return fees aren’t taxed, so on the original ORDER sale, totalAmount matches the fee’s amountSet.

If a fee is later changed, the sales history is appended rather than edited in place. A price change adds two FeeSale records with actionType: UPDATE, one reversing the previous amount and one recording the new amount. Removing a fee adds a FeeSale with actionType: RETURN. Summing totalAmount across the fee’s sales gives the amount currently charged.

The Fee interface itself only exposes id. To access amountSet, or percentage for a restocking fee, use inline fragments on the concrete RestockingFee and ReturnShippingFee types.

If you only need the fee objects rather than their sales history, restocking fees are also available through ReturnLineItem.restockingFee, while return-shipping fees are available through Return.returnShippingFees.

One caveat is that FeeSale.fee can be null if the underlying fee has been deleted. Once that happens, fee.__typename is also unavailable, so read the type while the fee is still live if you need it for historical reconciliation.

For a standard app, reading the order and FeeSale requires read_orders, while accessing FeeSale.fee also requires read_returns. Without read_returns, querying fee returns an access-denied error.

Relevant documentation:

Let me know if I can clarify anything and I hope this helps!

@Wes-Dev-Shopify

thanks for the detailed information on this, so effectively they come under “FEE” as linetype then we __typename, id and then value?

                        ... on FeeSale {
                            fee {
                                __typename
                                id
                                ... on RestockingFee     { percentage amountSet { presentmentMoney {
                                    amount
                                    currencyCode
                                } } }
                                ... on ReturnShippingFee { amountSet { presentmentMoney {
                                    amount
                                    currencyCode
                                } } }
                            }
                        }

Assume this is the same for Duty?

Also can we pul in name as well, ie:

{ lineItem { id name } }

Hey @ChrisBradley - good questions.

Duties use a similar sales model, but they appear as DutySale records with lineType: DUTY, rather than as FeeSale records:

... on DutySale {
  duty {
    id
    harmonizedSystemCode
    price {
      presentmentMoney {
        amount
        currencyCode
      }
    }
  }
}

Your fee fragment is valid for reading __typename, id, and amountSet. There isn’t a generic value field on the Fee interface. You can also use FeeSale.totalAmount for the amount recorded by the sale.

One distinction is how Shopify models duty amounts. A customs duty is itself a tax, but a Duty can also have separate taxLines allocated to it. duty.price is the customs-duty charge, while DutySale.totalTaxAmount contains any separate tax-line amount and DutySale.totalAmount is the sale total after taxes and discounts. Return fees don’t have these tax allocations, so the original fee sale’s totalAmount matches the fee’s amountSet. DutySale.duty is also non-null, unlike FeeSale.fee, which can become null if the fee is deleted.

lineItem { id name } can’t be selected directly on FeeSale or DutySale; it’s available directly on ProductSale. For a restocking fee, you can associate FeeSale.fee.id with ReturnLineItem.restockingFee.id, then read fulfillmentLineItem.lineItem { id name }. For a duty, query each order line item’s duties { id } and match that ID to DutySale.duty.id. A return-shipping fee belongs to the return itself, so it doesn’t have an individual product line-item association.

DutySale: DutySale - GraphQL Admin
FeeSale: FeeSale - GraphQL Admin
ReturnLineItem: ReturnLineItem - GraphQL Admin
TaxLine: TaxLine - GraphQL Admin

Hope this helps!