Return policies and reasons
A policy is how you stop deciding returns by hand. It says what is returnable, for how long, on whose shipping account, at what fee, and whether Flint may approve it without asking you.
Policies are versioned. The policy has a stable ID and a name; each version is an immutable set of rules. When a Return is created, the matching version is frozen onto it, so a Return decided last March can still show the rules it was judged against even after you changed them twice.
Write a policy#
curl -X POST https://api.withflintpay.com/v1/return-policies \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: policy-standard-001" \
-d '{
"name": "Standard returns",
"version": {
"priority": 100,
"scope": { "match_type": "all" },
"eligibility_result": "eligible",
"return_window": { "duration_seconds": 2592000, "starts_at_event": "delivered" },
"allowed_resolution_types": ["refund", "exchange"],
"approval_mode": "automatic",
"resolution_mode": "manual",
"completion_mode": "automatic",
"refund_timing": "after_receipt",
"is_merchandise_return_required": true,
"is_inspection_required": false,
"receiving_location_id": "loc_1kmn0aExample",
"return_shipping": { "payer": "buyer" },
"restocking_fee": { "calculation_type": "percent", "percent": 10 }
}
}'
That reads as: everything is returnable for 30 days from delivery, the buyer may ask for a refund or an exchange, we approve automatically, the buyer pays return shipping to that location, we keep 10 percent, and the money goes back once the goods arrive.
A version is validated when you write it, so these are rejected at create and publish rather than surfacing later:
- An ineligible policy commits no resolution behavior. When
eligibility_resultisineligible, you must omitallowed_resolution_types,resolution_mode,refund_timing, andcompletion_modeentirely. Nothing is returnable under it, so there is nothing to decide. The rest of the rules below apply only toeligibleandreview_requiredversions. completion_modeis required on an eligible or review-required version.refund_timingis required wheneverrefundis inallowed_resolution_types. It also constrains what else you can say:after_handoffandafter_receiptneedis_merchandise_return_required: true, andafter_inspectionneeds both handback andis_inspection_required: true.is_merchandise_return_required: truerequires areceiving_location_id, because it becomes the destination the buyer ships to.resolution_mode: "automatic"requiresresolution_selection_mode, andmanualmust not set it orautomatic_resolution_type.- Automatic resolution may only offer outcomes Flint can construct alone. Combining
resolution_mode: "automatic"andresolution_selection_mode: "buyer_requested"withexchangeorreplacementinallowed_resolution_typesis rejected, because picking replacement merchandise needs a person.
One check happens later than you might expect. Creating or publishing a version only confirms that receiving_location_id is present. Whether that Location exists, is active, and carries a postal address is checked when you activate the policy, which fails with a conflict naming the unusable location. A version can therefore be stored and only rejected at activation.
Publishing a change means publishing a new version, not editing the old one:
curl -X POST https://api.withflintpay.com/v1/return-policies/rpol_1kmn0aExample/versions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: policy-standard-002" \
-d '{
"expected_return_policy_revision": 1,
"expected_current_return_policy_version_id": "rpolv_1kmn0aExample",
"version": {
"priority": 100,
"scope": { "match_type": "all" },
"eligibility_result": "eligible",
"return_window": { "duration_seconds": 5184000, "starts_at_event": "delivered" },
"allowed_resolution_types": ["refund", "exchange"],
"approval_mode": "automatic",
"resolution_mode": "manual",
"completion_mode": "automatic",
"refund_timing": "after_receipt",
"is_merchandise_return_required": true,
"receiving_location_id": "loc_1kmn0aExample"
}
}'
Publishing takes two guards, not one: expected_return_policy_revision for the policy and expected_current_return_policy_version_id for the version you believe is current. Together they stop two people publishing over each other while both looked at the same screen.
PATCH /v1/return-policies/{return_policy_id} changes identity fields such as the name. It does not change rules. If you find yourself wanting to patch a window, publish a version instead.
A policy is evaluated only while it is active. /activate, /deactivate, and /archive control that, and none of them disturb Returns that already froze a version.
Which policy wins#
Policies match by scope and resolve by priority, so you layer a general rule under specific ones.
match_type is either all or include. With all, every other selector must be empty: it is the catch-all. With include, you name what it covers through product_ids, variant_ids, categories, location_ids, or channel_types.
That combination is how you express final sale. Give the catch-all a low priority and eligibility_result: "eligible", then add a higher-priority policy scoped to the products you will not take back with eligibility_result: "ineligible" and an ineligibility_reason. Buyers see the item as not returnable in the eligibility check, with the reason attached.
Do not copy the resolution fields across from your eligible policy when you write that one. An ineligible version must omit allowed_resolution_types, resolution_mode, refund_timing, and completion_mode, and is rejected if it sets any of them.
When nothing matches at all, an API caller can still create a Return. It goes to manual review, and deciding it needs no override reason because there was no policy to override. A hosted buyer portal treats an active matching policy as your opt-in to self-service, so no policy means the buyer sees no return option.
Windows#
A window is a duration plus the event it counts from.
starts_at_event | Counts from |
|---|---|
fulfilled | The fulfillment completing |
delivered | The carrier delivering |
picked_up | The buyer collecting in person |
service_completed | The work being finished |
delivered is usually what a buyer thinks "30 days" means. fulfilled starts the clock while the parcel is still on a truck, which quietly shortens the window by the transit time.
Fees and who pays shipping#
return_shipping.payer is merchant or buyer, and it is a statement of policy rather than a charge. Flint does not buy labels, so the amount is not on the policy. Where a fee is charged, it lands as a return_shipping_fee adjustment on the resolution, in the same arithmetic as the refund.
restocking_fee is either flat with a flat_money, or percent with a whole-number percent (10 means 10 percent). It surfaces on the resolution as a restocking_fee adjustment and appears in the Return's financial_summary as a deduction, never netted invisibly into the refund.
What automatic actually decides#
Two modes work together. approval_mode decides whether Flint approves the Return itself. resolution_mode decides whether it also creates and confirms the buyer's outcome.
Automatic resolution is deliberately limited to refund and no_monetary_action. An exchange or replacement means picking merchandise, which is a judgement Flint will not make on your behalf. Those still land in front of a person even under an automatic policy.
resolution_selection_mode decides who picks:
policy_defaultusesautomatic_resolution_typewithout asking.buyer_requestedwaits for the buyer to choose fromallowed_resolution_types. Until they do, the Return sits inrequestedand previews are the way to show them what each option is worth.
completion_mode closes the loop. Under automatic, Flint completes the Return when the last completion blocker clears. Under manual, you call /complete yourself once the list is empty.
Decide a request by hand#
Without an automatic policy, or when you want to overrule one, decide the Return directly. Each line picks one of two decision styles, and they take different fields.
decision_basis: policy_evaluation
Accept the policy proposal
Send approved_quantity and, when approving less than asked, the decline context. The explicit fields are rejected here.
{
"expected_return_revision": 1,
"completion_mode": "automatic",
"line_items": [
{
"return_line_item_id": "retli_1kmn0aExample",
"decision_basis": "policy_evaluation",
"approved_quantity": 1
}
]
}
decision_basis: explicit
State the outcome yourself
return_required_quantity is required. Approving any quantity also requires allowed_resolution_types and resolution_mode.
{
"expected_return_revision": 1,
"completion_mode": "automatic",
"line_items": [
{
"return_line_item_id": "retli_1kmn0aExample",
"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"
}
]
}
Several fields on the explicit arm are easy to miss because what they require depends on other fields:
resolution_mode: "automatic"also requiresselected_resolution_type. Flint will not pick the outcome for you. Withmanualyou can leave it unset and decide later.- Any
return_required_quantityabove 0 requires bothis_inspection_requiredandreceiving_location_id. Merchandise coming back needs somewhere to go and an explicit answer on whether anyone checks it. refund_timing: "after_inspection"requiresis_inspection_required: true, since otherwise the gate never opens.
A returnless line inverts all of that. With return_required_quantity: 0, is_inspection_required cannot be true, and refund_timing cannot be after_handoff, after_receipt, or after_inspection, because none of those events will ever happen. Use after_approval or manual, or leave the timing unset.
Deviating from an active policy requires an override_reason. Deciding where no policy matched does not, because there was nothing to override.
Returnless refunds
Set return_required_quantity: 0 while approving a quantity and the buyer keeps the merchandise. The Return still settles the money and still records why, so it reconciles like any other. Use it when return shipping costs more than the item is worth.
Reasons, and the three vocabularies it is not#
Return reasons are the buyer-facing list: why they say they are sending it back.
curl -X POST https://api.withflintpay.com/v1/return-reasons \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: reason-too-small-001" \
-d '{
"handle": "too_small",
"name": "Too small",
"categories": ["fit"],
"is_note_required": false
}'
Flint provides defaults; anything you add carries a merchant source. Archiving one stops buyers selecting it, and Returns that already recorded it keep the frozen name, so old Returns never lose their meaning when you tidy the list.
Four vocabularies use the word "reason" and they are not interchangeable:
| Vocabulary | Who supplies it | Where it lives |
|---|---|---|
| Return reason | The buyer | return_reason_id on the Return line |
| Inspection finding | The warehouse | finding_codes on an inspection line |
| Decline reason | The merchant | decline_reason on a decision |
| Refund reason | Flint | reason on the Refund, mapped from the buyer's return reason |
A buyer saying "damaged" and a warehouse finding damaged are different claims about the same parcel, and the difference is exactly what you want when one of them is wrong. Keep them separate.
Next steps#
- Buyer-initiated returns: the policy in front of a buyer.
- Return resolutions: where fees land and when money moves.
- Return policies API reference: every field on policies and versions.
