Soft-focus sunset behind a house roof and tree silhouette, standing in for Shopify prices and fees that change once a buyer’s destination comes into view

One Pen Per Price: Shopify Destination Fees, Markets vs Functions

One Pen Per Price: Shopify Destination Fees, Markets vs Functions

Oct 22, 2026 Updated Oct 26, 2026

Octavian Contis 11 minutes

Share

A merchandiser adds an environmental recycling fee to one SKU so it only applies when the order ships to California.

She types a California address in hosted checkout and the extra line appears.

Then someone pays for the same cart with Apple Pay, and the fee is gone.

Nothing crashed. No error. The money just quietly stopped arriving.

Introduction

This article teaches that inventory and the mechanism choice: for each SKU that needs a destination-specific fee or price, pick one owner (a Markets catalog, a checkout Function that reads delivery address, or a separate fee product), then prove the rule on hosted checkout and on Apple Pay.

Walking every live market for duties, currency, shipping profiles, and catalog gaps before paid international traffic belongs in Shopify Markets Black Friday Traps: Duties, Currency, and Catalog Gaps. Deciding when a Plus checkout needs a UI extension for display and a Function for logic belongs in When to Use Checkout UI Extensions vs Shopify Functions. Confirming UK VAT on EU orders after the September 2026 tax-product change belongs in Shopify Tax Checklist: Confirm UK VAT on EU Orders Over £135.

What actually broke?

Shopify documents this one. Checkout UI Extensions cannot apply attribute changes when the buyer uses an accelerated wallet such as Apple Pay or Google Pay.

The setup looked sensible. A checkout extension wrote the shipping state into a cart attribute. A Discount Function read that attribute and set the fee. On the wallet path, the write never happened, so the Function read an empty field.

Think of a cart attribute as a sticky note on a parcel. It works while the parcel goes through your own sorting room. Apple Pay sends the parcel down a different chute, and the note never gets stuck on.

The Function was trusting a note that sometimes does not exist.

The idea: one pen per price

Picture a paper price tag where merchandising writes a price, marketing crosses it out with a discount, and a developer pencils in a correction at checkout.

Every buyer who reads that tag sees something slightly different, depending on which pen got there last.

That is the pattern behind most destination fee bugs we see in merchant and developer threads this month. A fee that holds in hosted checkout and vanishes on Apple Pay. A Discount Function waiting on an attribute the wallet never wrote. A team faking country prices with a discount instead of a Markets catalog.

The fix is a rule we call one pen per price. For every amount that changes with destination, exactly one surface writes it. Everything else reads.

There are three pens worth choosing from:

  • a Markets catalog, for a country or configured market price
  • a checkout Function that reads delivery address, for a levy that changes inside one country
  • a separate fee product, when finance needs a taxable, reportable line

Pick one per SKU. Then prove it on hosted checkout and on Apple Pay.

Which problems look like this?

Three shapes come up again and again.

The first is a state or province levy on a named product. A recycling fee, an environmental surcharge, a destination handling line that must show as its own amount. Teams pre-fill the shipping address in a checkout UI extension, write the province into a cart attribute, and let a Discount Function change the fee variant from that attribute. That is the sticky note.

The second is a country price that should have lived in a Markets catalog. The store shows one base price, then a Function or an automatic discount rewrites the amount once the buyer reaches checkout. Express wallets and Shop Pay then see a different number from the product page.

Two pens. One tag.

The third is a fee SKU that merchandising added so finance can see a taxable line. Checkout still relies on the attribute handoff to decide whether that SKU belongs in the cart. Hosted checkout looks complete. The wallet cart quietly omits the line.

We have built on both sides of this. On Lanoguard, an auto and moto rust-protection brand, we shipped checkout UI extensions and Shopify Functions on a Shopify 2.0 theme, with internationalisation as a storefront feature. Destination rules that change inside one country belong in that Functions layer, with the delivery address as the input. Medik8, a beauty and skincare brand, we migrated to Shopify 2.0 with a multi-store architecture for regional merchandising. Country-level price and catalogue differences can live in Markets catalogs or separate shops. A levy that changes by state on one shop still needs a Function or a fee product.

Why can't Markets just do the state fee?

Because Markets only knows what you told it to match.

Shopify Markets assigns a buyer context from conditions you configure: location, company location, retail location, or channel. A catalog on that market publishes products and attaches a price list. Shopify can convert the store price, apply a percentage adjustment, or store a fixed local-currency price on the variant.

Nested markets inherit parent customisations, and the most specific match wins. A market that only matches a country prices California and Texas the same, because that is the match you configured.

If you have already created a market whose conditions match that state, fine. Otherwise the state levy needs a different pen.

Why does the Function miss the address?

Usually because it is looking in the wrong drawer.

Checkout UI Extensions write cart attributes through applyAttributeChange. Shopify's Attributes API lists the hard limit: attribute changes are unavailable on accelerated checkout methods such as Apple Pay and Google Pay. A Function keyed off that attribute is reading a field the wallet never updated.

The address Shopify already holds is a better source. The current Discount Function input can include delivery groups with a deliveryAddress. Shopify's own Function examples treat city and other address fields as optional.

Billing address on the cart is also nullable. A wallet that fills shipping first can still leave billing empty on an early Function run. So the code has to cope with blanks.

Legacy product-discount and order-discount Function APIs are a poor home for this job, because delivery groups are the field you need.

Cart Transform can add or reshape lines, including a fee component. Line-update operations on Cart Transform are limited to development stores and Shopify Plus. It still needs a destination signal, and the delivery address on the Function input is the signal Shopify already owns.

Destination fee path

SKU needs a destinationfee or priceWhat must change?Country or configuredmarket priceState levy or extra lineMarkets catalog price listFunction readsdeliveryAddressHosted checkout andApple PayDestination fee path
Destination fee path

How do I find out who is holding the pen?

Start with a sheet. One row per SKU that merchandising, finance, or compliance says must change with destination.

For each row, write down the product, the rule (country price, state levy, B2B company price), the surface that currently owns the amount, and whether Apple Pay is on.

Then walk the current path in admin. Before any code.

For a country price, open Markets, open the catalog on that market, and check a price list or a percentage adjustment exists for the variant. For a state levy, search the checkout editor and the custom app list for a UI extension that calls applyAttributeChange. Then open the Function that reads the matching attribute key.

Now test. Place two hosted checkouts for that SKU: one to a destination that should charge the extra amount, one to a destination that should not. Repeat both carts with Apple Pay or Google Pay.

The results point at the culprit:

  • Hosted right, wallet wrong: suspect the attribute handoff first.
  • Both wrong: suspect the catalog or the Function input.

For the wallet order, open the Function run log, or replay the run with the CLI. Confirm whether deliveryAddress.provinceCode was present, whether billingAddress was empty, and whether the cart attribute you expected was missing. Shopify's Function examples already guard address fields with optional reads, and your Function should do the same.

Put the plan on the same sheet. Markets catalog prices can ship without a custom Function, while checkout UI Extensions and custom Functions that change checkout logic need Shopify Plus, except on a development store where Shopify documents Function line-update support. Confirm this before you commission a custom app. What Shopify Plan Should I Choose? is the plan conversation, and this sheet stays on the SKU and the surface.

Checklist rowWhat you recordPass criterionOwner
SKU and ruleProduct, variant, country price or state levy, amount sourceEvery destination-specific SKU is namedCommerce / finance
Current surfaceMarkets catalog, Function, fee product, cart attribute, or appOne named owner per amountTech
Market conditionsCountry only, nested region, B2B company locationThe market can actually match the destination you sell intoCommerce
Hosted checkoutOne cart that should charge, one that should notThe expected line and amount appearCommerce
Apple Pay or Google PaySame two carts through the walletThe wallet cart matches the hosted cartTech
Function logdeliveryAddress, billingAddress, attribute keys, API targetThe Function read Shopify's address, not a missing attributeTech

Which pen should I pick?

The Markets catalog wins when the amount follows a country or a market you have already configured. Create or open the market, attach a catalog, and set a percentage adjustment or a fixed local-currency price on the variant.

Shopify Help says customers can view, pay, and receive refunds in local currency when you sell to markets. You can control prices with rounding, percentage adjustments, and product prices by country or region. Full local-currency checkout needs Shopify Payments and one-page checkout.

Add a country selector so the buyer can confirm the market. Read the market price from Shopify. Do not compute it from the base variant in your own code.

A checkout Function that reads delivery address wins when the amount changes inside one country, or when you need a levy Shopify will not store on a catalog. Use the current Discount Function API so cart.deliveryGroups can carry deliveryAddress.provinceCode. Treat the address as optional. Fall back only when a second, documented field is present.

And stop using the sticky note as the only source of truth.

A UI extension can still explain the levy on hosted checkout. Explanation is fine. The Function must keep working when that extension never runs.

A separate fee product wins when finance needs a taxable, reportable line with its own SKU. Keep that product in the catalogue. Let Cart Transform or the Discount Function add or price it from the delivery address. Line-update operations on Cart Transform stay on development stores and Shopify Plus.

Give the fee a title the buyer will recognise on the order. Keep it out of collection grids, so nobody adds a recycling fee to their basket for fun.

Then take the other pens away

This is the step teams skip.

Once you pick the owner, freeze every other writer. If the Function now reads delivery address, remove the attribute write. If the catalog now owns that variant, remove the discount that was faking a country price.

Automatic discounts and Discount Functions can both change an amount at peak. When a promo and a levy share a cart, check both writers on the same sheet. Shopify Automatic Discounts vs Functions for Black Friday Promos belongs on that desk.

Now run the two hosted checkouts and the two wallet checkouts again. Screenshot the order summary. Store the Function version and the market name on the sheet.

Is this a settings job or a build?

A single country price on one catalog is a Markets settings task. An afternoon, not a project.

A state levy that must survive Apple Pay, share the cart with a promo Function, and post as its own taxable SKU is checkout architecture. Theme section edits will not attach a province levy.

Get a technical partner in when more than one app already writes cart lines. Or when the Function run log still shows an empty delivery address on a paid wallet order. Or when B2B company-location catalogs and region markets both claim the same variant. Our Shopify UI Extensions and Functions work starts by mapping those writers.

If the shipping step is empty while add-to-cart still looks healthy, finish this fee inventory first, then use How to Fix Shopify Checkout Abandonment When Your Funnel Looks Healthy.

Conclusion

You can now list every SKU that needs a destination-specific fee or price, choose a Markets catalog, a checkout Function that reads delivery address, or a separate fee product, and prove the rule on hosted checkout and Apple Pay.

The catalog owns country and configured-market prices. The Function owns levies that change inside one country, and it reads the address Shopify already has. Keep a fee product only when finance needs a reportable line, and treat cart attributes as a weak link on accelerated wallets.

One price tag and one pen, then two test checkouts, including the one paid with Apple Pay.

If your sheet still shows two pens on one amount, book a stack assessment.

Frequently Asked Questions

Use a Shopify Markets catalog with a price list for that market. Shopify can convert the store price at the market exchange rate, apply a percentage adjustment, or store a fixed local-currency price on the variant. Load the market price from Shopify rather than computing it from the base variant. Full local-currency checkout needs Shopify Payments and one-page checkout. A country selector should be on the storefront so the buyer can confirm the market Shopify assigned.

A Markets catalog follows the buyer context you configured. If that market only matches a country, every state in that country sees the same catalog price. Nested markets can be more specific when your conditions include the province or state you care about. A recycling or environmental fee that changes inside one country usually needs a checkout Function that reads deliveryAddress.provinceCode, or a separate fee product the Function adds, unless you have already created a market whose conditions match that state.

Shopify’s Checkout UI Extensions Attributes API states that attribute changes are unavailable when the buyer uses an accelerated checkout method such as Apple Pay or Google Pay. A UI extension that writes the shipping state into a cart attribute, then a Discount Function that reads that attribute, will miss the write on those wallets. Point the Function at the delivery group’s deliveryAddress on the current Discount Function API, treat the address as optional, and complete an Apple Pay test order before you trust the fee.

Use a Discount Function when you already have a fee line or a priced variant and you need to change the amount from the delivery address. Use Cart Transform when you must add, expand, or present a fee line that is not already in the cart. Line-update operations on Cart Transform are limited to development stores and Shopify Plus. Pair the Function with a checkout UI extension only when the buyer needs an explanation on the page. Display and logic stay separate, as in the checkout extensions guide.

Country-level catalog prices through Shopify Markets are available without a custom Function. Checkout UI Extensions and custom Shopify Functions that change checkout logic require Shopify Plus, except on development stores where Shopify documents Function line-update support. If the fee only applies inside one country and Markets cannot match that province, Plus (or an app that already ships the Function) is the path that can read delivery address at checkout. Confirm the plan before you scope a custom app.

Place one hosted checkout to a destination that should charge the fee and one that should not, using a product from the SKU list. Repeat both carts with Apple Pay or Google Pay, and with Shop Pay if that wallet is on. Screenshot the order summary, the fee title, and the address Shopify stored. Open the Function run log or replay the run when the wallet cart disagrees with the hosted cart. Keep the vendor or Function version on the same sheet as the screenshots.

Bring in a technical partner when a Markets catalog, a Discount Function, and a fee SKU all write to the same checkout, or when Apple Pay still drops the line after you stopped using cart attributes. A checkout Functions review maps which surface owns the amount before you add another app. Theme edits will not attach a state levy. If purchases stop after the fee appears, use the checkout abandonment guide once the fee inventory is done.

Related Articles