Automate Return Creation
Create Loop returns programmatically — for any trigger where a return needs to exist but no shopper is at the keyboard. Scoped to refund returns for all eligible items, the sequence is 5 fixed calls plus a per-item loop of 3–5 calls. This blueprint walks the happy path, names the few values you hardcode, scales to bulk under Loop's rate limits, and closes the loop with Return Actions.
Before you start
- API key with
Draft Returns (read)andDraft Returns (write)scopes. To auto-process the resulting return via Return Actions, the same key also needs theReturnsscope. - Return reasons configured in the Loop admin — you'll need their reason IDs at runtime.
- A server-side orchestration layer. The Return Create API is HATEOAS-style: every response includes a
linksarray of legal next actions, so you can't blindly pre-script the whole sequence. - An idempotency-key strategy you control.
(shop_id, order_name, trigger_event_id)works well — track each trigger you've processed and skip it on retry. Loop prevents duplicate refunds for the same line item, so this is about keeping your own state clean.
The pieces you need
What you're wiring together, regardless of which path you take below.
Return Create client
Serverdraft_return_id from /draft-returns and threads it through to /submit.Defaults config
Serverreason_id (parent + sub-reason if your taxonomy has children), the credit type (refund or gift), and the return method name to match against each draft's per-draft return_method_options. No business logic — just configuration.Single-return orchestrator
Server/cancel.Bulk orchestrator
Serverdraft_return_id at every step.Process-return caller (optional)
Server/submit, calls Return Actions /warehouse/return/{id}/process to close out the return without manual review — the call that actually makes the automation hands-off.The flow
What happens, in order, from cart change to Loop seeing the protected order.
Initialize the draft return
POST/draft-returnswithshop_id,order_name,secondary_input(typically ZIP), andis_gift: false. Response returns thedraft_return_idandcontext.items_eligible_to_return— the list you're about to loop over.For each eligible item, run three calls
Default the automation to loop over every entry initems_eligible_to_returnunless the trigger explicitly scopes to a subset. For each item, run these three calls in order before moving to the next item.Add the returning item
POST/returning-itemswith the item'sorder_line_item_id. The response includes a newreturning_item_idyou'll use in subsequent calls.Answer any item-level user-input prompts
Check the responselinksforadd-returning-item-user-input-{user_input_id}-{returning_item_id}entries withis_required: true. These are merchant-configured prompts that fire on add-item (e.g. "Worn or washed?" — Yes/No). POST{"value": "..."}to each one before setting the reason or return type — required user inputs gate the rest of the per-item sequence.Set the return reason (+ sub-reason if required)
POST/returning-items/{id}/return-reasonwith your hardcodedreason_id. Then check the response: if aset-returning-item-specific-return-reason-{id}link appears withis_required: true, the parent has children — fetch the sub-reason options fromcontext.return_reason_options[returning_item_id](Loop reuses the same field to surface children) and POST the chosen childreason_idto the same URL. Some reasons also surface a requiredset-returning-item-image-uploads-{user_input_id}-{returning_item_id}link (e.g. "Please upload a photo of the damaged item.") — pick reasons that don't, or build the upload path.Set the return type to `credit`
POST/returning-items/{id}/return-typewith"return_type": "credit". Alwayscreditfor this use case — exchanges aren't in scope for automation.
Finalize items
POST/finalize-items(empty body). Required even when there's nothing in the cart. Locks the return's contents before the credit-type decision.Set credit type
POST/credit-typewithrefund(back to original payment) orgift(issue a gift card). Pick one in your defaults config — there's no item-by-item variant.Select the return method
Readcontext.return_method_optionsfrom the response and POST/return-methodwith thereturn_method_idof the option you want. Required before submit. Match byname(e.g.purchase-label,happy-returns) since IDs are scoped per-draft and won't match across drafts.Submit
POST/submit. Returns the finalized draft return;linksis now empty. The return exists in Loop.(Optional) Process via Return Actions
POST to Return Actions/warehouse/return/{id}/processto close out the return and trigger outcome execution — the call that makes the automation actually hands-off. Requires theReturnsscope on the same key.
When to use this
Use this when an upstream event needs a Loop return to exist, and no shopper is at the keyboard. Canonical example: a fulfillment fails delivery and ships back to the warehouse — something has to create the return so the refund can execute. Other triggers fit the same shape: back-office RMA tools, CS workflows, OMS events, marketplaces without portal access.
Scope: refund or store-credit returns for all eligible items. Gift flow and exchanges are intentionally out — they pull in enough additional state that the automation becomes a portal. If you need those, talk to your Loop point of contact.
Not a portal substitute. The same APIs power custom shopper portals — a different problem shape (real-time UX, full feature surface) that warrants its own walkthrough.
How the API works
A HATEOAS state machine. Every response carries four blocks — draft_return (current state), context (data for the next call), links (legal next actions), errors. Between most steps you read links to know what's legal next, because the available actions depend on what you've done so far and how the merchant's admin is configured.
Call count: 5 fixed + 3–5 per item. The per-item variance comes from configurable prompts that may surface as required links:
- After add-item: an item-level prompt like "Worn or washed?" (Yes/No).
- After set-reason: a sub-reason chain, or an image-upload prompt ("Photo of the damaged item").
All required user inputs must be satisfied before /finalize-items succeeds.
The decisions you have to make
Scoped to refund returns for all eligible items, the decision space collapses to four values you hardcode. Everything else is sequencing the API calls. If you find yourself needing to vary these per-return, you're drifting toward portal territory — pause and rethink the scope.
| Key | Type | Source | Example |
|---|---|---|---|
Return reason (parent) | Why every item is being returned | Configured reasons in the Loop admin | reason_id: 50211 for "warehouse-initiated" |
Return reason (sub-reason) | More specific reason — same URL, new rel | Child reasons of the parent reason | Pick one child if the parent requires it. Skip otherwise. |
Credit type | refund or gift | Set once per return after finalize | refund for the common warehouse-RMA case; gift for store-credit-only policies. |
Return method | Drop-off / box-and-ship / etc. — required | Per-draft context.return_method_options | Hardcode the name (e.g. purchase-label) and look up the matching id on each draft. |
Initialize: the entry point
shop_id + order_name + secondary_input (typically the shopper's ZIP) identifies the order. is_gift: false for this use case.
POST /api/v1/draft-returns
Content-Type: application/json
X-Authorization: <api-key>
{
"shop_id": 23141001,
"order_name": "#1001",
"secondary_input": "90210",
"is_gift": false
}Scaling to bulk
Rate limit: 300 requests per minute per API key. At ~8–10 calls per return, that's roughly 30 returns/min/key serialized — fine for steady-state, tight for spikes. Three patterns to handle it:
- Parallelize within a key. Use a token-bucket limiter to keep concurrent return-creations under 300/min total.
- Pace, don't burst. Spread calls across the minute — bursting trips 429s for the rest of the window.
- Gate on your own idempotency key (
shop_id+order_name+trigger_id) before initializing, so retries skip cleanly and you don't leave abandoned drafts behind.
Closing the loop with Return Actions
/submit creates the return; it doesn't process it. To release the outcome (refund or store credit) without anyone touching the Loop admin, follow up with Return Actions:
POST /api/v1/warehouse/return/{return_id}/process
Successful process returns true. Retries on an already-closed return surface UNPROCESSABLE_RETURN in the response body — treat that as a retry-safe no-op (Loop won't issue duplicate refunds).
When to skip auto-process: inspection-required workflows (let physical inspection drive process/flag later), or fraud-flagged triggers (let Loop's standard review path catch it). See the Return Actions integration guide for the full surface.
Things to watch for
- Always scan
linksfor required user inputs after add-item and set-reason. Item-level prompts ("Worn or washed?") and reason-triggered prompts (damage photos) gate the rest of the per-item sequence. Skipping returns 422 on the next call. - Artifact IDs are per-draft.
returning_item_id,return_method_id, anduser_input_idare scoped to the specific draft. Match return methods bynameand look up theidon each draft. context.return_reason_optionsis reused for sub-reasons. Before setting the parent it holds parent reasons; after, it holds children for that item. Re-read between calls./finalize-itemsis required even with no cart items. POST it with an empty body.- Use your own idempotency key on init (
shop_id+order_name+trigger_id). Loop prevents duplicate refunds, but tracking on your side keeps abandoned drafts off the floor. - The
Returnsscope is needed for/process.Draft Returns read/writecovers creation only — addReturnsto the same key for the Return Actions handoff.