Founder & Lead Engineer, RAITHub
Returns management software turns a return request into an RMA (return merchandise authorisation) that moves through fixed states: requested, approved, picked up, received, inspected, then refunded, exchanged or credited. For a standard Shopify store, a tool such as Loop Returns ($155 a month on Essential) or ReturnGO (from $147) covers it. Build custom when refunds run through local gateways, cash on delivery or batch-tracked stock.
If you would rather have it built for you, see how RAITHub would build this below.
What does returns management software actually do?
It replaces the email thread. A shopper asks to return an item, the software checks whether the item is eligible, issues an RMA number, books or prints a return label, tracks the parcel back, records the inspection, and then pays out a refund, ships an exchange or issues store credit. At the end, stock goes back on the shelf or gets written off, and the numbers land in a report.
Each of those steps touches something that costs money if it goes wrong: the payment gateway, the courier, the stock count and the customer's trust. That is why returns are worth modelling carefully, and why the hard part is rarely the return form.
What are the steps in an RMA flow?
- Request. The shopper picks the order, the items and a reason. The system checks the return window, whether the product is returnable (opened skincare often is not), and whether the order has already been refunded.
- Approval. Low-risk returns can be approved automatically; high-value or repeat returns go to staff.
- Pickup or drop-off. The system books a courier pickup or issues a label, and stores the tracking number.
- Receipt. The warehouse scans the parcel in against the RMA, so nobody refunds an item that never arrived.
- Inspection. Staff record condition: resellable, damaged, opened, wrong item, or not received in full.
- Resolution. Refund, exchange or store credit, possibly reduced if the inspection fails.
- Restock or write-off. Resellable stock returns to inventory; the rest is written off with a reason.
What should a return state machine look like?
Model the RMA as a state machine: a fixed list of statuses and the only moves allowed between them. The code then refuses impossible moves, such as refunding a return that was never received, and every status change can be written to an audit log.
type ReturnStatus =
| 'requested' | 'approved' | 'rejected' | 'cancelled'
| 'pickup_booked' | 'pickup_failed' | 'in_transit'
| 'received' | 'inspected'
| 'refund_pending' | 'refund_failed' | 'refunded'
| 'exchange_pending' | 'exchanged' | 'credited'
const allowed: Record<ReturnStatus, readonly ReturnStatus[]> = {
requested: ['approved', 'rejected', 'cancelled'],
approved: ['pickup_booked', 'cancelled'],
pickup_booked: ['in_transit', 'pickup_failed'],
pickup_failed: ['pickup_booked', 'cancelled'],
in_transit: ['received'],
received: ['inspected'],
inspected: ['refund_pending', 'exchange_pending', 'credited', 'rejected'],
refund_pending: ['refunded', 'refund_failed'],
refund_failed: ['refund_pending', 'credited'],
exchange_pending: ['exchanged'],
refunded: [], exchanged: [], credited: [], rejected: [], cancelled: [],
}
export function canMove(from: ReturnStatus, to: ReturnStatus): boolean {
return allowed[from].includes(to)
}
Enforce the move in the database as well as in code, so two staff members clicking at once cannot both win. A conditional update does it: UPDATE returns SET status = 'refund_pending' WHERE id = $1 AND status = 'inspected'. If no row changes, someone else got there first, and the second request stops. The design of the log behind it is covered in audit log design.
Which return reasons should you capture?
Capture a short, fixed list rather than free text, because the reasons are what your report runs on. Each reason should also drive a default action.
| Reason code | Who usually pays return shipping | Default inspection | Usual outcome |
|---|---|---|---|
| Changed mind | Shopper, under your policy | Unopened check | Refund or store credit; restock if sealed |
| Wrong size or shade | Policy choice; often the store, to win the exchange | Unopened check | Exchange first, refund second |
| Damaged in transit | Store, claim against courier | Photo evidence at request | Refund or replacement; write off; courier claim |
| Wrong item sent | Store | Confirm SKU | Replacement; restock the wrong item |
| Not as described | Store | Full inspection | Refund; flag the listing for review |
| Expired or near expiry | Store | Batch and expiry check | Refund; write off; check batch allocation |
Your return window and who pays shipping are policy decisions, and consumer law in many markets sets minimum rights for returns and refund deadlines. This is general information; confirm your obligations with your adviser.
Refund, exchange or store credit: which should you offer?
Offer all three, and put the one that keeps revenue first. An exchange keeps the sale; store credit keeps the money in your business; a refund loses both but keeps the customer's trust when the product was wrong.
| Resolution | Keeps the revenue? | Engineering it needs | Watch out for |
|---|---|---|---|
| Refund to original payment | No | Gateway refund API, idempotent, with failure handling | Double refunds on retries; failed refunds |
| Exchange | Yes | Reserve the replacement stock, create a linked order, handle price differences | Shipping the replacement before the original is received |
| Store credit | Yes, until spent | A credit ledger with balance, expiry and redemption at checkout | Credit that can be spent twice; tax treatment (ask your adviser) |
| Partial refund | Partly | Refund amount set at inspection, with a reason | Disputes when the deduction is not explained |
Cash on delivery needs its own answer. There is no card to refund, so the money has to go out another way: a mobile wallet transfer, a bank transfer, or store credit. Decide this before launch, and record which route each refund took.
How do you make gateway refunds safe to retry?
Give every refund an idempotency key derived from the RMA, store it with a unique constraint before calling the gateway, and treat the gateway's answer as an update to that row. Networks time out; staff click twice; jobs retry. Without this, each of those becomes a second refund.
Gateways help, but not forever. Stripe's idempotency docs say keys can be up to 255 characters, may be pruned once they are at least 24 hours old, and a reused key after pruning is treated as a new request. So a retry the next day can still refund twice unless your own database blocks it.
CREATE TABLE refunds (
id bigserial PRIMARY KEY,
return_id bigint NOT NULL REFERENCES returns(id),
idempotency_key text NOT NULL UNIQUE, -- e.g. 'rma-4821-refund-1'
gateway text NOT NULL, -- stripe, sslcommerz, bkash, manual
amount_minor integer NOT NULL CHECK (amount_minor > 0),
gateway_ref text,
status text NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'succeeded', 'failed'))
);
-- Insert first; a duplicate request hits the unique key and stops here
INSERT INTO refunds (return_id, idempotency_key, gateway, amount_minor)
VALUES ($1, $2, $3, $4)
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING id;
Refunds can also fail after they are accepted. Stripe's refund guide notes that customers usually see a refund in about 5–10 business days, that a failed refund can take up to 30 days to come back, that processing fees from the original charge are not returned, and that you cannot refund more in total than the original charge. Listen for the failure event and move the RMA to refund_failed so someone acts on it. Local gateways have their own refund APIs and timings; the integration side is in the bKash and SSLCommerz integration guide, and testing them is covered in testing payments and webhooks.
How should restock and inspection work?
Restock only after inspection, and only into the right place. A returned item is not stock until a person has said it is resellable. If you track batches and expiry dates, the item goes back into its own batch, not a generic pool, or your earliest-expiry-first picking will send an older unit out as if it were new.
Write the restock and the RMA status change in one database transaction, so the count cannot move without the return also moving. The same rule that stops two shoppers buying the last unit applies here; see how to prevent stock overselling. Write-offs need a reason code too, so damaged-in-transit losses can be claimed back from the courier.
What returns reports matter?
- Return rate by SKU and by reason. A product with a high "not as described" rate has a listing problem, not a returns problem.
- Resolution mix. The share of returns resolved as exchange or store credit rather than refund.
- Time to refund. From receipt to money sent. Slow refunds create support tickets and disputes.
- Cost per return. Shipping both ways, handling and write-off value.
- Restock rate. The share of returned units that went back on sale.
- Failed refunds and pickups. Anything stuck in refund_failed or pickup_failed for more than a day.
Loop, ReturnGO or AfterShip Returns vs a custom build: which should you choose?
The vendors are good at the shopper-facing part: portals, labels, exchange flows and incentives to take credit. Here is what their pricing pages showed when we checked in October 2026.
- Loop Returns: a free Checkout+ plan, Essential at $155 a month and Advanced at $340 a month, with exchanges and store credit on every plan and Instant Exchange and Bonus Credit on Advanced. Check which store platforms it supports before you plan around it.
- ReturnGO: Premium from $147 a month, Pro from $297 a month (adding API, webhooks and ERP integrations), and custom Enterprise pricing.
- AfterShip Returns: part of AfterShip's post-purchase suite. Its pricing page did not load when we checked, so confirm current plans directly.
Buy, build or hire?
| Option | Examples | Choose this when | Watch out for |
|---|---|---|---|
| Off-the-shelf returns app | Loop Returns, ReturnGO, AfterShip Returns | You run Shopify or a supported platform, take card payments and use couriers the app supports | Monthly fees that grow with volume; limited control over refunds through local gateways |
| No-code or template | Your platform's built-in returns, a form plus a spreadsheet, or an automation tool | A few returns a week and one person handling them | No stock link, no refund safety, and no reports you can trust |
| Custom build | Returns module inside your own commerce platform | Cash on delivery, local wallets, regional couriers, batch or expiry stock, marketplace sellers who bear their own returns | You own the code, the tests and the maintenance |
If your store is on a hosted platform and returns are your only pain, buy the app. If you are deciding whether to leave the platform altogether, start with the guide to building a marketplace or B2B store.
How long does a custom returns module take to build yourself?
For a developer who already knows your order, payment and stock code, a basic returns module (request, approval, receipt, inspection, refund through one gateway, restock) takes about 3–5 weeks, including tests. Each extra gateway, courier pickup integration or store-credit ledger adds days to weeks. The main risk is the money: a refund that runs twice, or a refund issued for an item that never came back. Both are prevented by the state machine and the idempotency table above, and both need tests that simulate retries and failures.
Why RAITHub for this
- Local rails and cash on delivery. RAITHub built and runs TheSkinProof, the founder's own multi-vendor marketplace (a venture, not a client), on bKash, Nagad, SSLCommerz and cash on delivery, with 217 API endpoints and 750+ automated tests. Those are the rails where off-the-shelf returns apps help least.
- Batch stock that stays correct. Sundor Skin, a B2B wholesale platform RAITHub built, allocates stock by batch, earliest expiry first, across 146 PostgreSQL tables with row-level security and 12 staff roles. Returns that restock into the right batch sit on exactly that kind of model.
- Stripe payments and roles. PropDesk, a property platform RAITHub built, collects rent through Stripe across 4 roles and runs 1,024 tests.
- QA first. Retry, double-click and failed-refund cases are written as tests before release, not discovered by customers.
When you don't need us
- You run a Shopify store with card payments and supported couriers: Loop, ReturnGO or AfterShip Returns will be live in days.
- You handle a few returns a week: a clear policy and your platform's built-in returns are enough.
- You want your returns processed for you. Warehousing and customer service are not RAITHub services; it builds and tests the software.
How RAITHub would build this
- Policy into rules. Return windows, non-returnable categories, reason codes and default outcomes, written as a spec you sign off.
- The RMA state machine. Statuses, allowed moves enforced in code and the database, and an audit log of every change.
- Money. Idempotent refunds through your gateways, a route for cash-on-delivery refunds, and a store-credit ledger if you want one.
- Stock and couriers. Inspection screens, restock into the right batch or variant, write-offs with reasons, and courier pickup bookings.
- Reports. Return rate by SKU and reason, resolution mix, time to refund and stuck returns.
Timeline: a returns module added to an existing store is a backend and API engagement, typically 6–12 weeks at fixed scope. As part of a new store, a first version fits inside a 4–6 week fixed-scope MVP. If refunds or stock are already going wrong on a live system, a 2–4 week code rescue comes first.
You receive: automated tests and CI covering retries, double submissions and failed refunds; handover docs and runbooks for stuck returns and failed refunds; and full IP, under an NDA signed before detailed discussion. See API and backend development and ecommerce development for what is included.
Next step: a free 15-minute technical audit, then a written fixed quote. Book the audit and bring your returns policy and the gateways and couriers you use.
Frequently asked questions
What is an RMA in ecommerce?
An RMA, or return merchandise authorisation, is the record and number a store issues when it accepts a return request. It links the returned items to the original order and tracks the return through pickup, receipt, inspection and resolution.
How much does returns management software cost?
On their pricing pages in October 2026, Loop Returns listed a free Checkout+ plan, Essential at $155 a month and Advanced at $340 a month, and ReturnGO listed Premium from $147 and Pro from $297 a month. Enterprise plans are quoted.
Should I offer store credit instead of refunds?
Offer it as an option, not the only option. Exchanges and store credit keep revenue, but a shopper who received the wrong or a faulty item expects a refund. Consumer law may also require refunds in some cases; confirm with your adviser.
How do I stop double refunds?
Give each refund an idempotency key derived from the RMA, store it in your database with a unique constraint before calling the gateway, and only move the return to refunded when the gateway confirms. Gateway idempotency alone can expire; Stripe may prune keys after 24 hours.
How do you refund a cash-on-delivery order?
There is no card to refund, so pay out by mobile wallet, bank transfer or store credit, and record which route was used. Decide the route before launch so staff are not improvising with each return.
When should returned stock go back into inventory?
Only after inspection marks it resellable, in the same database transaction as the RMA status change. If you track batches or expiry dates, restock into the original batch so earliest-expiry-first picking stays correct.
Can RAITHub add returns to my existing store?
Yes, if your store runs on code you control. RAITHub scopes it after a free 15-minute audit and gives a written fixed quote; a returns module for an existing store is typically a 6–12 week backend engagement.
Related posts
Ready to discuss your project?
Book a free 15-minute technical audit with our engineering team.