Returns
A Return tracks three things that move at their own pace:
- The decision. What the buyer asked for, and what you approved.
- The merchandise. What was sent back, received, inspected, and where it ended up.
- The value. What the buyer gets, through Refunds, replacement Orders, and payments.
They are separate because they genuinely come apart. A refund can succeed while the box is still in transit. A box can arrive for a Return nobody approved. Flint keeps a status for each one rather than collapsing them into a single field that would have to lie about at least one.
decision requested ──► approved / partially approved / declined
merchandise awaiting handoff ──► received ──► inspected ──► dispositioned ──► resolved
or "not required" throughout, when the buyer keeps the goods
resolution not selected ──► pending ──► fulfilled
all three settled ──► return.completed
The objects#
Everything hangs off one Return, which hangs off one Order.
Order
└── Return .......................... the request, and your decision on it
├── Return line items .......... one per order line, each with its own quantities
├── Receipts ................... what physically arrived
├── Inspections ................ what condition it was in, when you require a check
├── Dispositions ............... where it ended up: back on sale, scrapped, donated
└── Resolutions ................ what the buyer gets
├── Refunds ............... money back on the original payment
├── Replacement Order ..... merchandise going back out
└── PaymentIntent ......... when an exchange leaves a balance due
Receipts and inspections are observations: once recorded they are never edited. A mistake is corrected by recording a superseding observation, so the physical history stays intact.
Which status answers which question#
Four status fields sit on a Return, and the same words appear in several of them. Read the one that answers your question rather than inferring from a neighbour.
| You want to know | Read |
|---|---|
| Did we say yes? | decision_status |
| Did the goods come back? | merchandise_status |
| Did the buyer get their value? | resolution_status |
| What is still outstanding? | completion_blockers |
| Is the whole thing finished? | status, or the return.completed event |
completion_blockers is the one to build on. It is the authoritative list of what is still owed, and each entry points at what is holding it up: the Return line where there is one, plus the receipt, inspection, disposition, or resolution responsible. An empty array means nothing is outstanding.
Wait for return.completed, not for the refund
A resolution can reach a terminal state while merchandise work is still open. If a job needs every obligation cleared, act on return.completed or on an empty completion_blockers, never on a refund succeeding.
Pick your integration#
Most integrations touch six or eight of the routes on this surface. Find the row that describes you and start there.
| What you are building | Start here |
|---|---|
| Anything, for the first time | Your first return |
| A buyer sends something back by mail | Buyer-initiated returns |
| Someone hands merchandise over at a register | In-store returns |
| A warehouse or 3PL records what arrived | Receiving and inspecting |
| Refunds, exchanges, and replacements | Return resolutions |
| The rules: windows, fees, who pays shipping | Return policies and reasons |
The counter flow and the mail-in flow write the same durable facts. POST /v1/returns/process commits a whole Return in one call; the stepwise routes commit the same receipt, inspection, disposition, and resolution one at a time. You can start with either and move to the other without migrating data.
What you can model#
| Job | How |
|---|---|
| A return window, per product, category, or everything | Policy scope and return_window |
| Final sale items | A higher-priority policy scoped to them with eligibility_result: "ineligible" |
| Charge a restocking fee | restocking_fee on the policy version |
| Make the buyer pay return shipping | return_shipping.payer |
| Swap for a different size or variant | An exchange resolution with replacement line items |
| Send a replacement and collect nothing | A replacement resolution |
| "Keep it, we'll refund you" | Approve with return_required_quantity: 0 |
| Approve less than the buyer asked for | Per-line approved_quantity |
| Only refund once the goods are back | refund_timing of after_receipt or after_inspection |
| Accept a gift return | Supported. The refund goes to the original payer and payment method |
| Let a warehouse record arrivals without refund rights | The commerce.returns.operations.write scope on its own key |
Three things Flint does not do yet, so you can design around them rather than discover them:
- Store credit. Not published until there is a reconciled credit ledger. If refunding the original payer is not acceptable, decline the monetary resolution.
- Returning one component of a bundle. A bundle comes back as a whole commercial line. Its components are available for display and inventory lineage.
- A correction that leaves the buyer owing money. Rejected today. A correction can reduce a credit to zero but cannot turn it into a debt.
Where returns meet the rest of Flint#
Returns do not introduce a parallel world. They reuse resources you have already integrated.
- Orders. The Return names an
order_id, and each line names theorder_line_item_idandfulfillment_idit came from. As value settles, Flint recalculates the order's refunded and balance amounts and appends to its activity log. - Refunds. A resolution that refunds creates an ordinary Refund on the same order, drawn from the same refundable balance. It carries
return_idandreturn_resolution_id; refunds you create directly carry neither, and both are filters onGET /v1/refunds. - Inventory. A disposition is what moves stock. Sending merchandise back on sale produces an inventory return receipt and movements against a Location, so nothing lands in inventory until you say where it went.
- Shipments. Return labels and tracking are not on the Return. They use the same shipment resource as outbound delivery, with
direction: return. See Buyer-initiated returns.
Going straight to POST /v1/refunds when merchandise is coming back skips the other half of the job: no eligibility check, no restocking fee, no inspection, and no stock returned to a location.
Scopes#
Returns split write authority so a store, a warehouse, and a support tool can each hold only what they need.
| Scope | Authority |
|---|---|
commerce.returns.read | Eligibility, Returns, child facts, previews, and list filters |
commerce.returns.write | Create and maintain Return requests, cancel, and complete |
commerce.returns.decisions.write | Decide, reopen, waive inspection, and cancel approved line quantity |
commerce.returns.operations.write | Receipt, inspection, and disposition |
commerce.returns.resolutions.write | Propose, confirm, release, retry, cancel, and collect for resolutions |
commerce.returns.process.write | Composite in-store processing only |
commerce.return_policies.write | Create and version policies |
commerce.return_reasons.write | Create and archive merchant reasons |
Any Returns write scope can read what it needs to do its work. It gains no sibling write authority. A warehouse key records physical facts without the ability to refund, and a register key uses the composite command without separately holding every stepwise scope.
Webhooks#
Subscribe to what your actor acts on, not to everything.
| Integration | Start with |
|---|---|
| Merchant returns system | return.created, return.decision_recorded, return.updated, return.completed, return_resolution.updated |
| Buyer portal | No merchant subscription. Use buyer notifications and relationship-bounded reads. |
| Point of sale | return.updated, return.completed, return_disposition.updated, return_resolution.updated |
| Warehouse or WMS | return.decision_recorded, return.updated, return_receipt.created, return_receipt.verified, return_inspection.created, return_disposition.updated |
| ERP or finance | return.completed, return_resolution.updated, refund.updated, payment_intent.succeeded, payment_intent.payment_failed, order.updated, inventory.return_receipt.created, inventory.level.updated |
Deduplicate on event ID, acknowledge quickly, and read the resource when the next action depends on its current revision. Event order is not a substitute for resource state. Every payload and field is in the webhook events catalog.
Concurrency#
Two people can decide the same Return at once, so writes that change meaning take a revision. Send expected_return_revision (or the disposition, resolution, policy, or reason equivalent) from the resource you just read. On a conflict Flint returns 409 RETURN_REVISION_CONFLICT with the current revision in the error detail, so you can re-read and retry against it rather than hunting for what changed.
Money-moving and stock-moving commands take an Idempotency-Key. It is required on POST /v1/returns/process and POST /v1/returns/{return_id}/process, and optional everywhere else. Because the create form mints an ID you do not yet know, recover a lost response with GET /v1/returns?idempotency_key=... rather than creating a second Return.
Next steps#
- Your first return: a paid order to a settled refund in sandbox.
- Return policies and reasons: the rules everything else is evaluated against.
- Returns API reference: every field and route.
