Returns

A Return tracks three things that move at their own pace:

  1. The decision. What the buyer asked for, and what you approved.
  2. The merchandise. What was sent back, received, inspected, and where it ended up.
  3. 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.

text
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.

text
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 knowRead
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 buildingStart here
Anything, for the first timeYour first return
A buyer sends something back by mailBuyer-initiated returns
Someone hands merchandise over at a registerIn-store returns
A warehouse or 3PL records what arrivedReceiving and inspecting
Refunds, exchanges, and replacementsReturn resolutions
The rules: windows, fees, who pays shippingReturn 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#

JobHow
A return window, per product, category, or everythingPolicy scope and return_window
Final sale itemsA higher-priority policy scoped to them with eligibility_result: "ineligible"
Charge a restocking feerestocking_fee on the policy version
Make the buyer pay return shippingreturn_shipping.payer
Swap for a different size or variantAn exchange resolution with replacement line items
Send a replacement and collect nothingA replacement resolution
"Keep it, we'll refund you"Approve with return_required_quantity: 0
Approve less than the buyer asked forPer-line approved_quantity
Only refund once the goods are backrefund_timing of after_receipt or after_inspection
Accept a gift returnSupported. The refund goes to the original payer and payment method
Let a warehouse record arrivals without refund rightsThe 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 the order_line_item_id and fulfillment_id it 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_id and return_resolution_id; refunds you create directly carry neither, and both are filters on GET /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.

ScopeAuthority
commerce.returns.readEligibility, Returns, child facts, previews, and list filters
commerce.returns.writeCreate and maintain Return requests, cancel, and complete
commerce.returns.decisions.writeDecide, reopen, waive inspection, and cancel approved line quantity
commerce.returns.operations.writeReceipt, inspection, and disposition
commerce.returns.resolutions.writePropose, confirm, release, retry, cancel, and collect for resolutions
commerce.returns.process.writeComposite in-store processing only
commerce.return_policies.writeCreate and version policies
commerce.return_reasons.writeCreate 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.

IntegrationStart with
Merchant returns systemreturn.created, return.decision_recorded, return.updated, return.completed, return_resolution.updated
Buyer portalNo merchant subscription. Use buyer notifications and relationship-bounded reads.
Point of salereturn.updated, return.completed, return_disposition.updated, return_resolution.updated
Warehouse or WMSreturn.decision_recorded, return.updated, return_receipt.created, return_receipt.verified, return_inspection.created, return_disposition.updated
ERP or financereturn.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#

Rate this doc