Skip to main content
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.
The Returns API currently supports Shopify orders only. Support for additional marketplaces will be added over time.

Creating a return

Call POST /api/v1/returns with the Rye orderId and a reason (such as defective, wrong_item, or unwanted). The order must be completed.
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" }'
A new return starts in requested, with only requestedAt set on its timeline. The reference has the full request and response shape. Example response:
{
  "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).

Following a return

Poll GET /api/v1/returns/{returnId} to track a return until it reaches a terminal state. Once it is refunded, the response reports the amount returned to the shopper.
curl --request GET \
  --url https://api.rye.com/api/v1/returns/ret_3f9a… \
  --header "Authorization: Bearer $RYE_API_KEY"
Once the return reaches refunded, the response carries the reconciled timestamps and a refunds array reporting the amount returned to the shopper. Example response:
{
  "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.
{
  "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.
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.

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.
Test helpers are available in staging only (they return 404 in production) and require an API key with the test_helpers:write scope.

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.
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…" }'
Example response:
{
  "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.
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" }'
Example response:
{
  "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.
curl --request POST \
  --url https://staging.api.rye.com/api/v1/test-helpers/returns/ret_3f9a…/refund \
  --header "Authorization: Bearer $RYE_API_KEY"
Example response:
{
  "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.
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" }'
To simulate a failed return, call /fail:
curl --request POST \
  --url https://staging.api.rye.com/api/v1/test-helpers/returns/ret_3f9a…/fail \
  --header "Authorization: Bearer $RYE_API_KEY"
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.
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.

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.