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.
- requested moves to approved
- requested moves to partially approved
- requested moves to declined
- awaiting handoff moves to received
- received moves to inspected
- inspected moves to dispositioned
- dispositioned moves to resolved
When the buyer keeps the goods, the merchandise track reads not_required throughout.
- not selected moves to pending
- pending moves to fulfilled
When all three tracks settle, Flint sends 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 neighbor.
| 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.
Warning: 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 | Your first return: items are here |
| 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. Create with POST /v1/returns, then use POST /v1/returns/{return_id}/process to commit the next bounded set of receipt, inspection, disposition, and resolution facts together. The stepwise routes write the same facts one at a time.
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 revision |
| 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 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.
POST /v1/refunds moves money without applying return eligibility, restocking fees, inspection, or inventory updates. Use a Return when merchandise is coming back.
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, decide, reopen, waive inspection, cancel approved line quantity, cancel, and complete |
commerce.returns.operations.write | Receipt, inspection, and disposition |
commerce.returns.resolutions.write | Propose, confirm, release, retry, cancel, and collect for resolutions |
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 processing through POST /v1/returns/{return_id}/process requires all three scopes: commerce.returns.write, commerce.returns.operations.write, and commerce.returns.resolutions.write.
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.receipt.created, inventory.level.updated |
Deduplicate on event ID, acknowledge quickly, and read the resource when the next action depends on its current version. Event order is not a substitute for resource state. Every payload and field is in the webhook events catalog.
Concurrency#
Include expected_version from the resource you read to reject concurrent changes. It is optional on actions and scalar updates, and required when a PATCH replaces an owned collection. On a conflict Flint returns 409 RETURN_VERSION_CONFLICT with the current version 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/{return_id}/process and optional on other Return writes. Replaying the same process key returns the original processing result without repeating inventory or money effects.
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.
