In-store returns
At a counter, everything happens at once. The buyer is standing there, the item is in your hand, and the refund has to be authorized before they walk out. POST /v1/returns/process commits the whole Return in one call so a register never has to orchestrate seven requests while someone waits.
curl -X POST https://api.withflintpay.com/v1/returns/process \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pos-return-001" \
-d '{
"order_id": "ord_1kmn0aExample",
"completion_behavior": "complete_when_ready",
"receipt": {
"receiving_location_id": "loc_1kmn0aExample",
"source_system": { "source_system_type": "pos", "external_source_id": "store-1042" },
"received_at": "2026-07-29T14:30:00Z"
},
"line_items": [
{
"order_line_item_id": "li_1kmn0aExample",
"requested_quantity": 1,
"return_reason_id": "rrsn_1kmn0aExample",
"decision": {
"decision_basis": "explicit",
"approved_quantity": 1,
"return_required_quantity": 1,
"allowed_resolution_types": ["refund"],
"selected_resolution_type": "refund",
"resolution_mode": "automatic",
"refund_timing": "after_receipt",
"is_inspection_required": false,
"receiving_location_id": "loc_1kmn0aExample"
},
"received_quantity": 1,
"disposition": {
"disposition_type": "sellable",
"inventory_location_id": "loc_1kmn0aExample",
"reason": "inspection_result",
"occurred_at": "2026-07-29T14:30:00Z"
},
"resolution": { "resolution_type": "refund", "quantity": 1 }
}
]
}'
What "atomic" covers, and what it does not#
The response returns the full fact graph with stable child IDs. Those facts committed together: the Return, the decision, the receipt, the disposition, and the resolution either all exist or none do.
The effects they trigger are still asynchronous. The refund, the inventory movement, any replacement fulfillment, and any buyer payment settle after the call returns.
For a register that means: print the receipt, hand back the item, let the customer go. Do not block the lane on the refund reaching succeeded. Follow it on return_resolution.updated and return.completed, or reconcile at close of day.
One scope, not five#
The stepwise flow spreads authority across four write scopes because four different systems use it. A register needs none of that. commerce.returns.process.write grants exactly one thing: the authority to process a complete Return through this command.
A register key holding only that scope cannot decide an arbitrary Return, cannot record a warehouse receipt against someone else's Return, and cannot issue a standalone refund. That is the point. Give each register the narrow scope rather than a key that could do everything.
Replay, do not retry#
/process moves stock and money, so an Idempotency-Key is required. Without one the call fails with IDEMPOTENCY_KEY_REQUIRED rather than quietly succeeding.
When the network drops and you never see the response, replay the same key. Flint returns the original result. Reusing that key with different content is a conflict (IDEMPOTENCY_KEY_REUSED), not a silent overwrite.
Because the create form mints a return_id you have not seen yet, there is a recovery path that does not need it:
curl "https://api.withflintpay.com/v1/returns?idempotency_key=pos-return-001" \
-H "Authorization: Bearer YOUR_API_KEY"
Use the register's own transaction identifier as the key. It is naturally unique, and it means a lost response is recoverable from data the register already has.
When the buyer already started online#
If the buyer created the Return on your website and is now at the counter with the box, process the Return you already have:
curl -X POST https://api.withflintpay.com/v1/returns/ret_1kmn0aExample/process \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pos-process-ret-001" \
-d '{
"expected_return_revision": 2,
"completion_behavior": "complete_when_ready",
"receipt": {
"receiving_location_id": "loc_1kmn0aExample",
"source_system": { "source_system_type": "pos", "external_source_id": "store-1042" },
"received_at": "2026-07-29T14:30:00Z"
},
"line_items": [
{
"return_line_item_id": "retli_1kmn0aExample",
"received_quantity": 1,
"resolution": { "resolution_type": "refund", "quantity": 1 }
}
]
}'
Two differences from the create form. Lines are addressed by return_line_item_id, because they already exist. And the call takes expected_return_revision, so a decision made by support while the buyer was driving to the store cannot be silently overwritten. On a 409 RETURN_REVISION_CONFLICT, re-read the Return and process against its current revision. This form needs no recovery filter, since you already have the ID.
Moving to the stepwise flow#
The nested blocks in /process are the same facts the stepwise routes write, so growing into them is a re-mapping rather than a rewrite.
Block in /process | Stepwise equivalent |
|---|---|
| the top-level request | POST /v1/returns |
line_items[].decision | POST /v1/returns/{return_id}/decide |
receipt and line_items[].received_quantity | POST /v1/returns/{return_id}/receipts |
line_items[].inspection | POST /v1/returns/{return_id}/inspections |
line_items[].disposition | POST /v1/returns/{return_id}/dispositions |
line_items[].resolution | POST /v1/returns/{return_id}/resolutions, then /confirm |
The field names inside each block are close to the standalone request bodies but not identical, because the composite already knows the Return and line context that the standalone routes take explicitly. Read the returns reference for the exact shape of each rather than assuming they match field for field.
You would move to stepwise when the physical work stops being simultaneous: a warehouse receiving parcels days later, an inspection queue, or a support tool that decides before anything ships back.
Next steps#
- Return policies and reasons: replace the explicit decision block with a policy.
- Receiving and inspecting: the stepwise version of the receipt and disposition above.
- Return resolutions: exchanges, replacements, and corrections at the counter.
