> ## Documentation Index
> Fetch the complete documentation index at: https://docs-test.rye.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Returns

> Open a return against a completed order and follow it through to the shopper's refund.

The Returns API lets you open a return against a completed order and follow it
through to a refund. Returns are whole-order: you return the entire order rather
than individual line items. Once created, a return progresses asynchronously
toward the shopper's refund.

<Note>
  The Returns API currently supports Shopify orders only. Support for additional
  marketplaces will be added over time.
</Note>

## Creating a return

Call [`POST /api/v1/returns`](/api-v2/api-reference/returns/create-return) with
the Rye `orderId` and a `reason` (such as `defective`, `wrong_item`, or
`unwanted`). The order must be `completed`.

<CodeGroup>
  ```bash curl theme={null}
  curl --request POST \
    --url https://api.rye.com/api/v1/returns \
    --header "Authorization: Bearer $RYE_API_KEY" \
    --header 'Content-Type: application/json' \
    --data '{ "orderId": "order_8c2b…", "reason": "defective" }'
  ```

  ```javascript TypeScript SDK theme={null}
  import CheckoutIntents from 'checkout-intents';

  const client = new CheckoutIntents({ apiKey: process.env['RYE_API_KEY'] });

  const ret = await client.returns.create({
    orderId: 'order_8c2b…',
    reason: 'defective',
  });
  ```

  ```python Python SDK theme={null}
  import os
  from checkout_intents import CheckoutIntents

  client = CheckoutIntents(api_key=os.environ["RYE_API_KEY"])

  ret = client.returns.create(
      order_id="order_8c2b…",
      reason="defective",
  )
  ```
</CodeGroup>

A new return starts in `requested`, with only `requestedAt` set on its timeline.
The [reference](/api-v2/api-reference/returns/create-return) has the full request
and response shape.

Example response:

```json theme={null}
{
  "id": "ret_3f9a…",
  "state": "requested",
  "orderId": "order_8c2b…",
  "checkoutIntentId": "ci_…",
  "reason": "defective",
  "timeline": {
    "requestedAt": "2026-06-19T17:04:11.220Z"
  },
  "createdAt": "2026-06-19T17:04:11.220Z",
  "updatedAt": "2026-06-19T17:04:11.220Z"
}
```

`reason` accepts any of `defective`, `wrong_item`, `unwanted`, `color`,
`not_as_described`, `size_too_large`, `size_too_small`, `style`, or `other`.

## Lifecycle

A return moves through the states below and ends in `refunded`, `denied`, or
`failed`. The `state` field tells you where it is, and which details are present
on the response (a shipping label, a denial reason, or the refund).

```mermaid theme={null}
stateDiagram-v2
    [*] --> requested: Create return
    requested --> requires_action: Approved, items shipped back
    requested --> processing: Approved, no shipping needed
    requested --> denied: Declined
    requires_action --> refunded: Refund issued
    processing --> refunded: Refund issued
    requested --> failed
    requires_action --> failed
    processing --> failed
    refunded --> [*]
    denied --> [*]
    failed --> [*]
```

## Following a return

Poll [`GET /api/v1/returns/{returnId}`](/api-v2/api-reference/returns/get-return)
to track a return until it reaches a terminal state. Once it is `refunded`, the
response reports the amount returned to the shopper.

<CodeGroup>
  ```bash curl theme={null}
  curl --request GET \
    --url https://api.rye.com/api/v1/returns/ret_3f9a… \
    --header "Authorization: Bearer $RYE_API_KEY"
  ```

  ```javascript TypeScript SDK theme={null}
  import CheckoutIntents from 'checkout-intents';

  const client = new CheckoutIntents({ apiKey: process.env['RYE_API_KEY'] });

  const ret = await client.returns.retrieve('ret_3f9a…');
  ```

  ```python Python SDK theme={null}
  import os
  from checkout_intents import CheckoutIntents

  client = CheckoutIntents(api_key=os.environ["RYE_API_KEY"])

  ret = client.returns.retrieve("ret_3f9a…")
  ```
</CodeGroup>

Once the return reaches `refunded`, the response carries the reconciled
timestamps and a `refunds` array reporting the amount returned to the shopper.

Example response:

```json theme={null}
{
  "id": "ret_3f9a…",
  "state": "refunded",
  "orderId": "order_8c2b…",
  "checkoutIntentId": "ci_…",
  "reason": "defective",
  "timeline": {
    "requestedAt": "2026-06-19T17:04:11.220Z",
    "returnApprovedAt": "2026-06-19T18:10:02.000Z",
    "refundIssuedAt": "2026-06-20T09:30:44.000Z",
    "refundedAt": "2026-06-20T09:31:10.000Z"
  },
  "nextAction": {
    "type": "ship_items_to_merchant",
    "shipItemsToMerchant": {
      "label": { "url": "https://…/return-label.pdf" }
    }
  },
  "refunds": [
    {
      "id": "refund_…",
      "refundedAt": "2026-06-20T09:31:10.000Z",
      "shopperRefundTotal": { "amountSubunits": 2300, "currencyCode": "USD" }
    }
  ],
  "createdAt": "2026-06-19T17:04:11.220Z",
  "updatedAt": "2026-06-20T09:31:10.000Z"
}
```

`shopperRefundTotal.amountSubunits` is in minor units (cents for USD), so `2300`
is \$23.00.

### Handling `requires_action`

When a return reaches `requires_action`, the shopper needs to ship the items
back before the merchant will refund. The response carries a `nextAction` with a
prepaid return label at `nextAction.shipItemsToMerchant.label.url`. Forward that
URL to the shopper so they can print the label and send the items in.

```json theme={null}
{
  "id": "ret_3f9a…",
  "state": "requires_action",
  "orderId": "order_8c2b…",
  "checkoutIntentId": "ci_…",
  "reason": "defective",
  "timeline": {
    "requestedAt": "2026-06-19T17:04:11.220Z",
    "returnApprovedAt": "2026-06-19T18:10:02.000Z"
  },
  "nextAction": {
    "type": "ship_items_to_merchant",
    "shipItemsToMerchant": {
      "label": { "url": "https://…/return-label.pdf" }
    }
  },
  "createdAt": "2026-06-19T17:04:11.220Z",
  "updatedAt": "2026-06-19T18:10:02.000Z"
}
```

Once the shopper ships the items and the merchant issues the refund, the return
advances to `refunded` on its own. No further calls are required. When a
merchant approves a return without needing the items back, `nextAction.type` is
`no_action_required` (with no `shipItemsToMerchant` payload) and the return skips
straight to `processing`.

<Info>
  If the original order was paid from your drawdown balance, Rye credits that
  balance when it reconciles the refund. Orders paid by other methods refund the
  shopper's own funds and don't affect your balance.
</Info>

## Testing in staging

In staging there is no real merchant to approve a return or issue the refund, so
a real return would sit in `requested` indefinitely. The test-helper endpoints
let you create a simulated return and drive it through each state yourself, so
you can exercise your integration end to end.

<Note>
  Test helpers are available in staging only (they return `404` in production)
  and require an API key with the `test_helpers:write` scope.
</Note>

### Create a simulated return

Create a simulated return for an order. Unlike `POST /api/v1/returns`, this skips
the merchant and hands you direct control over the lifecycle. Only `orderId` is
required; `reason` defaults to `other`, and omitting `lineItems` returns the
whole order. The response includes the new return's `id`.

<CodeGroup>
  ```bash curl theme={null}
  curl --request POST \
    --url https://staging.api.rye.com/api/v1/test-helpers/returns \
    --header "Authorization: Bearer $RYE_API_KEY" \
    --header 'Content-Type: application/json' \
    --data '{ "orderId": "order_8c2b…" }'
  ```

  ```javascript TypeScript SDK theme={null}
  import CheckoutIntents from 'checkout-intents';

  const client = new CheckoutIntents({
    apiKey: process.env['RYE_API_KEY'],
    baseURL: 'https://staging.api.rye.com',
  });

  const ret = await client.testHelpers.returns.create({ orderId: 'order_8c2b…' });
  ```

  ```python Python SDK theme={null}
  import os
  from checkout_intents import CheckoutIntents

  client = CheckoutIntents(
      api_key=os.environ["RYE_API_KEY"],
      base_url="https://staging.api.rye.com",
  )

  ret = client.test_helpers.returns.create(order_id="order_8c2b…")
  ```
</CodeGroup>

Example response:

```json theme={null}
{
  "id": "ret_3f9a…",
  "state": "requested",
  "orderId": "order_8c2b…",
  "checkoutIntentId": "ci_…",
  "reason": "other",
  "timeline": {
    "requestedAt": "2026-06-19T17:04:11.220Z"
  },
  "createdAt": "2026-06-19T17:04:11.220Z",
  "updatedAt": "2026-06-19T17:04:11.220Z"
}
```

### Approve the return

Approve it to move it forward. `ship_items_to_merchant` (the default) lands the
return in `requires_action` with a prepaid label; `no_action_required` skips
straight to `processing`.

<CodeGroup>
  ```bash curl theme={null}
  curl --request POST \
    --url https://staging.api.rye.com/api/v1/test-helpers/returns/ret_3f9a…/approve \
    --header "Authorization: Bearer $RYE_API_KEY" \
    --header 'Content-Type: application/json' \
    --data '{ "nextAction": "ship_items_to_merchant" }'
  ```

  ```javascript TypeScript SDK theme={null}
  const ret = await client.testHelpers.returns.approve('ret_3f9a…', {
    nextAction: 'ship_items_to_merchant',
  });
  ```

  ```python Python SDK theme={null}
  ret = client.test_helpers.returns.approve(
      "ret_3f9a…",
      next_action="ship_items_to_merchant",
  )
  ```
</CodeGroup>

Example response:

```json theme={null}
{
  "id": "ret_3f9a…",
  "state": "requires_action",
  "orderId": "order_8c2b…",
  "checkoutIntentId": "ci_…",
  "reason": "other",
  "timeline": {
    "requestedAt": "2026-06-19T17:04:11.220Z",
    "returnApprovedAt": "2026-06-19T18:10:02.000Z"
  },
  "nextAction": {
    "type": "ship_items_to_merchant",
    "shipItemsToMerchant": {
      "label": { "url": "https://…/return-label.pdf" }
    }
  },
  "createdAt": "2026-06-19T17:04:11.220Z",
  "updatedAt": "2026-06-19T18:10:02.000Z"
}
```

### Issue the refund

Issue the refund to reach the terminal `refunded` state. `costBearer` controls
who absorbs the refund and defaults to `shopper`.

<CodeGroup>
  ```bash curl theme={null}
  curl --request POST \
    --url https://staging.api.rye.com/api/v1/test-helpers/returns/ret_3f9a…/refund \
    --header "Authorization: Bearer $RYE_API_KEY"
  ```

  ```javascript TypeScript SDK theme={null}
  const ret = await client.testHelpers.returns.refund('ret_3f9a…');
  ```

  ```python Python SDK theme={null}
  ret = client.test_helpers.returns.refund("ret_3f9a…")
  ```
</CodeGroup>

Example response:

```json theme={null}
{
  "id": "ret_3f9a…",
  "state": "refunded",
  "orderId": "order_8c2b…",
  "checkoutIntentId": "ci_…",
  "reason": "other",
  "timeline": {
    "requestedAt": "2026-06-19T17:04:11.220Z",
    "returnApprovedAt": "2026-06-19T18:10:02.000Z",
    "refundIssuedAt": "2026-06-20T09:30:44.000Z",
    "refundedAt": "2026-06-20T09:31:10.000Z"
  },
  "nextAction": {
    "type": "ship_items_to_merchant",
    "shipItemsToMerchant": {
      "label": { "url": "https://…/return-label.pdf" }
    }
  },
  "refunds": [
    {
      "id": "refund_…",
      "refundedAt": "2026-06-20T09:31:10.000Z",
      "shopperRefundTotal": { "amountSubunits": 2300, "currencyCode": "USD" }
    }
  ],
  "createdAt": "2026-06-19T17:04:11.220Z",
  "updatedAt": "2026-06-20T09:31:10.000Z"
}
```

### The deny and fail branches

To exercise the other branches, call `/deny` or `/fail` on the return instead.
Denying lands it in `denied` with a `denial` object carrying a machine-readable
`reason` (`final_sale`, `return_period_ended`, or the default `other`) and an
optional `note`. Failing lands it in `failed` with a `failure` object carrying a
`code` (`drawdown_credit_failed`, `merchant_unreachable`, or `other`) and a
human-readable `message`.

<CodeGroup>
  ```bash curl theme={null}
  curl --request POST \
    --url https://staging.api.rye.com/api/v1/test-helpers/returns/ret_3f9a…/deny \
    --header "Authorization: Bearer $RYE_API_KEY" \
    --header 'Content-Type: application/json' \
    --data '{ "reason": "final_sale", "note": "Past the return window" }'
  ```

  ```javascript TypeScript SDK theme={null}
  const ret = await client.testHelpers.returns.deny('ret_3f9a…', {
    reason: 'final_sale',
    note: 'Past the return window',
  });
  ```

  ```python Python SDK theme={null}
  ret = client.test_helpers.returns.deny(
      "ret_3f9a…",
      reason="final_sale",
      note="Past the return window",
  )
  ```
</CodeGroup>

To simulate a failed return, call `/fail`:

<CodeGroup>
  ```bash curl theme={null}
  curl --request POST \
    --url https://staging.api.rye.com/api/v1/test-helpers/returns/ret_3f9a…/fail \
    --header "Authorization: Bearer $RYE_API_KEY"
  ```

  ```javascript TypeScript SDK theme={null}
  const ret = await client.testHelpers.returns.fail('ret_3f9a…');
  ```

  ```python Python SDK theme={null}
  ret = client.test_helpers.returns.fail("ret_3f9a…")
  ```
</CodeGroup>

Every test-helper call returns the updated return in the same shape as
`GET /api/v1/returns/{returnId}`, so your polling sees exactly what it would in
production.

<Tip>
  Get an order's `orderId` from the order on its checkout intent
  (`GET /api/v1/checkout-intents/{id}/order`). A return can only be created when
  the order has no other active return; once a return reaches a terminal state
  (`refunded`, `denied`, or `failed`) you can open another.
</Tip>

### Error responses

Errors come back as a JSON object with a `message` field and a `traceId` you can
quote to support. The common cases on the Returns endpoints:

* **404 Not Found** for an unknown `returnId` or `orderId`, or a return owned by
  another developer. The body looks like `{ "message": "Not Found" }`.
* **409 Conflict** for an out-of-order transition (for example issuing a refund
  before the return is approved), or for creating a second return while one is
  already active for the order.
* **400 Bad Request** for an invalid body, such as an unrecognized `reason` or
  `costBearer`, an unknown `orderLineItemId`, or a non-positive or non-integer
  `quantity`.

Transitions are not idempotent. Replaying a transition on an already-advanced
return returns `409` rather than re-running it, so guard your retries on the
return's current `state` rather than blindly resending.
