Recipe: returns & refunds
Goal: take a placed order through the returns (RMA) lifecycle as a merchant, restock it, and settle the refund — safely, without double-refunding on a retry.
Audience: merchant (back-office, via the fdk_ API).
All routes here are the tenant-scoped merchant-commerce cluster. Every call is
authenticated by an fdk_ credential (Authorization: Bearer); the tenant is
carried by the credential (credential-carries-ownership),
never in the path or body. Money is integer cents.
Two paths: refund/cancel vs. returns
Choose by the order’s state:
- Not yet shipped — refund or cancel the order directly.
cancelreleases inventory and refunds any captured payment; a shipped/delivered order refuses cancel with a409and must go through returns instead. - Shipped/delivered — run the returns (RMA) lifecycle below, which restocks and settles the refund on receipt.
Direct full refund
POST /v1/merchant/orders/{id}/refund HTTP/1.1
Host: api.ruo.pro
Authorization: Bearer fdk_live_…
Content-Type: application/json
{ "reason": "customer changed their mind" }The scope is manage:own_orders — distinct from read:own_orders, so a
read-only integration key can never move money. The charge is derived
server-side from the order (you never send an amount). The response reports the
full split, including any store-credit/gift-card tenders returned:
{
"refund_id": "rfnd_…",
"charge_id": "chg_…",
"order_id": "ord_…",
"order_status": "refunded",
"amount_cents": 4900,
"refunded_total_cents": 4900,
"store_credit_returned_cents": 0,
"gift_card_returned_cents": 0,
"replayed": false
}This is idempotent per order — a retry replays (replayed: true), it never
double-refunds. An already-fully-refunded order, or an order with no card charge
to refund (tender-only), is a 409.
The returns (RMA) lifecycle
A return moves through requested -> approved -> received -> refunded (or
rejected). Each transition is its own route; an invalid transition for the
current status is a 409. The scope for all of them is manage:own_returns.
1. Find the return
GET /v1/merchant/returns HTTP/1.1
Authorization: Bearer fdk_live_…Returns a page of the tenant’s returns, newest first. GET /v1/merchant/returns/{id} fetches one return and its lines (each line
carries sku, quantity, unit_price_cents, line_refund_cents,
restocked).
2. Approve or reject
POST /v1/merchant/returns/{id}/approve HTTP/1.1
Authorization: Bearer fdk_live_…
Content-Type: application/json
{ "reason": "within window" }.../reject is the same shape. Both return the updated return; an invalid
state transition is a 409.
3. Receive — restock and settle the refund
This is the money-moving step, and it requires an Idempotency-Key
header. Send the order’s original charge to refund against:
POST /v1/merchant/returns/{id}/receive HTTP/1.1
Host: api.ruo.pro
Authorization: Bearer fdk_live_…
Content-Type: application/json
Idempotency-Key: return-{returnId}:receive-1
{ "charge_id": "chg_…" }{
"return_id": "ret_…",
"status": "refunded",
"order_id": "ord_…",
"order_status": "refunded",
"refund_id": "rfnd_…",
"refunded_cents": 4900,
"refunded_total_cents": 4900,
"lines": [
{ "order_item_id": "oi_…", "sku": "SKU-1", "quantity": 1, "line_refund_cents": 4900, "restocked": true }
]
}The
Idempotency-Keyheader is the safety on this step. Make it deterministic — derive it from the business event (e.g.return-{returnId}:receive-1), never a fresh random value — and reuse it across retries of the same receive. A retried receive with the same key replays the original settlement instead of refunding twice. This is the same discipline the checkout idempotency and the platform’s money invariants enforce everywhere money moves.
Partial restock
If restock cannot complete for every line, the response carries
restock_incomplete: true and restock_errors: [...] — the refund still
settled, but some inventory did not restock. Surface these; do not treat the
200 as “everything restocked”.
Related
- Checkout — order placement and the storefront-side cancel/refund calls.
- Gift cards / Store credit — how returned tenders are reflected in the refund split.
- Money invariants — deterministic idempotency and fail-closed money paths.