Shopify return v/s cancellation

I have a scenario where I need to either cancel line items or create returns for specific line items by obsering the Refund object present in GraphQL, as the Refund object is connected with Return and Order object. However, I haven’t been able to find a way in the GraphQL Admin API to determine whether a refund is associated with a return process or a cancellation process.

Because of this limitation, it becomes difficult for me to correctly classify and import the transaction into my system as either a return or a cancellation.

Could you please provide some guidance or clarification on how this distinction can be identified using the GraphQL Admin API, or suggest an alternative approach to handle this case?

Hi @Chinmay_Sharma! Good news - the API does expose this distinction. The Refund object has a return field that links to the associated Return object when the refund was initiated through a return. If that field is null, the refund came from somewhere else (cancellation, standalone refund, etc.).

For cancellations specifically, check the order’s cancelledAt and cancelReason fields - if cancelledAt is populated, the order was cancelled and any associated refunds are cancellation refunds.

Here’s how you’d query this:

query OrderRefundClassification($orderId: ID!) {
  order(id: $orderId) {
    id
    cancelledAt
    cancelReason
    refunds(first: 10) {
      nodes {
        id
        createdAt
        return {
          id
          name
          status
        }
        refundLineItems(first: 10) {
          nodes {
            lineItem {
              id
              title
            }
            quantity
          }
        }
      }
    }
  }
}

Your classification logic would be:

  • refund.return is not null = return-based refund

  • refund.return is null AND order.cancelledAt is populated = cancellation refund

  • refund.return is null AND order.cancelledAt is null = standalone refund (partial refund without return or cancellation)

Let me know if this helps solve the problem (or if I’ve misunderstood!)

Hi @Donal-Shopify, Thanks for your reply. I was actually trying to figure out a way in the refunds object itself and also the main part that makes it confusing is when cancellation is mixed with return i.e. partial cancellation and return happens on items within the order on Shopify, not necessarily in the same refund object. If you could throw some light on this, It would be great!

Ah, ok! You’re looking for the restockType field on each RefundLineItem - this gives you line-item-level classification which is exactly what you need for mixed scenarios.

query OrderRefundClassification($orderId: ID!) {
  order(id: $orderId) {
    id
    refunds {
      id
      createdAt
      refundLineItems(first: 50) {
        nodes {
          lineItem {
            id
            title
          }
          quantity
          restockType  # Key field: CANCEL, RETURN, NO_RESTOCK
        }
      }
    }
  }
}

The restockType values tell you exactly what happened:

  • CANCEL = unfulfilled items that were cancelled

  • RETURN = fulfilled items that were returned

  • NO_RESTOCK = standalone refund with no inventory impact

So for your scenario where some items are cancelled and others are returned across multiple refunds, you just iterate through each refund’s line items and check the restockType on each one. Works perfectly even when you have multiple refunds on the same order or line items with different restock behaviors.

I tested this with an order where I cancelled 2 unfulfilled items and returned 2 fulfilled items, and the restockType field correctly distinguished between them at the line item level. Much more reliable than trying to infer from refund.return or order.cancelledAt since those work at the refund/order level, not the individual line item level.

I think the NO_RESTOCK type is unclear. I explored this as an option. It could be a refund or cancellation. Please correct me if I am wrong!

@Donal-Shopify To be more specific, if I cancel a few quantities of an item and don’t restock and create a refund for the remaining quantity without restock. It would be wrong to rely on this field, then.

You’re right! NO_RESTOCK is ambiguous on its own. It doesn’t tell you whether it was a cancellation without restock or just a standalone refund.

The missing piece here is the line item’s fulfillmentStatus. I tested this approach against my test store with 4 different scenarios and it works reliably:

query OrderRefundClassification($orderId: ID!) {
  order(id: $orderId) {
    id
    cancelledAt
    cancelReason
    refunds {
      id
      createdAt
      return {
        id
      }
      refundLineItems(first: 50) {
        nodes {
          lineItem {
            id
            title
            fulfillmentStatus  # This is the key field
          }
          quantity
          restockType
        }
      }
    }
  }
}

Your classification logic would be:

  • Return: refund.return is not null, OR restockType = RETURN

  • Cancellation: restockType = CANCEL, OR (restockType = NO_RESTOCK AND lineItem.fulfillmentStatus is unfulfilled)

  • Standalone refund: restockType = NO_RESTOCK AND lineItem.fulfillmentStatus is fulfilled

I validated this with actual refunds:

  1. CANCEL + unfulfilled → cancellation with restock

  2. NO_RESTOCK + unfulfilled → cancellation without restock (your ambiguous case)

  3. RETURN + fulfilled → return with restock

  4. NO_RESTOCK + fulfilled → standalone refund

Cancellations happen on unfulfilled items, so NO_RESTOCK + unfulfilled = cancellation without restock. If it’s NO_RESTOCK + fulfilled, it’s a standalone refund.

This came up in another thread where someone noted the API doesn’t have full parity with what the admin UI can do for cancellations without restock - the admin uses an internal removal field that’s not exposed publicly. But the fulfillmentStatus approach works for classification.

Let me know if that covers your scenario or if there’s more I can do to help!

Thanks so much @Donal-Shopify, One last question from my end is, do these fields reliably work on the Admin and POS side as well?

Yes, these fields work consistently across Admin, POS, and API. The fulfillmentStatus and restockType are properties of the order/refund data model itself, not tied to where the refund was created. The GraphQL Admin API query I shared will return the same values regardless of the refund source, so your classification logic will work the same way whether a merchant processes refunds through the admin interface, at point of sale, or via your app.

I could see partialfulfillmentStatus as well. How would we infer if the refund is actually from the fulfilled quantity or the unfulfilled quantity, because there could be a possibility that some quantity of a line item is fulfilled and the remaining is not? Our system posts fulfillment on Shopify on a per-unit basis.

@Donal-Shopify Could you please help here in moving forward? I hope the partial fulfillment is clear and sensible.

Please be patience, it is the holiday season after all.

Hi @Chinmay_Sharma, thanks for waiting to hear back from me! The restockType field tells you exactly which portion the refund was applied to - you don’t need to infer it. I tested this against a dev store with a partially fulfilled order (3 of 5 units shipped):

restockType unfulfilledQuantity currentQuantity Applied to
CANCEL 2 → 1 5 → 4 Unfulfilled
RETURN stayed at 2 4 → 3 Fulfilled
NO_RESTOCK stayed at 2 3 → 2 Fulfilled

So your classification logic is straightforward:

  • restockType = CANCEL → cancellation (unfulfilled units)

  • restockType = RETURN → return (fulfilled units, restocked)

  • restockType = NO_RESTOCK → standalone refund (fulfilled units, no restock)

The one gap is “cancel unfulfilled without restocking” - CANCEL requires a locationId for restocking, and there’s no public API equivalent to what the admin UI can do. This is a known limitation being looked at internally. The workaround is to use CANCEL with a location and adjust inventory afterward - not ideal, but it’s the only option via API right now.

Let me know if there are any further edge cases you need looked at!