Cart & Checkout · · 12 min read

Adding a fee to the Shopify cart, without a product

Shopify has no primitive for “charge a bit extra.” Here is what merchants build instead, why the usual workaround quietly breaks four other things, and what the Cart Transform API actually changed.

A small ink dot and a large forest-green disc resting on a single hairline rule: the base price and the fee added to it
Fig. 01. One line, two charges. The whole problem in one drawing.
On this page

In short

Shopify has no native cart fee. The workaround most stores ship, a $0.01 “Handling Fee” product added to the cart, leaks into collections, search, reports and shipping calculations. The supported mechanism is the Cart Transform function's lineExpand operation, which splits a real cart line into components and prices each one, producing a genuine fee line item that survives all the way to the receipt. It works on every Shopify plan when it ships inside a public App Store app.

Why there is no native cart fee #

Shopify has no general-purpose way to add money to a cart that is not attached to a product. A Shopify staff member stated it plainly on the developer forums in June 2025: the official Function APIs “only allow you to reduce prices or block/validate, but not to add a fee.” That single sentence explains why an entire app category exists.

What Shopify does ship is three narrow mechanisms, each bolted to something else. None of them generalises.

Native Shopify charges that are not products
MechanismWhere it livesWhy it doesn't solve the problem
Shipping handling fee Settings → Shipping and delivery Only applies to carrier- or app-calculated rates, never to a flat rate. It is blended into the shipping total, so it is never its own line. Percentage is applied before the flat amount.
Checkout tipping Settings → Checkout → Tipping Three preset percentages, calculated on subtotal. It is a tip, and it is unavailable on carts containing a subscription.
Draft order custom line Admin API, draft orders Genuinely clean: a title and a price, no product required. But draft orders are merchant-created; this never touches buyer-driven online-store checkout.

Notice the shape of that table. Shopify clearly can render a money line in checkout that is not a product; tipping proves it. It just exposes exactly one, and hardcodes it.

Before you pay for anything

If the charge you want is a tip, stop here. Shopify's native tipping is free, takes three minutes to configure, and merchants routinely relabel it for charity fundraisers. Do not install an app for it.

What the hidden-product hack actually breaks #

The hidden-product hack is the most widely shipped Shopify fee workaround, and it is a product masquerading as a charge. You create a product called “Handling Fee” priced at the fee amount, leave it available to the Online Store channel (cart JavaScript cannot add a variant that is not), hide it everywhere you can, and add it to the cart with JavaScript when a condition is met. It works, in the narrow sense that money arrives. Then it leaks.

The leaks are not theoretical. A product is a product everywhere in Shopify, so every system that reads products picks it up:

  • It appears where products appear. Automated collections with rules like “price is less than $5” will scoop it up. Internal search can surface it. Anything that generates a feed from the catalogue, including Google Shopping, has to be told about it explicitly.
  • It distorts your reporting. Every fee-bearing order now contains an extra unit sold. “Products sold”, units per transaction, and best-seller rankings all shift, and the distortion grows with fee volume.
  • It can change shipping cost. This is the expensive one. If the fee product has a weight and is marked as requiring shipping, it is fed into carrier-calculated rates as real cargo. A fee designed to recover cost quietly adds cost.
  • The customer can edit it. It is a line item like any other, so a shopper can change its quantity or remove it entirely from the cart page unless you write code to stop them.
  • Discounts treat it as merchandise. A 20%-off-everything code discounts your handling fee too.

Two of those five follow the fee no matter how it is built. A fee that becomes a real line item through the supported route is still caught by order-level discounts unless the discount logic excludes it, and it still adds a unit to the order. What the supported route actually fixes is the storefront exposure, the shipping weight and the editability; the discount and reporting behaviour come back later in this article.

Every one of these is fixable with enough guard code. That is the actual argument against the hack: you do not ship it and walk away, you ship it and then maintain a growing list of exceptions in collection rules, feeds, reports and shipping settings. Forever.

Can Shopify Functions add a line item to a cart? #

Yes, through the Cart Transform API's lineExpand operation, though the mechanism is a side effect rather than a designed feature. This is worth stating clearly because the highest-ranking answer to this question is stale: a Shopify staff reply from April 2023 says “There is no way to do this with Functions currently.” That predates the Cart Transform release that made it possible, and merchants are still finding it.

Cart Transform exposes three operations: lineExpand, linesMerge and lineUpdate. The one that matters takes a single cart line and replaces it with several component lines, each of which can carry its own fixed per-unit price:

The shape of a fee, expressed as an expand operation

{
  "lineExpand": {
    "cartLineId": "gid://shopify/CartLine/1",
    "title": "Ceramic mug",
    "expandedCartItems": [
      {
        "merchandiseId": "gid://shopify/ProductVariant/111",
        "quantity": 1,
        "price": { "adjustment": { "fixedPricePerUnit": { "amount": "24.00" } } }
      },
      {
        "merchandiseId": "gid://shopify/ProductVariant/999",
        "quantity": 1,
        "price": { "adjustment": { "fixedPricePerUnit": { "amount": "2.99" } } }
      }
    ]
  }
}

The original item keeps its price. A second component, the fee variant, is priced independently. Because component prices are arbitrary, the cart total goes up. This is a bundling API being used as a fee API, and Shopify's own documentation ships “add gift wrapping to cart items” as the worked example, so the usage is sanctioned even if the naming is not.

One thing to be straight about: variant 999 in that example is a real product variant. lineExpand cannot conjure a line from nothing, and Shopify returns component_merchandise_not_found when the ID does not exist. A fee app, ours included, keeps a hidden charge product in the store to serve as that component. The difference from the hack is everything around it: the charge variant never needs publishing to a sales channel because the function references it server side, it is created non-shippable so it cannot touch carrier rates, its price comes from the function at cart time rather than from the product record, and the shopper cannot edit or remove it as a line of its own. “Without a product” means without a product your storefront sells, not without a variant record in Admin.

The payoff is that the result is a real line item. It shows in the cart, survives to checkout, lands on the order and the receipt, and appears in payouts like anything else. Nothing is hidden in a subtotal.

The two-part pattern

A function cannot read a checkbox. The production pattern is split: the storefront writes a cart attribute recording the shopper's choice, and the Cart Transform function reads that attribute to decide whether to expand. Attributes carry no price effect of their own; they are purely the signal.

Do you need Shopify Plus to add a cart fee? #

No, and the widespread claim that you do is wrong in a specific, checkable way. Several published guides state that Cart Transform functions require Shopify Plus. Shopify's documentation gates only one of the three operations, and it is not the one fee apps use.

Who can run what
CapabilityPlan requirement
Public App Store app containing FunctionsAny plan
Custom app containing FunctionsShopify Plus only
lineExpand / linesMergeAny plan
lineUpdatePlus or development stores only
Checkout UI extensions on information / shipping / payment stepsShopify Plus only

That distinction is the whole reason fee apps are built on lineExpand rather than the more obvious lineUpdate. It is also why installing an App Store fee app works on Basic, while building the identical function yourself as a custom app does not.

You do not have to guess, either. The Admin API exposes the gate directly: ShopFeatures.cartTransform returns eligibleOperations with three booleans (expandOperation, mergeOperation, updateOperation) telling you exactly what a given shop may do before you write any code.

Why percentage fees are harder than they look #

Cart Transform's percentage adjustments can only move prices down. The only percentage field the API exposes is percentageDecrease, and its validation error, invalid_price_adjustment_percentage_decrease, pins it to discounts: “the percentage decrease value must be less than or equal to 100.” A percentage increase is not so much rejected as inexpressible.

So a “2.9% + $0.30” surcharge is not a percentage operation at all. The function has to read the line costs, compute the amount itself, and emit the result as a fixed per-unit price. Percentage fees are arithmetic you own, not a flag you set, which is why they tend to sit in paid tiers.

Multi-currency compounds it. The function's input exposes presentmentCurrencyRate, and every output is processed in the presentment currency. A fee denominated in shop currency has to be multiplied by that rate by hand. When a fee app advertises “Shopify Markets support”, this is the work being described: real, necessary, and not something the platform does for you.

What a cart fee collides with #

A fee line does not live alone in the cart, and the collisions are the part that surprises people after launch. Shopify runs functions in a fixed order: cart transform first, then cart line discounts, then fulfillment and delivery methods, then delivery discounts, then payment customizations, with validation last.

Four consequences follow directly from that ordering and from the API's own limits:

  • Discounts see the fee. Because the transform runs first, the fee line already exists when discounts are calculated. An order-level “all products” code will discount it unless the discount logic excludes that line deliberately.
  • Apps can silently cancel each other. Each app gets one cart transform, but several apps' transforms all run, and collisions resolve by activation time, and the first activated wins. A store running a bundle app and a fee app can lose the fee, and which one survives depends on install order.
  • Subscriptions are excluded. Shopify rejects lineExpand, linesMerge and lineUpdate outright when a selling plan is present on the line. You cannot attach a Cart Transform fee to a subscription item, and the same compatibility table lists pre-order and try-before-you-buy as unsupported and POS as only partially supported, so deferred and in-person flows cannot be counted on to carry the fee either.
  • Failure mode is a choice you make. The activating mutation takes a blockOnFailure flag. Leave it false and a crashing function means fees silently stop being collected. Set it true and a bug in your function takes down checkout. Neither is comfortable; pick on purpose.

There are hard caps too: a maximum of 150 expanded cart items, and either all components carry prices or none do.

Since February 2025, Shopify's App Store requirements have forbidden apps from adding optional charges without explicit buyer consent. Requirements 1.1.9 and 5.6.5 carry identical text: apps “can't automatically add or pre-select optional charges to a buyer's cart that increase the total checkout price,” and may only add them “after displaying the additional cost in a manner that is clear to the buyer, and upon obtaining explicit buyer consent.”

Two further clauses, written for the post-purchase upsell section, show the pattern Shopify expects any optional charge to follow. Requirement 5.8.2 says the buyer “must be provided preset accept and decline options.” Requirement 5.8.8 says the price must update dynamically when quantity or variant changes. They bind post-purchase offers directly; read them as Shopify's clearest statement of what consent UI should look like. Non-compliance risks rejection or removal from the App Store.

In practice that settles a design argument that used to be contentious. An optional charge defaults to unchecked. The requirement's wording bans pre-selecting outright, so do not design a pre-ticked box and plan to collect consent afterwards. A pre-ticked fee is the pattern that App Store review and chargeback disputes punish alike.

Note the rule governs optional charges. A genuinely mandatory fee, such as a statutory bag charge or a bottle deposit, is a different compliance posture and is usually enforced with a validation function rather than a checkbox.

Disclosure

Normalize builds TackOn, one of the apps in this category. The mechanism described above is the platform's, not ours. The platform claims on this page link to Shopify's documentation, and the two staff quotes link to their original threads, so you can check everything without taking our word for it. The section below is where we tell you what we sell.

Build it or install an app? #

Build it if the fee logic is genuinely unusual and you have a developer who will still be there in a year. The function itself is the small part. The rest of the surface is what takes the time:

  1. A Cart Transform function, plus the Admin mutation to activate it and the write_cart_transforms scope.
  2. A theme app extension that renders the opt-in control and finds the cart in whichever theme the merchant is running, then keeps finding it after they change themes.
  3. Consent UI that satisfies requirements 1.1.9 and 5.6.5, following the accept-and-decline and dynamic-price pattern of 5.8.2 and 5.8.8.
  4. Currency conversion against presentmentCurrencyRate.
  5. An admin surface for the rules, and reporting so someone can tell whether any of it is working.

If the fee logic is ordinary (handling, shipping protection, gift wrap, a rush charge, a seasonal surcharge), that is a solved problem and buying it back is the cheaper decision.

Our app, stated plainly

What
TackOn: Cart Fees & Surcharges. Opt-in cart fees built on a Shopify cart-transform function, so the charge is a real line item through checkout. Launched December 2025.
Rules
Fixed, percentage or combined (% + fixed) charges, applied per item or once per cart, with price-based tiers, product bindings and date scheduling.
Price
Free for 20 fee-bearing orders a month. Starter is $5/month including 50, then $0.10 per additional order, capped at $99.99/month. Orders without a fee, abandoned carts and test orders never count.
Revenue
No commission and no revenue share. The money lands in your Shopify payout. This is the default for anything Cart-Transform-based, because Shopify's checkout pays one party.

The honest version of the pitch: this article describes the mechanism, and TackOn is our implementation of it. If you would rather build your own, the sections above are the map. If you would rather not, that is what we sell.

Questions merchants actually ask #

Does Shopify have a native way to add a fee to the cart?

No. In buyer-facing checkout, Shopify ships exactly two configurable charges that are not products: a handling fee that only attaches to carrier- or app-calculated shipping rates, and checkout tipping. Draft orders accept a custom line with a title and a price, but only on merchant-created orders. None of the three expresses a general-purpose cart surcharge.

Do you need Shopify Plus to add a cart fee?

No. Stores on any plan can install public App Store apps that contain Shopify Functions, and the lineExpand operation that fee apps are built on is not plan-gated. Only the lineUpdate operation is restricted to Shopify Plus and development stores, and only custom apps containing Functions require Plus.

Can a Shopify checkout UI extension add a fee?

No. The Cart Lines API's applyCartLinesChange accepts a merchandise ID and a quantity, plus optional line attributes and a selling plan ID. There is no price parameter, so a checkout extension can add an already-priced variant and nothing more, and the API returns an error during accelerated checkout such as Apple Pay, Google Pay or Meta Pay, so the add simply does not happen there.

Will a discount code apply to a Shopify cart fee?

Usually yes. Shopify runs cart transform functions before discount functions, so the fee line already exists in the cart when discounts are calculated. An order-level discount that targets all products will therefore discount the fee unless the discount explicitly excludes that line.

Can you add a fee to a Shopify subscription line?

No. Shopify rejects lineExpand, linesMerge and lineUpdate operations outright when a selling plan is present on the cart line. The same compatibility table lists pre-order and try-before-you-buy as unsupported and POS as partially supported.

Does a cart fee stay visible through Shopify checkout?

Yes, when it is created with Cart Transform. The fee becomes a real cart line item, so it appears as its own line in the cart, through checkout, on the order and on the receipt rather than being folded invisibly into the total.

Sources #

Every platform claim above traces to primary documentation or a linked staff answer. Where Shopify's docs and a vendor's marketing disagree, the docs win.

Abstract geometric portrait of Onik G.

Written by

Onik G., Engineering

Builds Shopify apps and storefront systems at Normalize, a commerce software studio in Dhaka. Four apps on the Shopify App Store, all held to the same standard: zero layout shift, theme-native, no third-party trackers.

TackOn · Cart fees & surcharges

Skip the function. Ship the fee.

Opt-in cart fees as real line items, with rules, tiers and scheduling. Free for 20 fee-bearing orders a month, and you keep 100% of what you collect.