A refund is not a return
On Provale Cup and Al's Flowers we keep money and the canonical order on Shopify. That includes refunds. The mistake that shows up when a store grows past "just refund it in admin" is treating a payment reverse as if it finished the return. Shopify's return APIs now make the split explicit: a refund moves funds; return processing confirms what came back, what gets restocked, and what the books should record.
Two jobs that used to blur into one button
A refund answers one question: how much money goes back, to which parent transaction, and whether the customer gets notified. That is necessary. It is not the whole job.
A return answers a different set: which line items are coming back, whether a reverse delivery exists, whether units restock, whether an exchange line should become a fulfillment order, and when the merchant's financial report should treat the event as a Sale. Those decisions can lag the payment by days — the box is still in transit — and they can happen without a full refund at all.
Collapsing both into refundCreate is how you get a paid-out customer, inventory that never returned to available, and an exchange that never released. The platform used to paper over some of that when create/approve implied more than they should. The returnProcess path does not.
Name the return states the way you name order states
Shopify returns move through statuses for a reason: REQUESTED waiting on merchant approval, OPEN once reverse logistics can start, CLOSED when processing has finished for the items that matter. Those are not UI chips. They are gates on which mutations are legal.
Approve creates the reverse fulfillment order. Process is what confirms returned quantities, records fees, releases even exchanges, and is the moment reporting should catch up. Refund can ride along inside process, or you can process without issuing money yet. Either way, "we called refund" is not a synonym for "the return is done."
We treat this the same way we treat Al's Flowers order webhooks: the event is a rumor until the durable record and the downstream step agree. A returns/create webhook that immediately issues a refund and marks the ticket closed will lie the first time a partial return or a declined inspection shows up.
Refunding the wrong unit is a real failure mode
Orders with multiple quantities of the same SKU are where refundCreate against an order line quietly becomes dangerous. The money math can look right while you attach the refund to the wrong physical unit relative to the open return.
Return processing keys off return line items — the units that were approved to come back — not a fresh guess from the original order. That is the seam you want if ops later asks "which of the two red vases did we credit?" and expects the answer to match the RMA.
If your custom portal still talks in order line ids only, map them into return line items at approve time and keep those ids through process. Do not re-derive from the order at refund time and hope the quantities still line up.
Keep Shopify as the return authority too
The same instinct that keeps checkout and the order id on Shopify applies after the sale. Custom code can own the customer portal UX, eligibility rules, print-shop tickets for damaged goods, or ERP restock codes. It should not invent a second ledger of "we refunded this" that drifts from Shopify's Return and Refund records.
On Al's Flowers, the order webhook is the start of ops — PDF ticket, PrintNode — not a parallel order database. Returns should hang off the same authority: listen for returns/* and refunds/create, drive shop-specific work from the stored return id, and let process close the commercial loop.
What we avoid is a support tool that refunds via one API and restocks via a spreadsheet. The next person looking at the order in admin will not see the spreadsheet.
What we ask before we schedule a returns flow
Do customers self-serve a request that needs approve, or does staff create the return already open? Are partial returns and exchanges in scope, or refund-only for the first release? Who decides restock — always, never, or after inspection?
Those answers decide whether native admin plus a thin webhook listener is enough, or whether you need a portal that calls returnRequest, returnApproveRequest, and returnProcess in sequence. They also decide whether Al's-style post-order automation should open a second ticket when a return lands, or stay silent until process fires.
If the honest workflow today is "refund in admin and hope inventory catches up," the smallest useful version is often process-with-refund on a staff-created return — not a full RMA product. Build the lifecycle you can draw on a whiteboard. The refund amount is one field on that lifecycle, not the system of record.
Questions
- Can we still refund without creating a return?
- Yes — not every money movement is a logistics return. Use a plain refund when nothing is coming back. Use a Return when items, restock, reverse delivery, or exchanges are part of the story, and process that return instead of pretending the refund closed it.
- Should refund happen at approve or at process?
- Default to process: you have confirmed what was accepted back (or you are explicitly processing without physical receipt). Refunding at approve pays out before inspection finishes, which is a policy choice you should make on purpose, not an accident of which mutation you called first.
- Where should custom return portals write their state?
- Keep the Return id and status on Shopify as truth. Your app can store portal UX state and shop-specific flags keyed to that id. Avoid a second refund/restock ledger that support has to reconcile by hand.