Return resolutions
A resolution is what the buyer gets. Approving a Return says merchandise may come back; a resolution says what happens in exchange for it.
Five kinds exist. Four of them are outcomes you choose up front:
| Type | What the buyer gets |
|---|---|
refund | Money back on the original payment |
exchange | Different merchandise, with the difference settled either way |
replacement | The same merchandise again, with no money moving |
no_monetary_action | Nothing. Used when goods come back but no value is owed |
The fifth, correction, fixes one of the other four after it has already settled. See Fixing one after it settled.
Store credit is reserved and not published yet. If refunding the original payer is not acceptable, decline the monetary resolution rather than working around it.
Preview, create, confirm#
Three steps, and it matters that they are three.
Preview prices an option without changing state or creating approvals or reservations. Use it to show a buyer what they would get before they commit, or to check your own arithmetic before moving money.
curl -X POST https://api.withflintpay.com/v1/return-previews \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "resolution",
"resolution": {
"return_id": "ret_1kmn0aExample",
"resolution_type": "refund",
"line_items": [
{ "return_line_item_id": "retli_1kmn0aExample", "quantity": 1 }
]
}
}'
Every preview names the based_on_return_revision it was calculated against. A preview from before the Return changed is recognisably stale rather than quietly wrong.
Create proposes the resolution and reserves line value, so two resolutions cannot both claim the same approved quantity.
curl -X POST https://api.withflintpay.com/v1/returns/ret_1kmn0aExample/resolutions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: resolution-001" \
-d '{
"resolution_type": "refund",
"line_items": [
{ "return_line_item_id": "retli_1kmn0aExample", "quantity": 1 }
]
}'
Confirm freezes the economics and starts the effects. Before this call the resolution is editable and nothing has moved. After it, the amounts are settled facts.
curl -X POST https://api.withflintpay.com/v1/return-resolutions/retres_1kmn0aExample/confirm \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: confirm-resolution-001" \
-d '{}'
Confirmed is not the same as paid#
A confirmed resolution can sit in pending for a long time, and that is usually correct. execution_blockers says what it is waiting for.
| Blocker | Cleared by |
|---|---|
handoff_pending | The buyer hands the parcel to a carrier |
receipt_pending | The warehouse records the merchandise arriving |
inspection_pending | An inspection and its acceptance decision |
manual_release_pending | Someone calling /release |
buyer_payment_pending | The buyer paying an exchange balance |
refund_pending | The refund settling |
replacement_order_pending | The replacement Order being created |
fulfillment_pending | The replacement shipping |
credit_effect_pending | A linked credit effect completing |
The first three exist because of refund_timing, which the decision sets per line. after_handoff pays on drop-off, after_receipt on arrival, after_inspection once the goods are checked. That single field is how you avoid refunding for goods that never show up.
A resolution's own lifecycle runs proposed to pending to partially_fulfilled to fulfilled, with requires_action, failed, and canceled as the ways out. requires_action means someone has to do something, usually the buyer paying a balance.
Fees#
Restocking and return shipping charges are not separate objects. They are adjustments on the resolution, which is what keeps them in the same arithmetic as the refund.
| Adjustment type | Typical effect |
|---|---|
restocking_fee | Deduction |
return_shipping_fee | Deduction |
other_fee | Deduction |
goodwill_credit | Credit |
price_correction | Either |
A policy proposes its fees automatically; you can also add them by hand. Deductions reduce what the buyer receives and appear in the Return's financial_summary as deduction_money, separately from returned_total_money and refunded_money. Nothing is netted invisibly.
When the buyer owes money#
An exchange for something more expensive leaves a balance. Flint links a Return-scoped PaymentIntent to the resolution and holds buyer_payment_pending until it succeeds.
The replacement Order receives value from the returned items before the buyer pays the difference. Read its return_credit_settlements for the applied amount_money, return_id, return_resolution_id, and created_at. The credit is already included in settlement_amounts.paid_money. Display it as return credit alongside the buyer's payment so the receipt explains both sources of value.
curl -X POST https://api.withflintpay.com/v1/return-resolutions/retres_1kmn0aExample/checkout-session \
-H "Authorization: Bearer YOUR_API_KEY"
The 201 uses the standard checkout launch envelope: data.checkout_session with its hosted url, data.checkout_access with its checkout_auth_token, and data.reused_existing. From there it is the ordinary payment collection flow, unchanged. The body is optional: its return_url brings the buyer back to your customer account after paying, and an address outside that account fails with INVALID_RETURN_URL. Do not copy processor secrets onto the Return.
This route is the only way to collect a return balance. POST /v1/checkout-sessions for the replacement Order fails with 409 RETURN_CHECKOUT_REQUIRED, so every payment carries the resolution's fixed balance.
Collect the balance in your own checkout#
Send "surface": "embedded" to collect the balance in a checkout you build, here or on POST /v1/me/return-resolutions/{return_resolution_id}/checkout-session with a customer session. Offering Affirm also needs redirects.success_redirect_url.
curl -X POST https://api.withflintpay.com/v1/return-resolutions/retres_1kmn0aExample/checkout-session \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"surface": "embedded"}'
The response has no hosted page, so checkout_session.url is omitted, and checkout_session.payment_collection carries the Stripe account, publishable key, and Elements options. Pay the replacement Order in checkout_session.order_id with the checkout credential, as Build your own checkout describes.
A resolution has one open checkout session at a time. A request for the session's current surface returns it with a new credential and reused_existing: true. A request for the other surface replaces the session when no payment is in progress on it, and fails with 409 CHECKOUT_SURFACE_CHANGE_NOT_ALLOWED when one is.
If that payment fails terminally, the resolution becomes failed and exposes retry:
curl -X POST https://api.withflintpay.com/v1/return-resolutions/retres_1kmn0aExample/retry \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: retry-resolution-001" \
-d '{
"reason": "payment_method_updated"
}'
Retrying starts a new attempt. Earlier PaymentIntent IDs stay on payment_intent_ids, and a late event from an abandoned attempt cannot settle the new one. Launch /checkout-session again for the fresh attempt.
Canceling an unpaid exchange#
You can cancel an exchange with an unpaid balance after Flint creates its replacement Order. Replacement-order creation must be the only completed effect. The Order must have no successful or in-progress payment, fulfillment, refund, or other use. A PaymentIntent waiting for a payment method does not block cancellation.
curl -X POST https://api.withflintpay.com/v1/return-resolutions/retres_1kmn0aExample/cancel \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: cancel-exchange-001" \
-d '{ "reason": "expired" }'
Cancellation ends the Order's open checkout session, reverses its return credit, closes the replacement Order, and cancels the resolution and its effects together. The buyer's checkout link stops working, and the Return no longer asks them to pay that exchange balance. The returned quantity and value become available for another resolution. Retrying a successful cancellation does not reverse the credit again.
If a payment has succeeded or is in progress, a fulfillment has started, or another effect has completed, cancellation returns a conflict. Other resolution types retain their existing cancellation rules: you can cancel before an incompatible effect starts.
Fixing one after it settled#
Confirmed money movements are never edited backward. To change an outcome that already happened, reopen the Return and add a correction resolution that names the one it corrects.
curl -X POST https://api.withflintpay.com/v1/returns/ret_1kmn0aExample/resolutions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: correction-001" \
-d '{
"resolution_type": "correction",
"corrects_return_resolution_id": "retres_1kmn0aOriginal",
"adjustments": [
{
"adjustment_type": "price_correction",
"value_effect": "credit",
"status": "applied",
"assessed_amount_money": { "amount": 500, "currency": "USD" },
"applied_amount_money": { "amount": 500, "currency": "USD" },
"reason": "manual_correction"
}
]
}'
A correction is adjustments only. Sending line_items, replacement merchandise, or pricing_basis is rejected, because a correction adjusts value rather than re-deciding what came back.
A net credit creates an ordinary linked Refund once confirmed. A deduction can reduce that credit to zero, but a correction that would leave the buyer owing money is rejected today. Confirm the correction, wait for its effect to succeed, then complete the Return again.
Retrieve the history with GET /v1/return-resolutions?corrects_return_resolution_id=retres_1kmn0aOriginal.
How this lands elsewhere#
A resolution that refunds creates a real Refund on the same order, drawn from the same refundable balance, and returned by the ordinary refund endpoints. It carries return_id and return_resolution_id; refunds you create directly carry neither. Both are filters on GET /v1/refunds.
The Refund's own reason is derived from the buyer's return reason rather than copied: defective becomes defective_product, arrived_late becomes arrived_too_late, changed_mind becomes customer_changed_mind, and so on. Reasons you defined yourself have no equivalent and become other, as does a refund covering lines that gave different reasons. When you need the buyer's exact wording, read the Return line, not the Refund.
Exchanges and replacements create a real Order, linked as replacement_order_id, which is fulfilled and shipped like any other.
Next steps#
- Refunds: the lifecycle of the Refund a resolution creates.
- Return policies and reasons: let a policy propose fees and outcomes for you.
- Return resolutions API reference: every field and route.
