Part 1 of 7 · Return and RMA handler series ~10 min read

A return and RMA handler on AWS for a few dollars a month

A customer asking to return something kicks off a surprising amount of dull, error-prone work: check the policy, issue an RMA number, generate a prepaid label, wait for the parcel, inspect what comes back, then decide whether they get their money back or a replacement. Miss the return window and you eat a return you didn’t owe; lose track of the parcel and you refund something that never arrived. This post walks through the design of a small system that runs the whole returns desk as one orchestrated flow — and never moves money on its own.

Key takeaways

  • The whole returns desk runs as one Step Functions state machine: request → eligibility → RMA & label → await inbound → inspect → decide.
  • Eligibility is plain code against your policy — return window, non-returnable flags, condition, and prior returns.
  • It issues a real RMA number and a prepaid carrier label, then tracks the inbound parcel by its tracking number.
  • Once the parcel is received and inspected, it proposes refund or replacement with the reason attached.
  • Designed on AWS for about $2.90/month at roughly 120 returns a month. A human approves every refund or replacement.

The whole system on one page

Before any code, here’s the shape of what we’re designing. A return isn’t a single action — it’s a small process that plays out over days: someone asks, you check the policy, you issue a number and a label, the parcel travels back, somebody opens the box, and only then does anyone decide whether it’s a refund or a replacement. The trouble is that each step usually lives in a different place — an email, a courier portal, a spreadsheet, somebody’s memory — and the gaps between them are where returns get lost, refunded twice, or refunded for goods that never came back. The system below holds the whole process in one place and walks every return through the same stages.

System architecture: a return request, the policy and orders it is checked against, and a Step Functions state machine with five stages inside AWS At the top, three external boxes in a row. Far left, “Customer” — where a return request starts, by a returns form, an email, or a reply to an order. Centre, “Policy & orders” — a Google Drive sheet of orders (order number, customer, items, purchase date, channel) mirrored into AWS, plus a written returns policy doc holding the return window, the non-returnable flags, and the refund-or-replace rules. Far right, “Returns desk” — the person who approves the final refund or replacement. Each connects by an arrow to the AWS account container below: the customer sends a request in, policy and orders ground every check, and the returns desk receives a proposal to approve. Inside the AWS account sits a Step Functions state machine drawn as a row of five stage boxes. First, Eligibility — plain-code checks of the request against the policy. Second, RMA & label — issues a unique RMA number, calls the carrier label API, stores the label, and emails the customer. Third, Await inbound — the execution pauses and waits for the parcel, woken by a real carrier scan. Fourth, Inspect — a staffer records the condition of what arrived. Fifth, Decide — plain-code proposal of refund or replacement with the reason attached. Arrows run left to right through the five stages. Below the Await inbound stage, a small box reads “Carrier tracking — webhooks and a daily sweep” feeding the wait. A note at the bottom reads: eligibility and the decision are plain code; a human on the returns desk approves every refund or replacement, and nothing moves on its own. Customer asks to return something Policy & orders window, flags, orders sheet Returns desk approves refund or replace request in grounds proposal to approve AWS account Step Functions state machine Eligibility check the policy RMA & label issue + email Await inbound track the parcel Inspect record condition Decide refund or replace Carrier tracking webhooks + daily sweep Eligibility and the decision are plain code — a human approves every refund or replacement, and nothing moves on its own.
Fig 1. Three things outside, one state machine inside. The customer’s request, the policy and orders it’s checked against, and the returns desk that approves. Inside AWS, every return walks the same five stages, with the carrier’s scans waking the wait.

What you set up once (the outside)

  • Policy and orders. A Google Sheet in a Drive folder with one row per order: order number, customer name and email, the items and their prices, the purchase date, and the sales channel. You already keep this in whatever you sell through; this just puts it where the handler can read it. Alongside it, a short returns-policy doc holds the rules the system enforces — the return window (say 30 days from delivery), the non-returnable flags (final-sale, hygiene, perishable), the conditions you accept back, and the rule for when a return should be a refund versus a replacement. Changing the window or a flag is a doc edit, not a deploy.
  • How returns come in. A customer starts a return one of three ways, covered in Part 2 — a returns form on your site that posts to a Lambda Function URL, a reply to an order email that lands through SES, or a manual start by your own team for a phone request. Whatever the lane, it becomes one normalised request: which order, which item, and the customer’s stated reason.
  • The returns desk. The person who signs off the outcome — usually whoever runs customer service or the warehouse. They never have to chase a parcel or look up a policy; they get a single email when a return is ready to resolve, with the inspection result and the proposed outcome, and three buttons: Approve, Switch (refund instead of replace, or the reverse), and Reject. A tap records the decision — it never reaches into your payment system on its own.

What runs on every return (the inside)

Every return is one execution of a Step Functions state machine, and it moves through the same five stages in order.

  • Eligibility. The first stage finds the original order and runs a short list of plain-code checks against the policy: is it inside the return window, is the item flagged non-returnable, has it already been returned, is the stated condition allowed. Every check must pass. A fail is declined with the exact reason, or escalated where policy says a person should decide. This is Part 2.
  • RMA & label. A cleared return gets a unique RMA number and a prepaid shipping label from the carrier’s label API, with the key pulled from Secrets Manager. The label PDF is stored in S3, and the customer gets one email with the RMA number, the label, and exactly what to do. This is Part 3.
  • Await inbound. The execution then pauses — cheaply, for as many days as it takes — holding a task token and the tracking number. A real carrier scan, arriving as a webhook, wakes it when the parcel is delivered back. A daily sweep catches the parcels that never ship. This is Part 4.
  • Inspect. When the box arrives, a staffer opens it and records what they find — matches the order, sealed, used, damaged in transit — with a photo or two stored in S3. That recorded condition, not a guess, is what the decision runs on. This is Part 5.
  • Decide. The last stage applies the policy in plain code and proposes one outcome — full refund, partial refund, replacement, or reject — with the reason attached, and sends it to the returns desk to approve. This is also Part 5.

In plain words

A customer fills in the returns form on Tuesday: order 4821, one pair of boots, “too small.” The handler finds order 4821 — delivered nine days ago, well inside the 30-day window, boots not flagged non-returnable — and clears it. It issues RMA RMA-4821-7, calls the carrier for a prepaid label, drops the PDF in S3, and emails the customer: “Here’s your return label and RMA number; pop the boots back in the box and drop it at any pickup point.” The execution pauses. Six days later the carrier scans the parcel as delivered to your warehouse; the webhook wakes the execution and emails the warehouse to inspect it. A staffer checks the boots back in — unworn, tags on, matches the order — and records it. The decide stage applies the policy: returned in time, correct item, resaleable, customer wanted a different size that’s in stock, so it proposes a replacement, not a refund, and sends it to the returns desk. The manager taps Approve; the replacement is queued and the customer is told it’s on the way. Nothing was lost, nothing was refunded by mistake, and the only human decision was the one that mattered.

The cost of running this is about $2.90 a month at roughly 120 returns. The cost of not running it is the refund issued for a parcel that never came back, the return waved through after the window because nobody checked the date, and the customer left waiting a week for a label nobody got round to.

Design rules that shaped every decision

  • One return, one execution. The whole process — request to outcome — is a single Step Functions run you can see the state of at any moment.
  • Plain code decides what’s allowed. Eligibility and the refund-or-replace decision are deterministic checks against a written policy, not a model.
  • It owns the logistics, not just the money. RMA, label, and inbound tracking are part of the system — not a separate courier portal.
  • Track the real parcel. The wait ends on an actual carrier scan, never on a guess that the box probably arrived.
  • A human approves the outcome. Every refund and every replacement is signed off by a person, with the reason in front of them.
  • Policy lives in a doc. The window, the flags, and the rules change without a deploy.

Why this shape

Most small teams run returns as a chain of disconnected steps. Someone emails a label from the courier site, someone else watches for the parcel, and a third person — days later, with no memory of the original request — decides what to do with whatever turned up. The gaps are expensive: a refund goes out before the item is back, a return slips through after the window because nobody re-checked the date, or a parcel is “received” on trust and the refund is keyed against a box that’s still in a depot somewhere. None of these are hard problems individually; they’re just easy to drop when the steps live in different places and different heads.

Modelling the whole thing as one state machine fixes the gaps by construction. The return can’t reach the decision before the parcel is genuinely received, because the decide stage is downstream of a wait that only ends on a real scan. It can’t be approved without an inspection, because inspect comes first. And it can’t be refunded by accident, because the only thing the machine ever does on its own is propose — a human approves the money or the swap. The policy that drives it all sits in a doc you can edit, so the rules stay yours.

The next four posts walk through each stage in turn: how a request gets checked, how an RMA and label get issued, how an inbound parcel gets tracked, and how a refund or replacement gets decided. One diagram per post. A cost breakdown and a final engineering reference at the end.

All posts