An Australian brand sells a $189 AUD jacket to a customer in Portland. The order clears, the parcel flies, the tracking pings. Six days later the customer opens a return. That return does not cost the brand $189. It costs the outbound freight already spent, the US import duty already paid on the customer’s behalf, the reverse freight back across the Pacific, the handling at the receiving end, and roughly three weeks of a customer refreshing their bank app waiting for money that has not landed.
We build Shopify stores for Australian brands selling into the US, New Zealand, Singapore and the EU, and we see the same shape every time. The outbound side is engineered properly: Shopify Markets configured, duties collected at checkout, currency rounding that does not look like a spreadsheet error. The return side is a support inbox, a shared Google Sheet, and a warehouse manager who guesses.
That asymmetry is expensive, and it is getting more expensive. This article is the build specification for the direction nobody architects.
A cross-border return costs roughly two and a half times a domestic one
The numbers are worth sitting with before you design anything. The typical return rate on a cross-border order sits around 20 percent, and cross-border returns cost about 2.5 times what a domestic return costs to process. Across all ecommerce, the total cost of handling a return runs roughly 30 percent higher than the refund amount itself, once freight, labour, inspection and restocking are counted. Refunds on international orders take two to three times longer to reach the customer than domestic refunds, because the parcel has to clear customs in the other direction before most merchants will release the money.
Here is what that looks like on a real order, using round numbers we see on US-bound parcels from a Melbourne 3PL.
| Line | Domestic AU return | US cross-border return |
|---|---|---|
| Order value | $189 AUD | $189 AUD |
| Outbound freight (sunk) | $9 | $34 |
| Import duty and fees paid at checkout (sunk unless recovered) | $0 | $21 |
| Reverse freight | $9 | $46 |
| Inspection, handling, restock | $7 | $14 |
| Total cost of the return | $25 | $115 |
| Cost as a share of order value | 13% | 61% |
Sixty-one percent. On a category running 20 percent returns, that is roughly 12 points of gross margin evaporating on the international channel that the domestic P&L never warned you about. Most brands find out when the year-end numbers land, because returns are booked as a refund line and the freight is booked somewhere else entirely.
The build response is not “reduce returns”. That is a merchandising problem and a different article. The build response is: make each return cost less, recover what is recoverable, and give the customer their money back faster than the parcel moves.
Where the default Shopify returns setup breaks once you cross a border
Shopify’s native returns stack is genuinely good. Self-serve returns let a customer sign into their account, open the order, and request a return without emailing anyone. You switch it on under Settings, Customer accounts, and you shape what is requestable under Settings, Policies, in the return and cancellation rules section. Items outside the window or flagged final sale appear as ineligible before the customer submits, which kills a whole category of support ticket.
It breaks in four specific places the moment a second country is involved.
One return window for the world. A 30 day window is generous in Australia and non-compliant in the EU, where Directive 2023/2673 requires you to give consumers a straightforward electronic way to exercise their 14 day right of withdrawal, in force from 19 June 2026. A single global rule either over-serves one market or breaks the law in another.
One return address. The default return location is wherever your fulfilment location is. For an Australian brand that means every US return flies back to Australia, which is the single most expensive routing choice available and almost never the right one.
No duty or tax recovery hooks. The duty you collected at checkout and remitted onward does not come back automatically when the order does. If your order records do not carry the customs data, it never comes back at all.
No visibility on the reverse leg. Domestic returns are short enough that “we will refund when it arrives” is acceptable. On a 19 day reverse transit, silence reads as theft.
Run this against your own store before you design anything. If any answer is “we do not know”, that is your build backlog.
# Cross-border returns readiness check
1. Do return windows differ by market? Y / N
2. Is there a return address inside each major market? Y / N
3. Is duty paid recorded per line item on the order? Y / N
4. Is return reason captured as structured data, not free text? Y / N
5. Does the customer get status events on the reverse leg? Y / N
6. Is refund release tied to scan-in, receipt or inspection? which?
7. Can you report return cost per market, per month? Y / N

Step one: return rules that vary by market
Shopify’s rules framework is hierarchical. A storewide default applies everywhere unless a market-specific rule overrides it for customers whose shipping address falls in that market. Each override can carry its own return window, its own set of allowed reasons and actions, its own restocking treatment, and its own cancellation eligibility. This is the first thing to configure, and it takes an afternoon.
The matrix we start from for an Australian brand selling into four regions looks like this. Adjust the numbers to your category, but keep the shape.
| Market | Window | Return shipping | Restocking | Driver |
|---|---|---|---|---|
| Australia and NZ | 30 days | Free, prepaid label | None | Home market, cheap reverse freight, competitive norm |
| United States | 21 days | Free above $150 AUD order value, otherwise deducted | None | Reverse freight is the dominant cost, so tie it to basket size |
| European Union | 14 days minimum withdrawal, 21 day goodwill window | Customer pays unless faulty | None | Directive 2023/2673 electronic withdrawal is mandatory from 19 June 2026 |
| Singapore and Hong Kong | 21 days | Customer pays | None | Low volume, no in-market consolidation yet |
Two notes from doing this repeatedly. First, the EU withdrawal right is a floor, not a policy. You can offer 21 days of goodwill on top, but the electronic withdrawal path has to exist and has to be easy to find, and it cannot be a PDF form emailed to a support address. Second, resist making return shipping free in every market because it is free at home. Free returns on a US order where the reverse leg alone is $46 AUD is a decision to spend $46 to recover a $189 item, and it should be a deliberate one, not a default inherited from your Australian policy.
Step two: routing the parcel, which is where the money actually is
Once the return is approved, you have four real options, and choosing correctly per item is worth more than every other optimisation in this article combined.
| Route | When it wins | Typical cost per unit |
|---|---|---|
| Return to origin (back to AU) | High value, serialised, or single-location inventory | $40 to $60 AUD |
| In-market consolidation, batch freight home | Steady volume in one country, 40+ units per month | $12 to $22 AUD landed |
| In-market resale via a local 3PL or outlet channel | Non-seasonal, resellable, market has demand | $8 to $15 AUD |
| Refund and let the customer keep it | Item value below the total reverse cost | Item cost only |
The fourth row makes merchants uncomfortable and it is frequently the correct answer. The test is arithmetic, not sentiment. If the recoverable value of the unit is less than what it costs to get it back and make it saleable, shipping it is a decision to lose money twice.
We implement this as a routing function that runs when a return is approved, not as a warehouse judgement call. Here is the core of it, simplified.
function routeReturn(item, market) {
const reverseFreight = FREIGHT[market][item.weightBand];
const handling = HANDLING[market];
const recoverableDuty = item.dutyPaid * DRAWBACK_RATE[market];
const resaleValue = item.cost * CONDITION_FACTOR[item.reason];
const costToRecover = reverseFreight + handling - recoverableDuty;
if (resaleValue < costToRecover) return "REFUND_KEEP";
if (market.consolidationHub && item.value < 300) return "CONSOLIDATE";
if (item.serialised || item.value >= 300) return "RETURN_TO_ORIGIN";
return "IN_MARKET_RESALE";
}
Two things make this work in production. CONDITION_FACTOR is keyed off the structured return reason, so “wrong size” retains close to full resale value while “damaged in transit” retains almost none. And DRAWBACK_RATE is per market, because duty recovery is real in some jurisdictions and fictional in others, which is the next section.

Step three: wiring returns into the Admin API so the data survives
If returns live in a helpdesk macro, you have no data. Shopify’s Returns API gives you the objects to model this properly, and it has matured considerably. Returns are now supported in the Customer Accounts API, which is what makes the self-serve path work end to end.
The distinction that trips people up: returnRequest creates a return in REQUESTED state that a merchant must approve or decline, using returnApproveRequest or returnDeclineRequest. returnCreate skips straight to OPEN, on the assumption approval already happened, and creates a reverse fulfilment order you can then attach a reverse delivery to. A reverse delivery is the set of items packaged together and sent back to you or to a third party fulfilment service. For cross-border work you almost always want the approval step, because approval is where the routing function runs.
mutation ApproveAndRoute($id: ID!, $tracking: ReverseDeliveryTrackingInput!) {
returnApproveRequest(input: { id: $id }) {
return { id status }
userErrors { field message }
}
}
mutation CreateReverseDelivery(
$fulfillmentOrderId: ID!
$lineItems: [ReverseDeliveryLineItemInput!]!
$labelInput: ReverseDeliveryLabelInput
) {
reverseDeliveryCreateWithShipping(
reverseFulfillmentOrderId: $fulfillmentOrderId
reverseDeliveryLineItems: $lineItems
labelInput: $labelInput
trackingInput: { number: "RA123456789AU", url: "https://..." }
) {
reverseDelivery { id }
userErrors { field message }
}
}
Alongside this, record the customs facts on the order at the time of sale, because you cannot reconstruct them later. The minimum viable schema we attach as order and line item metafields:
order.metafields.customs.entry_number # customs entry reference
order.metafields.customs.duty_paid_aud # duty remitted, AUD
order.metafields.customs.import_vat_aud # import VAT or GST, AUD
line_item.metafields.customs.hs6 # 6 digit HS classification
line_item.metafields.customs.origin_country
line_item.metafields.returns.route # routing decision, set on approval
line_item.metafields.returns.condition # graded at receipt
That schema is not academic. It is the difference between a duty recovery claim you can substantiate and one you cannot.
Step four: recovering the duties and taxes you have already paid
This is the part almost nobody builds, and it is where the recoverable money sits. The rules differ sharply by destination, so treat each market separately.
United States. When a US customer returns an item, the duty you paid is not automatically refunded to you. The recovery mechanism is duty drawback, a US Customs and Border Protection program that can refund up to 99 percent of eligible import duties, tariffs and certain fees when goods are subsequently exported from the US or destroyed under compliant conditions. The catch is entirely about records: you must be able to match the import entry to the specific goods that left again. This matters far more since the US de minimis exemption was eliminated on 29 August 2025, meaning every commercial shipment entering the US now attracts duty and full customs procedures regardless of value. Duty that used to be zero on a $189 AUD parcel is now a real, recurring line, which makes drawback worth building for rather than ignoring.
European Union. The EU removed its €150 duty exemption and, from 1 July 2026, applies a flat €3 duty per unique HS6 code on any order cleared on or after that date, with no grace period. That duty cannot be paid through IOSS, so it is assessed to the customs broker who then invoices the shipper. A separate €2 handling fee per unique HS code takes effect no later than 1 November 2026, bringing the combined charge to €5 per HS code. On the VAT side the position is better: an IOSS registrant reduces its quarterly remittance by the VAT associated with returned orders, which is clean in principle and entirely dependent on consistent order-level tracking across both the sale and the return.
Australia. Exports are GST-free, so there is no GST to recover on the outbound leg. The exposure is on the return leg: goods coming back into Australia need to be entered correctly as returned Australian goods, or you will pay import duty and GST on your own stock. Get the customs broker instructions right once and it stops being an issue.
The practical build here is a monthly reconciliation job, not a heroic quarterly effort.
# monthly duty recovery run
returns_last_month = shopify.returns(status="CLOSED", market="US")
for r in returns_last_month:
entry = r.order.metafields["customs.entry_number"]
if entry and r.route in ("RETURN_TO_ORIGIN", "DESTROYED"):
drawback_queue.append({
"entry": entry,
"hs6": r.line_item.metafields["customs.hs6"],
"duty_aud": r.order.metafields["customs.duty_paid_aud"],
"proof": r.reverse_delivery.tracking_number,
})
export_csv(drawback_queue, "drawback_claim.csv") # hand to broker
One brand we work with recovers a mid five figure AUD amount annually from this single job. It runs in under a minute and the entire build was two days of work, most of it spent agreeing the metafield schema with their broker.
Step five: the refund clock is what customers actually judge you on
International refunds take two to three times longer than domestic ones, and customers do not distinguish between “in transit” and “ignoring me”. The fix is partly operational and partly communication, and the communication half is nearly free.
Decide explicitly where in the reverse journey you release the money. There are three defensible positions.
| Trigger | Customer wait | Fraud exposure | Best for |
|---|---|---|---|
| Refund on first carrier scan | 1 to 2 days | Highest | Repeat customers, low fraud categories, high LTV |
| Refund on arrival in destination country | 7 to 12 days | Moderate | The sensible default for most cross-border volume |
| Refund on inspection at warehouse | 18 to 25 days | Lowest | High value, serialised, or known abuse categories |
Most brands sit on row three by accident rather than by decision. Moving to row two on a US programme typically halves the “where is my refund” ticket volume, and the incremental fraud exposure is small if you gate it by customer history rather than applying it universally.
Then instrument the reverse leg the way you instrument the outbound one. Subscribe to the return webhook topics, map carrier events onto a customer-facing timeline, and send three messages rather than none: return approved with the label, parcel scanned by carrier, refund issued. Three transactional emails, built once.
returns/approve -> email: label attached, expected timeline by market
returns/request -> internal: routing function runs, route stored
reverse_deliveries/* -> carrier scan events, drive customer timeline
refunds/create -> email: refund issued, banking timeline by region

What we measure once it is live
A returns build that is not measured drifts back to the spreadsheet within two quarters. These are the numbers we put on a dashboard from day one, split by market.
| Metric | Why it matters | Healthy direction |
|---|---|---|
| Return rate by market and category | Isolates whether a market or a product is the problem | Flat or falling |
| Total return cost per unit, by market | The number the P&L actually feels | Falling as routing matures |
| Route mix (origin, consolidate, resale, keep) | Shows the routing function is being used, not overridden | Shift away from return to origin |
| Days from request to refund | The customer experience metric | Under 12 days cross-border |
| Duty recovered as a share of duty paid on returns | Proves the recovery job is working | Rising toward the drawback ceiling |
| Return-related tickets per 100 international orders | Support load, and a proxy for communication quality | Falling |
| Resale recovery rate on returned units | How much value survives the reverse leg | Above 70% for apparel |
If you track only one of these, track total return cost per unit by market. It is the number that tells you whether an international channel is genuinely profitable or whether it is being subsidised by the domestic one.
How We Do It at Insiteful
We have built and rebuilt around 200 Shopify stores over 25 years, and cross-border returns is one of the areas where we have changed our own approach the most. Early on we treated returns as a post-launch tidy-up. Now it is scoped into the international build itself, because retrofitting the customs metafield schema onto 18 months of historical orders is not possible and the recovery window closes behind you.
On an international build, our sequence looks like this. During discovery we get the return cost per market on the table before we design the checkout, because that number changes the shipping thresholds and sometimes changes which markets go live first. We configure market-specific return rules as part of the Markets setup, in the same pass as duties and currency, not afterwards. The customs metafield schema goes in before the first international order is placed, and we test it by putting a real order through and pulling the fields back out via the API. The routing function ships with the store, seeded with freight tables from the client’s actual carrier rate card rather than published rates. And we build the three transactional emails on the reverse leg before launch, because that is the cheapest support-cost reduction available anywhere in an international programme.
We build native Shopify. No headless, no separate returns platform bolted on unless volume genuinely demands it. In most cases the native Returns API plus a small private app doing the routing and the monthly recovery run covers a brand comfortably past $10M AUD in international revenue. The instinct to buy a returns SaaS product on day one usually adds a monthly fee and a second source of truth without solving the two things that actually cost money, which are routing and duty recovery.
The related builds we most often pair this with are international shipping configuration, duties and import taxes at checkout, and 3PL integration, since the routing function is only as good as the fulfilment data feeding it. If you also run physical retail, in-store returns is the other half of the same problem.
Where to start this week
You do not need the whole build to stop the bleeding. In priority order: pull your last 90 days of international orders and calculate actual return cost per unit by market, because most brands have never seen this number. Configure market-specific return rules, which is an afternoon and immediately fixes your EU compliance position ahead of 19 June 2026. Add the customs metafields so that from today forward your duty recovery claims are substantiable. Then build the routing function, then the recovery job.
If your international channel is growing and the returns side is still a shared inbox and a warehouse manager’s judgement, that gap is compounding quietly every month. We scope this as part of a build assessment, where we look at your actual order data, your carrier rates and your market mix, and tell you what the returns architecture should be before you spend anything on it. You can see how our process works here.