# Handle returns, exchanges and claims

> Take the returns and exchanges shoppers ask for on store orders, and open claims, then approve, receive and refund each one with the Commerce API.

Source: https://www.coritan.com/docs/organizations/storefront/commerce-returns/

A *return* brings units of a store order back for a refund. An *exchange* brings them back for other variants, and refunds the difference when the new ones cost less. A *claim* settles units that were damaged or lost after they shipped, with a refund or a replacement, and nothing comes back.

Shoppers ask for returns and exchanges on your storefront, through the [Store API](/docs/organizations/storefront/store-api/#let-the-shopper-ask-for-a-return). You open, approve, receive and refund them with the Commerce API. The routes are under `https://api.coritan.com/api/v1/orgs/{org_slug}/commerce/returns` and `/commerce/return-reasons`, and the API reference lists them under [Commerce](/docs/api/reference/organizations/commerce/commerce/).

## Before you begin

- Commerce must be on for your organization, as [Sell with the Commerce API](/docs/organizations/storefront/commerce-api/#before-you-begin) describes.
- Reading returns and return reasons takes an organization API key with the `commerce.orders:read` scope, or a member's token with the Tier 1 support role or above.
- Opening, approving, declining, receiving and cancelling returns, and changing the reasons, take `commerce.returns:write`, or the Tier 2 support role or above. A key with it also needs `commerce.orders:read` to read returns.
- Refunding a return takes `commerce.refunds:write`, or the Billing role or above. [Scopes and roles](/docs/organizations/storefront/commerce-api/#scopes-and-roles) lists every scope.

## How a return moves

| `kind` | What comes back | How it is settled |
| --- | --- | --- |
| `return` | The units | A refund of what they are worth, less any restocking fee and shipping deduction |
| `exchange` | The units | Other variants on an exchange order, and a refund of any difference |
| `claim` | Nothing | A refund, or the same variants sent again, as its `claim_type` says |

A return the shopper asks for starts `requested`. One you open, and every claim, starts `approved`.

| Status | What it means |
| --- | --- |
| `requested` | The shopper asked. It waits for you to approve or decline it. |
| `approved` | You agreed, and the goods can come back. |
| `rejected` | You declined it, and `rejection_reason` says why. |
| `received` | The goods are back. |
| `refunded` | We refunded it, and `refund_id` names the refund. |
| `completed` | It was settled with nothing to refund: an exchange with no difference to give back, fees that took the whole value, or a replaced claim. |
| `canceled` | You cancelled it before the goods were back, or a claim before it was settled. |

Units are worth their share of what is left to refund on their order item, so discounts and earlier refunds count. Amounts are in minor units of the return's `currency_code`. The shopper sees each step on their order's timeline, and you see it on yours.

### What can come back

- Only units that shipped, and only as many as are not already returned, refunded, or named by another open return or a claim. A gift card never comes back: refund it on the order while it is unspent.
- A shopper asks within the store's returns window: `returns_window_days` days from when the item was delivered, or shipped when no delivery is recorded. When it arrived in more than one shipment, the latest one counts. You can open a return yourself at any time.
- An order takes up to 50 returns, whatever their status. A cancelled order takes none, and neither does an exchange order.
- A return names up to 100 order items, each once, and an exchange up to 50 new variants.

## Set the returns window and the reasons

The returns window is 30 days unless you set it. Set it with `PATCH /commerce/store`, which takes `commerce.store:write` or the Admin role:

```bash
curl -X PATCH "https://api.coritan.com/api/v1/orgs/acme/commerce/store" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"settings": {"returns_window_days": 14}}'
```

The window is a whole number of days. `0` stops shoppers from asking, and `null` brings back the 30 days. [Change the store's settings](/docs/organizations/storefront/commerce-api/#change-the-store-s-settings) lists the other settings.

Return reasons are what a shopper picks from when they say why. Add one with `POST /commerce/return-reasons`:

```bash
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/return-reasons" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"code": "too_small", "label": "It is too small", "description": "For clothes and shoes that do not fit.", "rank": 10}'
```

| Field | What it holds |
| --- | --- |
| `code` | 1 to 64 lowercase letters, digits, hyphens or underscores, starting with a letter or digit. It never changes. |
| `label` | What the shopper reads, up to 255 characters. |
| `description` | More about it, up to 500 characters. |
| `rank` | Its place in the list, from 0 to 100,000, lowest first. It is 0 unless you set it. |
| `is_disabled` | `true` hides it from shoppers. |

The answer is `201` with the `return_reason`.

- `GET /commerce/return-reasons` lists every reason in the order shoppers see them, disabled ones included. Shoppers see only the enabled ones.
- `PATCH /commerce/return-reasons/{reason_id}` changes the `label`, `description`, `rank` or `is_disabled`. To use another code, add a new reason and disable the old one.
- `DELETE /commerce/return-reasons/{reason_id}` deletes a reason that no return names. We keep a reason that a return names and disable it instead, and the answer says `deleted: false`.

A shopper's item may name the `code` of an enabled reason in its `reason_code`, and an item of a return you open may name any of the store's reasons. Another code answers `422`.

## Open a return yourself

Open a return for a shopper who asks you directly, or after the returns window has closed. `POST /commerce/returns` names the order by its `id` or its `order_` ID, and the units in `items`:

```bash
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/returns" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Idempotency-Key: return-1043-1" \
  -H "Content-Type: application/json" \
  -d '{
    "order_id": "order_01j8z3k4m5n6p7q8r9s0t1v2w3",
    "items": [{"order_item_id": 5521, "quantity": 1, "reason_code": "too_small"}],
    "instructions": "Pack the shoes in their box and take the parcel to any DHL point.",
    "label_url": "https://shop.example.com/labels/1043.pdf"
  }'
```

| Field | What it takes |
| --- | --- |
| `items` | Each `order_item_id` from the order's `items` and a `quantity`, with an optional `reason_code` and a `note` of up to 500 characters. |
| `kind` | `return`, `exchange` or `claim`. It is `exchange` when you send `exchange_items`, and `return` otherwise. |
| `exchange_items` | For an exchange, each `variant_id` and `quantity` to send instead. |
| `claim_type` | For a claim, `refund` or `replace`. |
| `restocking_fee`, `shipping_deduction` | Minor units to keep back from the refund. |
| `waive_difference` | `true` sends an exchange's new items when they cost more than what comes back. |
| `location_id` | The stock location that sellable units go back to. |
| `instructions`, `label_url` | How to send the goods back: text of up to 2,000 characters, and an `https` link to a return label of up to 1,000 characters. |
| `note` | A note of up to 2,000 characters. The shopper sees it on the return. |

The answer is `201` with the `return`, already `approved`. The shopper reads your `instructions` and `label_url` on the return in the Store API.

## Approve or decline a request

Find the requests that wait for you with `GET /commerce/returns?status=requested`. The list is newest first, and `count` is every return the filters match.

| Filter | Takes |
| --- | --- |
| `status` | A status from [How a return moves](#how-a-return-moves) |
| `kind` | `return`, `exchange` or `claim` |
| `order` | An order's `id` or its `order_` ID |
| `page`, `per_page` | Up to 100 a page; 20 by default |

`GET /commerce/returns/{return_id}` answers one return, with each item's `quantity`, `reason_code` and `note`, and the order it belongs to in `order_id` and `order_display_id`.

To approve a request, send the fees and how to send the goods back:

```bash
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/returns/214/approve" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Idempotency-Key: return-214-approve" \
  -H "Content-Type: application/json" \
  -d '{"restocking_fee": 300, "instructions": "Pack the items in their box and take the parcel to any DHL point.", "label_url": "https://shop.example.com/labels/214.pdf"}'
```

The body is optional, and takes `restocking_fee`, `shipping_deduction`, `waive_difference`, `location_id`, `instructions` and `label_url` as [Open a return yourself](#open-a-return-yourself) describes. The answer is `200` with the return, now `approved`, and `refund_amount` says what it refunds if every unit comes back. The fee and the deduction together cannot be more than the return refunds.

To decline a request, say why. The shopper reads the `reason`, which is up to 500 characters:

```bash
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/returns/214/reject" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Idempotency-Key: return-214-reject" \
  -H "Content-Type: application/json" \
  -d '{"reason": "The shoes have been worn outside, so we cannot take them back."}'
```

The return becomes `rejected`, and the shopper can ask for its units again while the window is open. You can approve or decline only a `requested` return.

## Receive the goods

When the parcel arrives, record what came back and in what `condition`: `sellable`, which goes back in stock, or `damaged`, which does not.

```bash
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/returns/214/receive" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Idempotency-Key: return-214-receive" \
  -H "Content-Type: application/json" \
  -d '{"items": [{"order_item_id": 5521, "quantity": 1, "condition": "sellable"}, {"order_item_id": 5522, "quantity": 1, "condition": "damaged"}], "location_id": 4}'
```

- Without `items`, every unit comes back sellable. Name an item twice to split its units between the two conditions.
- An item can come back with fewer units than the return names, even none, as long as one unit comes back in all. An item you leave out of `items` did not come back.
- Sellable units go back in stock at the `location_id` you send, else the one the return names, else where they shipped from.
- You can receive only an `approved` return, and never a claim.

The answer is `200` with the `return`, now `received`. Each item shows its `received_quantity`, its `conditions`, and a `condition` of `sellable`, `damaged` or `mixed`. `refund_amount` is what the units that came back are worth, less the fee, the deduction and an exchange's new items. When that leaves nothing on a return, it is `completed` at once.

On the order, each item's `returned_quantity` counts the units that came back. The order's `fulfillment_status` becomes `partially_returned`, or `returned` once every unit it ships has come back.

## Refund a return

`POST /commerce/returns/{return_id}/refund` settles a `received` return or an `approved` claim:

```bash
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/returns/214/refund" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Idempotency-Key: return-214-refund"
```

We refund the `refund_amount` as any order refund: the card or PayPal payment first, then any gift cards the shopper paid with, with a credit note, as [Refund or cancel an order](/docs/organizations/storefront/commerce-api/#refund-or-cancel-an-order) describes. We email the shopper, and send `commerce.payment.refunded` as well as the return's own webhook.

The answer is `200` with the `return`, now `refunded` with its `refund_id`, and the `refund`. When nothing is left to refund, the return becomes `completed` and `refund` is `null`. The body is optional: a `reason` of up to 64 characters for the refund, which is the return's `kind` unless you send one, and a `note` of up to 500 characters.

> [!WARNING]
> A refund sends money back to the shopper at once, and cannot be undone.

Refunding an open return's units on the order, by `items`, answers `409` with `units_in_return`: refund them through the return. Once the goods are in, the order keeps their value for the return, and its items' `refundable_quantity` leaves them out.

## Exchange items

Name the new variants in `exchange_items`, each with a `variant_id` and a `quantity`. We price them as the order was priced: in its currency, for its region and the customer's groups, with tax for its address. Each must be on sale in the order's sales channel and in stock, and cannot be a gift card. A variant with no price in the order's currency answers `409` with `price_unavailable`, and one whose tax we cannot work out for the address answers `409` with `tax_unavailable`.

`difference_amount` is what the new items cost, less what the units coming back are worth:

- At zero or below, the exchange refunds what is left over, less any fee and deduction.
- Above zero, the shopper would owe you the difference, and we cannot collect it. Asking for the exchange, opening it and approving it answer `409` with `exchange_payment_required`. Send `waive_difference: true` when you open, approve or refund the exchange to send the new items anyway, and `difference_waived` shows it.

When you receive an exchange, we work the difference out again from the units that came back. If it is covered or waived, we create an *exchange order* that sends the new items. It has the order's addresses, costs nothing, and ships like any order. The return's `exchange_order_id` names it, and the exchange order's `exchange_for` names the return.

When you also hold `commerce.refunds:write` or the Billing role, receiving refunds the difference at once, and the answer's `refund` holds the refund. When that refund cannot be made, `refund_error` holds the error with its `status`, and the return stays `received` until you refund it.

An exchange can wait after you receive it, with no exchange order, in two cases. The order's timeline notes each one for you, and the shopper does not see it:

- The units that came back are worth less than the new items, and you did not waive the difference. Refund it with `waive_difference: true` to send the new items, or with `as_return: true` to refund the units that came back instead.
- The new items are out of stock. Add stock, then refund the return, which sends them first. Or refund it with `as_return: true`.

`as_return: true` settles the exchange as a return: it drops the new items and refunds the units that came back, less the fee and the deduction. It works only before the exchange order exists.

An exchange order takes no refund and no return. Cancel it before it ships, and what its new items took is refundable again on the order they came from. In the Store API, the shopper's view of the return has the exchange order's `order_` ID in `exchange_order_id`, and an `exchange_order_access_token` that opens it.

## Settle a claim

Open a claim for units that were damaged or lost after they shipped, with `kind` set to `claim` and a `claim_type`:

```bash
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/returns" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Idempotency-Key: claim-1043-1" \
  -H "Content-Type: application/json" \
  -d '{"order_id": 1742, "kind": "claim", "claim_type": "replace", "items": [{"order_item_id": 5521, "quantity": 1}], "note": "The parcel arrived crushed."}'
```

A claim starts `approved` and brings nothing back, so it takes no fee, no deduction and no `exchange_items`. Its units are held for it from the start: they cannot be refunded on the order or named in a return. Settle it with `POST /commerce/returns/{return_id}/refund`, which takes `commerce.refunds:write` or the Billing role for a replacement too:

- A `refund` claim refunds what the units are worth, and becomes `refunded`.
- A `replace` claim sends the same variants again on an exchange order at no charge, and becomes `completed`.

## Cancel a return

`POST /commerce/returns/{return_id}/cancel` ends a return that is `requested` or `approved`, before its goods are back, or a claim before it is settled. You can send a `reason` of up to 500 characters, and the shopper reads it on their order's timeline.

```bash
curl -X POST "https://api.coritan.com/api/v1/orgs/acme/commerce/returns/214/cancel" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Idempotency-Key: return-214-cancel" \
  -H "Content-Type: application/json" \
  -d '{"reason": "The shopper decided to keep the shoes."}'
```

The return becomes `canceled`, and its units can be named in a new return.

> [!CAUTION]
> A cancelled return cannot be opened again. To take the units back later, the shopper asks again while the window is open, or you open a new return.

## Have a 3PL receive the goods

A [webhook provider](/docs/organizations/storefront/commerce-api/#connect-a-fulfilment-provider) can receive returned goods for you. Turn it on in the provider's `config`, which takes `commerce.store:write` or the Admin role:

```bash
curl -X PATCH "https://api.coritan.com/api/v1/orgs/acme/commerce/fulfillment-providers/3" \
  -H "X-API-Key: $ORG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"config": {"returns": true}}'
```

When you approve or open a return of units that such a provider shipped, we send the provider a `return.requested`. We sign and retry it as [Check what we send to a provider](/docs/organizations/storefront/commerce-api/#check-what-we-send-to-a-provider) describes. It holds:

- the `order`, with its `id`, `display_id`, `email` and `currency_code`;
- the `return`, with its `id`, `kind`, `note`, `instructions` and `label_url`, the `location` the goods go to, the `shopper_address`, and `requested_at` and `approved_at`;
- the `items`, each with its `reason_code`.

A `2xx` answer's `reference` becomes the return's `provider_reference`. When you cancel a return that the provider has received, we send it a `return.canceled` with the return's `reference` and your `reason`. A claim sends the provider nothing.

The provider reports the goods with a `return.received` event, which it sends as [Send events from the provider](/docs/organizations/storefront/commerce-api/#send-events-from-the-provider) describes:

```json
{
  "id": "rma-2291",
  "type": "return.received",
  "data": {
    "return_id": 214,
    "items": [{"sku": "SHOE-42", "quantity": 1, "condition": "sellable"}]
  }
}
```

`items` names units by `sku` or `order_item_id`, each with a `quantity` and a `condition`. Without `items`, every unit comes back sellable. Sellable units go back in stock at the `location_id` in `data`, which must be a location the provider runs, else where the `return.requested` sent them. The provider's receipt refunds nothing, so refund the return yourself. An exchange's new items still go out.

`GET /commerce/fulfillment-providers/{provider_id}/deliveries` lists these messages too, and its `kind` filter takes `return_create` and `return_cancel`.

## Return webhooks

These reach your organization webhooks as [Receive commerce webhooks](/docs/organizations/storefront/commerce-api/#receive-commerce-webhooks) describes. The `data` of each `commerce.return` event holds the `return_id`, `order_id`, `public_id`, `kind`, `claim_type`, `status`, `currency_code` and `livemode`, and the fields below.

| Event | When | Also in `data` |
| --- | --- | --- |
| `commerce.return.requested` | A shopper asks, or you open a return. | `requested_by`, `items`, `exchange_items` |
| `commerce.return.approved` | You approve a request, or open a return. | `restocking_fee`, `shipping_deduction`, `refund_amount`, `provider_id` |
| `commerce.return.rejected` | You decline a request. | `reason` |
| `commerce.return.received` | The goods are back. | `received_by`, `items`, `refund_amount`, `difference_amount` |
| `commerce.return.refunded` | Its refund went through. | `refund_id`, `amount` |
| `commerce.return.completed` | It was settled with nothing to refund. | `refund_amount`, `exchange_order_id` |
| `commerce.return.canceled` | You cancel it. | `reason` |

`commerce.exchange.created` follows when an exchange order is made. Its `data` holds the `return_id`, `kind`, the original `order_id` and `public_id`, the `exchange_order_id`, `exchange_public_id` and `display_id` of the new order, its `items` and `livemode`.

## Limits and retries

Every write on a return takes an optional `Idempotency-Key` header of up to 128 characters. A retry with the same key within 24 hours gets the first answer back and changes nothing. The same key with another body, or for another return, answers `409` with `idempotency_key_reused`, and a retry while the first request runs answers `409` with `idempotency_in_progress`. A request that fails keeps no key. The reason routes take no key.

Refunding a return counts towards the organization's limit of 30 refunds and cancellations a minute, retries included, and so does the refund made when you receive an exchange. Beyond it, the refund route answers `429` with `rate_limited` and a `Retry-After` of 60 seconds, and receiving an exchange puts that error in `refund_error`.

## Result

A return you refunded shows `refunded`, with the `refund_id` of a refund on its order and a credit note. One settled without a refund shows `completed`. The order lists each of its returns under `returns`, with its `status`, and its items' `returned_quantity` counts what came back.

## Troubleshooting

`409` with `item_not_returnable`
: The units have not shipped, or are already returned, refunded, or named by another open return or a claim. `returnable_quantity` says how many of the item can come back now. A gift card never comes back: refund it on the order while it is unspent.

`409` with `return_window_closed`
: The shopper asked after the returns window closed, at the time in `closed_at`. You can still open the return yourself.

`409` with `invalid_return_status`
: The return's `status` does not allow that step, and `allowed` lists the statuses that do. A claim is never received: refund it to settle it.

`409` with `exchange_payment_required`
: The new items cost `difference_amount` more than what comes back. Send `waive_difference: true` to send them anyway, or, once the goods are in, refund with `as_return: true`.

`422` with `fees_too_large`
: The restocking fee and the shipping deduction come to more than the return refunds. `refundable` is the most they can be together.

`409` with `insufficient_inventory`
: The new items of an exchange are out of stock. Add stock and refund the return again, or refund it with `as_return: true`.

`409` with `exchange_sent`
: The exchange's new items have gone out on an exchange order, so it cannot be settled as a return.

`409` with `units_in_return`
: The order refund named units that an open return holds, and `return_id` says which. Refund them through the return, or cancel the return first.

`409` with `exchange_order`
: The order sends an exchange's new items, so it takes no return and no refund. Cancel it before it ships, and its value is refundable again on the original order.

`409` with `refund_in_progress`
: A refund of this return is waiting for the payment provider's answer. Wait for it before you refund or cancel the return.

`409` with `too_many_returns`
: The order has 50 returns already.

`409` with `reason_code_taken`
: The store has a reason with that `code`. Pick another code, or change the reason that has it.

`502` with `refund_failed`
: The payment provider refused the refund, and its reason is in the `message`. The return stays `received`, or `approved` for a claim, so you can refund it again.

## Related

- [Sell with the Commerce API](/docs/organizations/storefront/commerce-api/)
- [Build a checkout with the Store API](/docs/organizations/storefront/store-api/)
- [Receive organization webhooks](/docs/organizations/webhooks/)

## API

- `GET /api/v1/orgs/{org_slug}/commerce/returns`: Newest first; count is every return the filters match (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-returns)
- `POST /api/v1/orgs/{org_slug}/commerce/returns`: Create return (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-returns)
- `GET /api/v1/orgs/{org_slug}/commerce/returns/{return_id}`: Get return (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-returns-return-id)
- `POST /api/v1/orgs/{org_slug}/commerce/returns/{return_id}/approve`: Approve return (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-returns-return-id-approve)
- `POST /api/v1/orgs/{org_slug}/commerce/returns/{return_id}/reject`: Decline a requested return; the shopper reads the reason (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-returns-return-id-reject)
- `POST /api/v1/orgs/{org_slug}/commerce/returns/{return_id}/receive`: Receive return (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-returns-return-id-receive)
- `POST /api/v1/orgs/{org_slug}/commerce/returns/{return_id}/refund`: Refund return (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-returns-return-id-refund)
- `POST /api/v1/orgs/{org_slug}/commerce/returns/{return_id}/cancel`: End a return before its goods are in, or a claim before it is settled (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-returns-return-id-cancel)
- `GET /api/v1/orgs/{org_slug}/commerce/return-reasons`: Every reason, disabled ones included, in the order shoppers see them (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-get-api-v1-orgs-org-slug-commerce-return-reasons)
- `POST /api/v1/orgs/{org_slug}/commerce/return-reasons`: A reason shoppers may pick for a return (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-post-api-v1-orgs-org-slug-commerce-return-reasons)
- `PATCH /api/v1/orgs/{org_slug}/commerce/return-reasons/{reason_id}`: Label, description, rank or whether shoppers may pick it; the code never changes (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-patch-api-v1-orgs-org-slug-commerce-return-reasons-reason-id)
- `DELETE /api/v1/orgs/{org_slug}/commerce/return-reasons/{reason_id}`: Delete return reason (https://www.coritan.com/docs/api/reference/organizations/commerce/commerce/#op-delete-api-v1-orgs-org-slug-commerce-return-reasons-reason-id)
