Magento Add Extra Fee in Checkout: Code vs Extension
Add a fee to Magento checkout with custom code or an extension. Covers payment, shipping and customer group fees, plus what breaks.

Magento has no built-in way to add an arbitrary fee to checkout. No admin setting, no core feature, nothing to toggle. So every store that needs a handling charge, a payment surcharge, or a small order fee ends up choosing between two paths: write a custom totals collector, or install an extension.
We have done 800+ Magento builds, and we also build and sell the OCM Labs Extra Fees module for Magento. So read this knowing where our money comes from. We will still show you the working custom code first, because for some stores custom code is the right answer, and you should know which store you are.
What does it take to add an extra fee to Magento checkout?
You register a custom total in a sales.xml file and write a collector class that adds the amount during totals collection. That gets a fee into the quote's grand total, and it is about 80 lines of code. The other 90 percent of the job is everything after the quote: rendering the fee in the checkout summary, persisting it to the order, carrying it onto invoices, credit memos, emails, and PDFs, taxing it, and refunding it exactly once.
Here are the 80 lines, followed by what they do not do, because that list is what decides custom versus extension.
The custom code path: a working totals collector
Two files. First, etc/sales.xml registers the total in the quote's collector chain:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Sales:etc/sales.xsd">
<section name="quote">
<group name="totals">
<item name="acme_handling" instance="Acme\Fee\Model\Total\Quote\Handling" sort_order="420"/>
</group>
</section>
</config>The sort order matters. Core collects the subtotal at 100, shipping at 350, tax at 450, and the grand total at 550. A fee at 420 lands after shipping and before tax and the grand total.
Second, the collector itself:
<?php
declare(strict_types=1);
namespace Acme\Fee\Model\Total\Quote;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Magento\Quote\Api\Data\ShippingAssignmentInterface;
use Magento\Quote\Model\Quote;
use Magento\Quote\Model\Quote\Address\Total;
use Magento\Quote\Model\Quote\Address\Total\AbstractTotal;
class Handling extends AbstractTotal
{
private const CODE = 'acme_handling';
private const BASE_FEE = 10.00;
public function __construct(private readonly PriceCurrencyInterface $priceCurrency)
{
$this->setCode(self::CODE);
}
public function collect(
Quote $quote,
ShippingAssignmentInterface $shippingAssignment,
Total $total
) {
parent::collect($quote, $shippingAssignment, $total);
if (!count($shippingAssignment->getItems())) {
return $this;
}
$baseFee = self::BASE_FEE;
$fee = $this->priceCurrency->convertAndRound($baseFee, $quote->getStore());
$total->setTotalAmount(self::CODE, $fee);
$total->setBaseTotalAmount(self::CODE, $baseFee);
return $this;
}
public function fetch(Quote $quote, Total $total): array
{
return [
'code' => self::CODE,
'title' => __('Handling Fee'),
'value' => $total->getTotalAmount(self::CODE),
];
}
}This adds a flat $10 fee (in base currency, converted to the display currency) to every cart with items. collect() runs during totals collection; fetch() exposes the fee as a totals segment so the frontend and the totals REST API can see it.
One common bug: do not add your fee to the grand total yourself. The grand total collector at sort order 550 sums every registered total amount. If your collector also calls setGrandTotal($total->getGrandTotal() + $fee), the fee gets counted twice. setTotalAmount() and setBaseTotalAmount() are all you need.
What this collector does not handle
Each item below is separate work.
Checkout display. fetch() puts the fee in the totals payload, but Luma's cart and checkout render totals with Knockout components declared in jsLayout. An unknown segment does not display. You need a JS component extending Magento_Checkout/js/view/summary/abstract-total plus jsLayout entries in both checkout_cart_index.xml and checkout_index_index.xml before a shopper sees the line.
Conditions. The sample charges everyone. The moment someone says "only for carts under $50" or "only for hazmat SKUs," you are writing condition logic, and product attributes are not free to check: quote items only carry a short whitelist of attributes, so custom attributes mean reloading products.
Persistence to the order. Quote data dies at order placement unless you copy it. That means new columns on sales_order plus a fieldset.xml entry or an observer on sales_model_service_quote_submit_before. Skip this and the fee is in the grand total but invisible on the order view, which confuses everyone who opens the order.
Invoices and credit memos. Order totals do not flow into invoice totals automatically. You need separate collectors for the invoice and credit memo, registered in sales.xml under order_invoice and order_creditmemo, plus a policy for partial invoices: which invoice carries the fee, and how do you stop a second credit memo from refunding it again? Untracked fees get refunded twice.
Tax. The sample fee bypasses Magento's tax engine entirely. If the fee is taxable where you sell, you have to register it as an associated taxable so core computes the tax, or maintain your own tax math across zones and customer groups. Nobody should maintain their own tax math.
Emails and PDFs. Order, invoice, and credit memo emails render totals from blocks; PDF invoices are drawn by a separate renderer registered through pdf.xml. Each surface needs the fee added or the shopper sees one number at checkout and a different one in their inbox.
Admin order creation. Orders placed by staff in the admin run the same quote collectors, which works in your favor here, but any logic that assumed a frontend session breaks.
We wrote a whole post on the second half of this list, because it is where DIY fees actually fail: what happens to checkout fees on invoices and credit memos.
When is custom code the right call?
Custom code wins when the fee is one, flat, unconditional, and boring. A $2 processing fee on every order, always invoiced in full, never partially refunded, on a store with a developer who can own the code. In that case the collector above plus a day of wiring for display and persistence is cheaper than any extension, and there is no third-party dependency to update at upgrade time.
It is also the right call when your logic is too strange for any rule builder. Some pricing rules are better written as code than expressed in any configurable module. If you need that kind of thing built, that is what our Magento support retainers are for.
When does an extension pay for itself?
The math flips as soon as any of these are true:
- You need more than one fee, or the amounts change often enough that a non-developer should manage them.
- Fees depend on conditions: payment method, shipping country, customer group, product attributes, cart subtotal.
- The fee is taxable.
- Your store does partial invoices or partial refunds.
- You need the fee correct on emails, PDFs, and the admin grid, not just at checkout.
Each of those is days of development and testing, not hours. At $250 one-time, the extension pays for itself the first time you do not have to build invoice-level fee tracking by hand. Updates and support for new Magento versions are included for 12 months.
What does the OCM Labs Extra Fees module do?
The OCM Labs Extra Fees module for Magento turns fees into rules you manage in the admin under Sales > Extra Fees. Each fee is fixed or percentage based, shows as its own labeled line item in cart and checkout, and carries through the entire order lifecycle without template edits. This is our module, so treat this section as the sales pitch it is; everything in it is in the public docs where you can verify it.
The short version of what you get:
- A condition builder. Fees trigger on product attributes and SKUs (six operators) or on eight cart-level conditions: subtotal, item quantity, shipping country, region, postcode, customer group, shipping method, and payment method. Conditions combine with AND or OR. A fee with no conditions applies to every cart.
- Fixed or percentage fees, with caps. Percentage fees compute from the matching items' pre-discount, tax-exclusive row totals and accept optional min and max amounts.
- Optional fees. Mark a fee optional and it becomes an unchecked checkbox in the checkout summary that the shopper opts into, with the same selection exposed over REST and GraphQL for headless builds.
- Tax done by core. Taxable fees run through Magento's tax engine with a per-fee tax class or a global default. There is also a tax-inclusive entry mode for stores that price that way.
- The full lifecycle. Fees appear on orders, invoices, credit memos, all three email types, PDF documents, customer account and guest views, and admin order creation. The fee is captured on the first invoice and refunded on the first credit memo after it, never more than was invoiced. The details are in the order lifecycle docs.
- Admin tooling. Store view scoping with per-store labels, date-range scheduling, a Fee Revenue report showing invoiced and refunded amounts per order, a change log of who edited what, and a view-only ACL role for finance staff.
The module supports Magento Open Source and Adobe Commerce 2.4.7 to 2.4.9 on PHP 8.3 to 8.4, with Luma and Hyva. Cart totals render through the included Hyva compatibility module, and checkout runs through the standard Luma fallback; the native Magewire-based Hyva Checkout product is not supported. The price is $250 one-time, with 12 months of updates and support included.
If you are still comparing options, our other modules live on the Magento extensions page.
How do you add a fee by payment method, shipping method, or customer group?
These are the fee types stores ask for most, and in the Extra Fees module each one is a single fee record with a single condition. Create the fee under Sales > Extra Fees > Manage Fees, choose fixed or percentage, add the condition, and the fee shows as its own labeled line in the checkout totals. The full condition list is in the fees and conditions docs.
Payment method fees
Add a Payment Method condition set to the method code: checkmo for check or money order, cashondelivery for cash on delivery, or whatever code your card gateway registers. Is and Is Not both work, so you can charge for one method or for everything except your preferred one.
Payment and shipping conditions never match on an incomplete cart. Until the shopper has selected a payment method, the condition fails and nothing shows on the cart page. The fee appears at the payment step, when the choice exists. That is deliberate.
Writing this yourself has a trap. $quote->getPayment()->getMethod() is the right call, and during Luma checkout it still returns nothing. The selected radio button lives in client-side checkout data, and the server does not hear about it until payment information is saved, usually at Place Order. Your collector skips the fee, then the order goes through with a fee the shopper never saw. The fix is a JS mixin on the payment selection action that sends the method to the server and triggers a totals recollection. That mixin and the totals refresh are where "a 3% card fee" stops being an afternoon of work.
Before you surcharge cards
Card surcharges are regulated by the card brands and by some states. Check your processor agreement and local law before you add one. A fee on cash on delivery or purchase orders is a business decision, not a card-brand matter.
Shipping method fees
Add a Shipping Method condition with the carrier and method code in carrier_method format, for example flatrate_flatrate. The shopper sees "Express Processing Fee" as its own line instead of wondering why express shipping costs what it does. Like payment conditions, this one resolves only after a method is picked, and never on a virtual-only cart.
Remote area surcharges use the Shipping Country, Region, and Postcode conditions: Shipping Country Is US AND Shipping Postcode Is a specific code, or a region rule for shipments to Alaska and Hawaii.
Customer group fees
Add a Customer Group condition. A fee only wholesale accounts pay and a fee everyone pays except one group are each one rule. Conditions mix freely, so wholesale accounts paying by check on orders under $500 is Customer Group Is Wholesale AND Payment Method Is checkmo AND Cart Subtotal < 500, on a single fee.
The AND/OR toggle applies across the whole condition set. Under AND, each condition must be satisfied by at least one cart item, but a single item does not have to match all of them at once.
Percentage surcharges and stacking
A percentage fee is calculated from the matching items only, using pre-discount, tax-exclusive row totals, not from the order grand total. Shipping and tax are not in the base. Check the resulting amount against your processor agreement. Enter 3 for 3%, not 0.03, and use the min and max caps to stop a percentage growing past a limit.
Every fee whose conditions match fires on its own line. There is no priority or conflict resolution, so a card surcharge plus a small order fee means a small cart paid by card gets both. Review your conditions as a set whenever you add a fee, and use Is Not conditions to carve out overlaps you do not want.
Custom code or extension: the comparison
Custom code: free to write, yours to maintain, and correct exactly as far as you build it. Budget for the invoice, credit memo, email, PDF, and tax work before you decide it was free.
Extension: $250, someone else's maintenance burden, and lifecycle behavior that has already been argued about, decided, and tested. The tradeoff is that you live inside its rule model. Ours is documented in full, so you can check whether the rule model fits before spending anything.
If you already know your fee is one flat amount forever, write the collector. Everyone else, do the arithmetic on your developer's day rate first. To see the rules in the admin, start on the Extra Fees product page.
✦ FAQ
Does Magento have a built-in extra fee feature?+–
How do I add a fee to Magento checkout programmatically?+–
Why is my custom fee doubled in the grand total?+–
Why does my custom fee disappear from the invoice?+–
How much does the OCM Labs Extra Fees module cost?+–
Can one store run multiple extra fees at once?+–
How do I add a fee for a specific payment method in Magento?+–

Shane Blandford
Founder of Orange Collar Media, a Denver ecommerce agency behind 800+ Magento and Shopify builds. In Magento since version 1.x - building, rescuing, and supporting revenue-critical stores since 2008.
✦ KEEP READING
Related from the Journal.
Magento 2 Follow-Up Emails After Purchase: What Core Does and What It Doesn't
Magento 2 sends no delayed follow-up emails after purchase. What core does, what a DIY cron queue needs, and where a per-SKU module fits.

Magento 2 Email Template Variables: {{var}}, {{for}} and Product Data
A working reference for Magento 2 email template variables: var, trans, if, depend, for loops, layout, strict mode, and how to list product data per item.

How to Customize the Magento 2 Checkout Success Page by Product
Customize the Magento 2 checkout success page per product: a CMS block for every order, a SKU-checking view model, and the gotchas.
