/

Product

Returns API for Agentic Commerce

Sophia Willows

Head of Engineering @ Rye

7 minutes read

A return is one API call. Rye drives it to completion — filing the return with the merchant, surfacing any next action for the shopper, and tracking the refund through to settlement — so you write one create call and one poll.

TL;DR / Key Takeaways

  • A shopper taps "Return this order." You make one POST /api/v1/returns call with the Rye order id and a reason. Rye drives the return to completion, and you poll GET /api/v1/returns/{returnId} to show the shopper where it stands.

  • Rye files the return with the merchant, surfaces any next action for the shopper (typically a prepaid return label, when the merchant requires the item back), and tracks the refund through to completion.

  • Every return comes back as one flat, typed response: a state, a timeline of timestamps, and — when refunded — the exact shopperRefundTotal to pay the shopper.

  • The developer writes a create call and a poll. No merchant integration, no label generation, no refund reconciliation.

  • The first release covers Shopify merchants and whole-order returns. Partial returns, a list endpoint, webhooks, and more platforms are on the roadmap.

Trust Runs Through the Return

Trust is the gate for agentic commerce — most shoppers still won't hand an agent their card, and the research keeps landing on the same reasons (see what it takes for consumers to trust agentic commerce). A big one sits after the purchase: if an agent buys for me, can I send it back, and will the refund actually come? For an agent to be trusted with the purchase, the return behind it has to be handled just as cleanly.

Today, Rye makes that part programmatic with the Returns API. For an agent buying across third-party merchants, a return has meant a manual, multi-party process: reach the merchant, get it approved, produce a shipping label, confirm the refund issued, one order at a time through a support queue. The Returns API runs that whole path through your code.

It closes the loop for both sides of the network. Developers building on Rye never touch the merchant relationship or the comms. A merchant putting its catalog in front of Rye's developers knows the return — for Shopify today — is handled programmatically on Rye's side. The full shopping flow, purchase through refund, is accounted for.

What the Returns API Does

One POST /api/v1/returns call kicks off a fully managed return on any completed Shopify order. Rye handles every step that used to be manual:

  • Files the return with the merchant programmatically, so you never touch the merchant relationship or the card.

  • Surfaces any next action for the shopper — typically a prepaid return shipping label at nextAction.shipItemsToMerchant.label.url, when the merchant requires the item back. (Some merchants don't require it; those returns move straight to processing.)

  • Tracks the refund through to completion — Rye reflects the merchant's refund status from filing through issued, with a timestamped timeline, so you don't poll the merchant yourself.

  • Settles the refund and tells you the amount — when a return reaches refunded, the response carries shopperRefundTotal, the exact amount in the shopper's currency to pay back.

What Integrating Looks Like

The whole developer experience is submit, then poll. A shopper taps "Return this order," and you create the return against the Rye order id you got at checkout (the order must be completed):

import CheckoutIntents from "checkout-intents";
const rye = new CheckoutIntents({ apiKey: process.env.CHECKOUT_INTENTS_API_KEY! });

// A shopper tapped "Return this order."
const ret = await rye.returns.create({
  orderId: order.ryeOrderId,   // Rye order id from checkout; the order must be "completed"
  reason:  "defective",        // defective | wrong_item | unwanted | color |
                               // not_as_described | size_too_large |
                               // size_too_small | style | other
});
// ret.state === "requested"

Then poll one endpoint to render the shopper's status:

const ret = await rye.returns.retrieve(returnId);

switch (ret.state) {
  case "requested":        // submitted, awaiting the merchant's decision
  case "requires_action":  // approved — hand the shopper the return label:
                           //   ret.nextAction.shipItemsToMerchant.label.url
  case "processing":       // refund in flight, awaiting settlement
  case "refunded":         // done — ret.refunds[0].shopperRefundTotal is the
                           //   amount to pay the shopper, shaped as a Money:
                           //   { amountSubunits, currencyCode }
  case "denied":           // merchant declined — ret.denial.reason explains
                           //   (final_sale | return_period_ended | other)
  case "failed":           // ret.failure.code + ret.failure.message explain why
}

Create, retrieve, narrow on state. That is the full surface. The same response also carries a timelinerequestedAt, returnApprovedAt, refundIssuedAt, refundedAt — so you can render a timestamped status to the shopper without keeping your own clock.

The whole developer experience is submit, then poll. A shopper taps "Return this order," and you create the return against the Rye order id you got at checkout (the order must be completed):

import CheckoutIntents from "checkout-intents";
const rye = new CheckoutIntents({ apiKey: process.env.CHECKOUT_INTENTS_API_KEY! });

// A shopper tapped "Return this order."
const ret = await rye.returns.create({
  orderId: order.ryeOrderId,   // Rye order id from checkout; the order must be "completed"
  reason:  "defective",        // defective | wrong_item | unwanted | color |
                               // not_as_described | size_too_large |
                               // size_too_small | style | other
});
// ret.state === "requested"

Then poll one endpoint to render the shopper's status:

const ret = await rye.returns.retrieve(returnId);

switch (ret.state) {
  case "requested":        // submitted, awaiting the merchant's decision
  case "requires_action":  // approved — hand the shopper the return label:
                           //   ret.nextAction.shipItemsToMerchant.label.url
  case "processing":       // refund in flight, awaiting settlement
  case "refunded":         // done — ret.refunds[0].shopperRefundTotal is the
                           //   amount to pay the shopper, shaped as a Money:
                           //   { amountSubunits, currencyCode }
  case "denied":           // merchant declined — ret.denial.reason explains
                           //   (final_sale | return_period_ended | other)
  case "failed":           // ret.failure.code + ret.failure.message explain why
}

Create, retrieve, narrow on state. That is the full surface. The same response also carries a timelinerequestedAt, returnApprovedAt, refundIssuedAt, refundedAt — so you can render a timestamped status to the shopper without keeping your own clock.

The whole developer experience is submit, then poll. A shopper taps "Return this order," and you create the return against the Rye order id you got at checkout (the order must be completed):

import CheckoutIntents from "checkout-intents";
const rye = new CheckoutIntents({ apiKey: process.env.CHECKOUT_INTENTS_API_KEY! });

// A shopper tapped "Return this order."
const ret = await rye.returns.create({
  orderId: order.ryeOrderId,   // Rye order id from checkout; the order must be "completed"
  reason:  "defective",        // defective | wrong_item | unwanted | color |
                               // not_as_described | size_too_large |
                               // size_too_small | style | other
});
// ret.state === "requested"

Then poll one endpoint to render the shopper's status:

const ret = await rye.returns.retrieve(returnId);

switch (ret.state) {
  case "requested":        // submitted, awaiting the merchant's decision
  case "requires_action":  // approved — hand the shopper the return label:
                           //   ret.nextAction.shipItemsToMerchant.label.url
  case "processing":       // refund in flight, awaiting settlement
  case "refunded":         // done — ret.refunds[0].shopperRefundTotal is the
                           //   amount to pay the shopper, shaped as a Money:
                           //   { amountSubunits, currencyCode }
  case "denied":           // merchant declined — ret.denial.reason explains
                           //   (final_sale | return_period_ended | other)
  case "failed":           // ret.failure.code + ret.failure.message explain why
}

Create, retrieve, narrow on state. That is the full surface. The same response also carries a timelinerequestedAt, returnApprovedAt, refundIssuedAt, refundedAt — so you can render a timestamped status to the shopper without keeping your own clock.

Tracking a Return

A return reports its status through a small set of states. Render whichever state a return is in now — a merchant can approve and refund in one motion, so a return can move straight past the intermediate steps.

  • Requested — submitted; waiting on the merchant to approve.

  • Requires action — the merchant approved and requires the item shipped back. Hand the shopper the prepaid return label. (Not every merchant requires this — when they don't, the return moves straight to processing.)

  • Processing — the refund is in flight, awaiting settlement.

  • Refunded — done. shopperRefundTotal is the exact amount to pay the shopper.

  • Denied — the merchant declined. denial.reason comes back typed: final_sale, return_period_ended, or other.

  • Failed — the return couldn't complete. failure.code and a stable failure.message say why.

The terminal states — refunded, denied, failed — are final, and each carries a typed payload, so you can build your shopper-facing UI against them with confidence.

Why Returns Are Hard

Merchants don't refund instantly. Approval and refund can land days apart, on the merchant's clock. Each return runs on a durable, crash-safe workflow that waits as long as it takes and survives restarts without losing state.

Returns don't follow one shape. Some merchants require the item shipped back; others refund without it. Some approve and refund in one motion. Rye normalizes that variation into a single typed status surface — one state, one timeline, typed denial and failure reasons — so you build against one flow.

The money has to reconcile. A refund isn't done when the merchant says so; it's done when the funds settle. Rye tracks the refund through to settlement and hands you shopperRefundTotal, the exact amount to pay the shopper.

You get a state to render and a number to pay. None of the rest.

What's in the First Release

  • Shopify merchants, programmatic end to end.

  • Whole-order returns — one call returns every item on the order.

  • One active return per order at a time, with predictable error handling.

  • Generated SDKs in TypeScript, Python, Go, and Java from a single spec, plus a documented REST API.

On the Roadmap

These are deliberate fast-follows — the API was designed with room for them from day one:

  • Partial and item-level returns — return a single item or quantity instead of the whole order.

  • A list endpoint — fetch all returns for a shopper or order.

  • Webhooks — get pushed a status change instead of polling.

  • More merchant platforms beyond Shopify.

Checkout Was Step One

Universal Checkout API made buying across any merchant a single API call. The Returns API makes sending it back one too. Together they make the post-purchase lifecycle programmable: the parts of commerce that used to live in support queues become endpoints you can build against.

Placing an order and taking it back now share the same contract: create, then poll.

Frequently Asked Questions

How do AI shopping agents handle returns?

With Rye, a return is one API call. When a shopper wants to send an order back, the agent calls POST /api/v1/returns and Rye drives the rest — filing with the merchant, surfacing a prepaid shipping label when one's required, and tracking the refund through to settlement. The developer never touches the merchant relationship.

Can you automate returns and refunds with an API?

Yes. Rye's Returns API handles the full returns-and-refunds flow programmatically for Shopify orders: create the return, poll one endpoint for status, and get back the exact shopperRefundTotal to pay the shopper.

How does a developer start a return?

One POST /api/v1/returns with the Rye order id and a reason. The order must be completed. Rye files it with the merchant and drives the rest. You poll GET /api/v1/returns/{returnId} for status.

Which merchants are supported?

The first release covers Shopify merchants, end to end, with more platforms on the roadmap. Rye supports a broader set of merchants for checkout; returns coverage is expanding from Shopify first.

What does the shopper see?

When the merchant requires the item back, the response carries a prepaid shipping label at nextAction.shipItemsToMerchant.label.url to hand the shopper. After the refund settles, the state moves to refunded and shopperRefundTotal is the exact amount to pay back.

How do I track refund timing?

Every return carries a timeline of timestamps — requestedAt, returnApprovedAt, refundIssuedAt, refundedAt — so you can show the shopper exactly where the return stands.

Can I return a single item?

Whole-order returns ship first — one call returns every item on the order. Partial and item-level returns are on the roadmap.

What happens when a return is denied or fails?

Both come back typed. A denial carries denial.reason (final_sale, return_period_ended, or other); a failure carries failure.code and a stable failure.message. You branch on them directly.

Stop the redirect.
Start the revenue.

Stop the redirect.
Start the revenue.

Stop the redirect.
Start the revenue.