Storefronts and marketplaces
Put the unit back once, not twice
A refund is issued, and the unit is supposed to become sellable again. The restock itself is one call. The difficulty is that it must happen exactly once: a retried job that adds the unit back a second time creates stock which does not exist, and subtracting one afterwards is a correction, not an undo. Shopify reached the same conclusion about its own inventory mutation and made the idempotency key mandatory.
- Triggers
- Two
- Runs
- Every 10 minutes
- Stack
- 5 tools
- Durable execution
- Either one
The flow
The top track answers the customer. The bottom track moves stock on a timer, behind a check that the same refund has not already been restocked.
A refund is created
Shopify Admin API
Claim the refund id
Sheets · Airtable
Tell the customer
Slack · WhatsApp
Every 10 minutes
n8n schedule
List unrestocked claims
n8n
Restock not yet applied?
Temporal · Inngest
Adjust stock with a key
Shopify Admin API
Do nothing
—
Step by step
- 01
A refund is created
Shopify Admin APIA refund webhook fires. It carries the refund id and the restock behaviour chosen when the refund was made. Read both, and write neither back yet.
- 02
Claim the refund id
Sheets · AirtableOne row per refund id, unique on that id. Writing the row is the claim; if it already exists, this refund has been seen and nothing further is owed. This table is the idempotency record, and it is cheap because it belongs to you.
- 03
Tell the customer
Slack · WhatsAppConfirm the refund to the customer, and tell whoever handles returns. This message is about money returned, not about stock — two different facts, and they do not have to arrive together.
- 04
Every 10 minutes
n8n scheduleThe restock runs on the timer rather than on the webhook, so a refund that arrived while the flow was down is still picked up on the next pass.
- 05
List unrestocked claims
n8nRead claims with no restock confirmation. A claim written but never confirmed is precisely the crash case, which is why the claim and the confirmation are separate columns rather than one flag.
- 06
Restock not yet applied?
Temporal · InngestConfirm the restock has not run, then run it, inside one execution that can be retried whole. Neither n8n nor Make remembers that the adjustment already went through, so a retry after a lost response adds the unit a second time, and the stock figure has no way to show which unit was the phantom. Either durable execution, or an idempotency key the platform enforces.
- 07
Adjust stock with a key
Shopify Admin APIThis applies only to refunds recorded as no-restock, or issued through the return refund mutation, which takes no restocking input. A refund created with cancel or return has already been restocked by Shopify, and adding a unit here would create the phantom stock this case exists to prevent. Adjust with a key derived from the refund id, so a retry cannot add a second.
What the platform will not allow
The restock is one call, but what that call means, and how often it may safely be made, is set by Shopify.
refundCreate "creates a refund for an order, allowing you to process returns and issue payments back to customers", and it is the mutation that lets you specify restocking behaviour for line items — where returnRefund "focuses solely on handling the financial refund without any restocking input". Restock is decided at refund time, not bolted on afterwards.
Shopify Admin GraphQL API — refundCreateThe restock behaviour is an enum, not a yes or no. CANCEL says "use this when restocking unfulfilled line items"; RETURN says "use this when restocking line items that were fulfilled"; NO_RESTOCK records that the line "was not restocked". The older LEGACY_RESTOCK is deprecated and "is not accepted when creating new refunds". Choose the wrong one and the unit comes back from the wrong state, or does not come back.
Shopify Admin GraphQL API — RefundLineItemRestockTypeinventoryAdjustQuantities applies a delta, not an absolute quantity. Shopify's own note: "as of version 2026-01, this mutation supports an optional idempotency key using the @idempotent directive", and "as of version 2026-04, the idempotency key is required and must be provided using the @idempotent directive". The platform moved that key from optional to mandatory for the same reason this flow exists — a delta applied twice cannot be undone by applying it again.
Shopify Admin GraphQL API — inventoryAdjustQuantitiesA WhatsApp customer service window opens when the user messages or calls you, and resets if they do so again before it expires; "when the window closes, you can only send pre-approved template messages". A refund confirmation sent days after the customer last wrote must therefore be a template, not free text.
WhatsApp Cloud API — Send messages
When this is not worth building
Three cases where the upkeep costs more than the phantom stock.
You do not restock refunds at all — the returned unit is written off, sent for inspection, or resold through another channel. Then the refund and the stock level are separate stories, and forcing them into one flow invents a link your operation does not have.
Refunds are rare enough that one person handles each by hand and remembers doing it. The whole flow exists to guarantee once; someone touching five refunds a week already guarantees that, and more cheaply.
You have nowhere to record that a restock happened. Without a claim row every part of this is guesswork, because the flow cannot tell a refund it has not processed from one it processed and forgot.