Multi Currency and Cross Border Pricing in Headless Commerce: Duties, Rounding, and the Presentment Problem (2026)
Selling internationally on a packaged storefront is mostly a settings screen: turn on the currencies, let the platform convert, and the theme shows the right number. Go headless and the convenience disappears, because now your own frontend is responsible for asking the API for a price in the customer's currency, displaying it with the correct rounding, carrying that currency all the way through the cart and into checkout, and reconciling what the customer paid against what the store settles in. Each of those steps has a way to go wrong that a themed store never exposed you to, and the most common one is subtle: the storefront requests a presentment currency and the API quietly returns the amount in the store's base currency anyway, so the customer sees a euro sign in front of a dollar number. Add duties and import taxes on a cross border order and the surface expands again, because now the total the customer sees at checkout has to include or exclude a duty depending on whether you sell delivered duty paid or delivered duty unpaid, and getting that wrong means either a surprised customer or a margin you did not plan to give away. This post is about the architecture that keeps a headless international store honest: which system owns the converted price, how presentment currency actually flows through the Storefront API, why rounding is a real rule and not a rounding error, and how duties change the checkout total.
Multi Currency and Cross Border Pricing in Headless Commerce: Duties, Rounding, and the Presentment Problem (2026)
On a themed store, selling in another currency is a toggle. The platform owns the conversion, the theme reads the converted price, and the customer sees a clean local number without anyone writing code. Go headless and every one of those quiet steps becomes your frontend's job: request the price in the right currency, round it the way that currency expects, carry it through the cart, and settle it against a base currency that is different from what the customer saw. The failure that catches most teams is the first one, where the storefront asks for a presentment currency and the API returns the store's base currency anyway. Here is the architecture that prevents that and the cross border pricing rules that sit on top of it.
The Store Owns Conversion, Not Your Frontend
The first rule is the same one that governs inventory and tax: pick one system of record for the converted price and never let your frontend invent its own. It is tempting, when the API is being difficult, to fetch a base currency price and multiply by a rate you pulled from somewhere, because it makes the number appear. Do not. The moment your storefront computes its own converted price, you have two sources of truth, and they will disagree at checkout because the platform applies its own exchange rate and rounding rule that your multiplication did not replicate. The customer then sees one price on the product page and a different one at payment, which is the single fastest way to lose an international sale.
The platform is the correct owner because it already holds the exchange rate, the per currency rounding rule, and the settlement logic that turns what the customer paid back into your base currency. Your frontend's job is to request the price in the presentment currency and display exactly what comes back, nothing more. Whether you should express markets as one store with many currencies or several regional stores is a separate architecture decision we cover in the Shopify Markets versus multi store comparison; this post assumes one store presenting many currencies, which is where the headless pricing pitfalls concentrate.
The Presentment Problem
The specific bug that defines headless international pricing is worth naming plainly, because teams lose days to it. You issue a Storefront API query with a presentment currency context, expecting amounts in that currency. In a themed store this works. In a custom storefront, a well known failure is that amounts come back in the store's local currency regardless of the presentment context, so your carefully requested euro price is actually the dollar number wearing a euro sign. The total is wrong, the tax is wrong, and nothing errors, which is why it survives into production.
The fix is to pass the currency context correctly at the point the platform expects it and to verify the returned currency code on every price rather than trusting the request. Treat the currency code as data you check, not an assumption you make. A price object that does not carry the currency you asked for is a bug to surface loudly in development, not a value to render.
# Request the buyer's context so amounts return in the
# presentment currency. Then verify the code on the response.
query ProductPrice($handle: String!) @inContext(country: DE, language: DE) {
product(handle: $handle) {
variants(first: 1) {
nodes {
price { amount currencyCode } # verify currencyCode === "EUR"
}
}
}
}
// Guard: never render a price whose currency does not match context.
function assertPresentment(price, expected) {
if (price.currencyCode !== expected) {
throw new Error(
`presentment mismatch: got ${price.currencyCode}, expected ${expected}`
);
}
return price;
}
The guard looks paranoid until the first time it catches a base currency amount that would otherwise have shipped a wrong total to a real customer.
Rounding Is A Rule, Not An Accident
Every presentment currency carries a rounding rule, and it is a deliberate merchandising rule rather than a floating point artifact. A price that converts to 18.73 in a currency you have set to round to the nearest 0.95 should present as 18.95, because the platform applies the rounding rule you configured for that currency. If your frontend does its own conversion, it skips this rule entirely and shows 18.73, which then disagrees with the checkout that applied the rule. This is the same class of error as the presentment bug: two systems computing a number that only one of them is authorized to compute.
The design consequence is that your storefront must display the price the platform returns, already rounded, and must never re round or reformat the numeric amount beyond adding the currency symbol and separators for locale. Formatting for display is your job; deciding the actual amount is the platform's. Keep that line clean and a whole category of penny level mismatches disappears.
| Concern | Owned by | Your frontend does |
|---|---|---|
| Exchange rate | Platform | Nothing, request presentment price |
| Rounding rule per currency | Platform | Display the rounded amount as is |
| Currency symbol and separators | Frontend | Format for locale only |
| Settlement to base currency | Platform | Nothing, reconcile in reporting |
Duties Change The Checkout Total
Cross border pricing is not finished at conversion, because a shipment that crosses a customs border may owe import duties and taxes, and who pays them changes the number the customer sees. The two models are delivered duty paid, where you collect the duty at checkout and the customer pays nothing on delivery, and delivered duty unpaid, where the carrier collects from the customer at delivery and your checkout total excludes it. This is configured per market, and it directly changes the total your headless checkout must present.
Delivered duty paid is the better experience because the customer sees one complete figure and faces no surprise at the door, but it requires your checkout to compute the duty accurately, which means either the platform's managed pricing folds duty and import tax into the displayed price or you integrate a duty calculation into the checkout flow. Delivered duty unpaid is simpler for you to implement and worse for the customer, who gets a delivery held hostage for a fee they did not expect. For considered purchases and higher order values, the delivered duty paid experience usually pays for itself in completed checkouts and fewer refused deliveries. The tax calculation side of this, distinct from duty, is covered in the tax engine comparison, and the physical fulfillment and carrier side in the fulfillment platform comparison.
The Reconciliation You Owe Finance
The last piece is not visible to the customer at all. The customer pays in the presentment currency, and you settle in your base currency, so every order carries two amounts: what the buyer paid and what you received after the platform's conversion. Your order data has to preserve both, because finance reconciles revenue in the base currency while support and refunds happen in the currency the customer actually paid. If your headless order pipeline flattens everything to a single currency, you lose the ability to refund the exact amount the customer paid and to reconcile settlement accurately. Persist the paid currency and the settled currency on every order and keep them distinct through your entire data model.
When This Applies To Your Stack
Multi currency headless work earns its complexity when a meaningful share of your revenue crosses a currency or a customs border. If you sell in one currency to one country, none of this applies and you should not build it. If you sell internationally, the presentment guard, the rounding discipline, the duty model, and the two currency reconciliation are not optional extras; they are the difference between an international store that quietly loses money to mismatches and one that presents an honest price and settles cleanly. Start by making the platform the sole owner of conversion and rounding, then layer duties and reconciliation on top.
If you are taking a headless build international and want the presentment flow, rounding, duty model, and reconciliation designed so the price the customer sees is the price you settle, that is the kind of headless commerce work we do at Contra Collective. The failure modes here are quiet, and quiet failures in pricing are the expensive kind.
FAQ
Why does my headless storefront show the wrong currency amount? A known headless failure is that Storefront API amounts return in the store's base currency even when you request a presentment currency, so the number is right for the base currency and wrong for the one you displayed. Verify the currency code on every returned price and pass the buyer context at the point the platform expects it.
Should my frontend convert prices itself? No. Converting on the frontend creates a second source of truth that will disagree with checkout, because the platform applies its own exchange rate and per currency rounding rule. Request the presentment price and display exactly what comes back.
What is the difference between delivered duty paid and delivered duty unpaid? Delivered duty paid means you collect import duties and taxes at checkout so the customer pays nothing on delivery. Delivered duty unpaid means the carrier collects from the customer on delivery and your checkout total excludes the duty. The first is a better experience, the second is simpler to implement.
How do I handle refunds when the customer paid in another currency? Persist both the presentment amount the customer paid and the base currency amount you settled, on every order. Refund in the currency the customer paid, and reconcile revenue in the base currency, which requires keeping the two amounts distinct through your whole data model.
More from the lab.
Segment vs RudderStack vs mParticle: Customer Data Platform for Headless Commerce (2026)
A customer data platform is the least visible and most load-bearing piece of a headless commerce stack. It is the layer that captures every event from the storefront, the mobile app, and the backend, resolves those events into a single view of the customer, and fans them out to analytics, email, ads, and the warehouse. Because it sits in the middle of everything, the CDP choice quietly decides three things that are expensive to change later: whether you own your customer data or rent access to it, how much of your compliance and consent surface lives inside a vendor versus your own infrastructure, and how your bill scales as event volume grows, which in commerce it always does. Segment, RudderStack, and mParticle are the three platforms most enterprise commerce teams shortlist, and they embody genuinely different philosophies rather than being feature-for-feature clones. Segment is the managed incumbent optimized for time to value. RudderStack is the warehouse-first, self-hostable challenger built for teams that want to own the pipeline. mParticle is the mobile-heavy enterprise option with the deepest identity and audience tooling. For a headless architecture, where the storefront is decoupled and events originate from several surfaces at once, the differences in how each handles server-side collection, identity resolution, and pricing at volume are what separate a clean integration from a costly one. This post lays out those differences and the decision framework that follows from them.
Migrating from Salesforce Commerce Cloud to Headless Shopify Plus: A Replatforming Playbook (2026)
The decision to leave Salesforce Commerce Cloud is usually made on cost and velocity, and by the time it reaches engineering it has hardened into a deadline. That is where replatforming projects go wrong, because moving from SFCC to a headless Shopify Plus stack is not a migration in the copy the data and flip the switch sense; it is a rebuild of the parts of your commerce logic that lived inside SFCC cartridges and pipelines, wrapped around a data migration that is the easy part by comparison. SFCC gave you a monolith where the storefront, the business logic, and the platform were fused, and headless Shopify Plus deliberately unfuses them: Shopify becomes the commerce engine behind an API, and the storefront becomes your own application. Everything that made SFCC feel complete, the cartridge ecosystem, the pipeline customizations, the server side rendering baked in, becomes something you now own explicitly. This playbook walks the migration in the order that actually de-risks it: what maps cleanly from the SFCC data model to Shopify, what has to be rebuilt rather than ported, how to protect the SEO equity that a careless cutover destroys, and the sequencing that lets you move without a big bang launch. The projects that fail treat this as a data problem. The ones that succeed treat it as a rebuild with a data migration attached.
Real-Time Inventory Sync for Headless Commerce: NetSuite, Shopify, and the Oversell Problem (2026)
In a packaged storefront, inventory is mostly the platform's problem: it owns the number, it decrements it at checkout, and it hides the fact that the number is a lie for the few seconds it takes to settle. Go headless and add an ERP as the system of record, plus a marketplace or two and a retail POS, and that convenient fiction falls apart, because now four systems each hold their own idea of how many units exist and they update on different clocks. The storefront reads a cached count that is seconds or minutes stale, the ERP commits the truth on its own schedule, the marketplace polls when it feels like it, and somewhere in the gaps two customers buy the last unit. Overselling is not an edge case in this architecture; it is the default outcome of treating a distributed count as if it were a single authoritative one. This post is about the sync design that keeps a headless stack honest: which system owns the number, how updates propagate without hammering the ERP, why a safety buffer is a real strategy rather than an admission of defeat, and how a reservation pattern turns the last unit problem from a race condition into a queue. The right answer depends on your order volume, your channel count, and how much oversell your margins can actually absorb.