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.
@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:
CANCEL + unfulfilled → cancellation with restock
NO_RESTOCK + unfulfilled → cancellation without restock (your ambiguous case)
RETURN + fulfilled → return with restock
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!
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.
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 = 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!