Custom Shipping Rate in Magento 2 | Building Your Own Shipping Cost Logic
AI generated
Magento 2 · Shipping Logic

Custom Shipping Rate
building your own shipping cost logic in Magento 2

Once shipping costs are no longer flat rates or simple table based rules, your shop needs its own carrier logic. Magento 2 offers a clear entry point for this through a custom shipping method with `collectRates()` and system configuration.

14 min read Shipping Carrier Magento 2.4.8

1. When a custom shipping logic makes sense

A Custom Shipping Rate in Magento 2 becomes interesting once the standard shipping methods are no longer enough. This is the case, for example, when shipping costs depend on cart weight, product attributes, delivery zones, customer groups, hazardous goods, cold chain shipping or an external API. In these situations, a custom carrier implementation is often cleaner than workarounds built on price rules or confusing tables.

Magento already ships with shipping methods and Table Rates. For simple scenarios these are sufficient. But once the logic becomes functionally complex, or you need data from an ERP, a freight carrier API, or individual product rules, a custom carrier is the better path. A Custom Shipping Rate in Magento 2 can then be cleanly encapsulated, configured and tested inside a module.

One important boundary matters here: custom shipping logic should not turn into a collection of hardcoded special cases in the template or checkout frontend. The calculation belongs in a server side carrier class and its services. That way the behavior stays traceable and remains controllable even through later Magento upgrades.

2. Module structure and configuration

For a Custom Shipping Rate in Magento 2 you need a regular module with a carrier class, configuration and admin settings. The module should live cleanly under app/code/Vendor/Module. Configuration is typically done through config.xml, system.xml and etc/acl.xml, so the shop owner can enable the method, change the title, or control base prices.

In this example we use a module called Mironsoft_CustomShipping. It gets its own configuration area with an active flag, title, method name and base price. These values should not be hardcoded in the carrier. Especially with shipping methods, texts and prices often change during operation. A Custom Shipping Rate in Magento 2 without admin configuration is therefore only useful for very narrow special cases.


<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <carriers>
            <mironsoft_customshipping>
                <active>1</active>
                <title>Mironsoft Shipping</title>
                <name>Custom Shipping Rate</name>
                <price>9.90</price>
            </mironsoft_customshipping>
        </carriers>
    </default>
</config>

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <section id="carriers">
            <group id="mironsoft_customshipping" translate="label" sortOrder="910"
                   showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Mironsoft Custom Shipping</label>
                <field id="active" translate="label" type="select" sortOrder="10"
                       showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Enabled</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="title" translate="label" type="text" sortOrder="20"
                       showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Title</label>
                </field>
                <field id="name" translate="label" type="text" sortOrder="30"
                       showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Method Name</label>
                </field>
                <field id="price" translate="label" type="text" sortOrder="40"
                       showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Base Price</label>
                </field>
            </group>
        </section>
    </system>
</config>

For the method to appear cleanly in the admin, the XML structure and the carrier code must use the same carrier ID. Here that is mironsoft_customshipping. Exactly these kinds of small inconsistencies are often the root cause later, when a Custom Shipping Rate in Magento 2 was seemingly developed correctly but never shows up at checkout.

3. A custom carrier class with collectRates()

The heart of a Custom Shipping Rate in Magento 2 is the carrier class. It typically extends AbstractCarrier and implements CarrierInterface. The central method is called collectRates(). Magento calls it whenever available shipping methods are calculated for the current address and the current cart.

Inside collectRates() you first check whether the method is enabled. Then you evaluate the request context: destination address, items, weight, subtotal, customer context, or other factors. If the method is not applicable, you return false. If it is applicable, you build a RateResult with one or more shipping methods.


<?php
declare(strict_types=1);

namespace Mironsoft\CustomShipping\Model\Carrier;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Quote\Model\Quote\Address\RateRequest;
use Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory;
use Magento\Quote\Model\Quote\Address\RateResult\MethodFactory;
use Magento\Quote\Model\Quote\Address\RateResultFactory;
use Magento\Shipping\Model\Carrier\AbstractCarrier;
use Magento\Shipping\Model\Carrier\CarrierInterface;
use Psr\Log\LoggerInterface;

/**
 * Custom shipping carrier implementation.
 */
final class CustomShipping extends AbstractCarrier implements CarrierInterface
{
    protected $_code = 'mironsoft_customshipping';

    public function __construct(
        ScopeConfigInterface $scopeConfig,
        ErrorFactory $rateErrorFactory,
        LoggerInterface $logger,
        private readonly RateResultFactory $rateResultFactory,
        private readonly MethodFactory $methodFactory,
        array $data = []
    ) {
        parent::__construct($scopeConfig, $rateErrorFactory, $logger, $data);
    }

    /**
     * Collects available shipping rates for the current quote address.
     */
    public function collectRates(RateRequest $request)
    {
        if (!$this->getConfigFlag('active')) {
            return false;
        }

        $result = $this->rateResultFactory->create();
        $method = $this->methodFactory->create();

        $method->setCarrier($this->_code);
        $method->setCarrierTitle((string) $this->getConfigData('title'));
        $method->setMethod('standard');
        $method->setMethodTitle((string) $this->getConfigData('name'));

        $price = (float) $this->getConfigData('price');

        $method->setPrice($price);
        $method->setCost($price);

        $result->append($method);

        return $result;
    }

    /**
     * Returns the allowed shipping methods for this carrier.
     *
     * @return array<string, string>
     */
    public function getAllowedMethods(): array
    {
        return [
            'standard' => (string) $this->getConfigData('name')
        ];
    }
}

This baseline delivers a single shipping method with a configurable price. It is not yet complex business logic, but the crucial structure is in place. Every serious Custom Shipping Rate in Magento 2 builds on exactly this pattern and then extends the price or availability rules.

4. Returning a RateResult and shipping method

Many developers initially understand collectRates() as a function that simply returns a number. In reality, Magento expects a RateResult object with one or more Method instances. This is exactly where you define how the shipping method appears at checkout. Carrier code, method code, title and price must be consistent, otherwise the method gets built but is not displayed cleanly at checkout.

A Custom Shipping Rate in Magento 2 can also return multiple methods from a single carrier, for example Standard, Express and Same Day. In that case the carrier stays the same, but the method code and price logic differ. For more complex cases this is often cleaner than several completely separate carrier classes.

The distinction between price and cost also matters. setPrice() is the amount the customer sees. setCost() can be relevant for internal logic or margin considerations. Many simple implementations set both values identically. That is technically fine as long as no differentiated cost logic is needed.

5. Building your own shipping cost rules

Now comes the genuinely interesting part: the business calculation. A Custom Shipping Rate in Magento 2 becomes valuable once you can build rules that go beyond a fixed price. Typical criteria are weight, number of packages, hazardous goods flags, destination country, postal code zones, bulky products, warehouse locations, or B2B customer contexts.

This logic should not grow uncontrollably directly inside the carrier class. For simple cases that is still manageable. Once several conditions get combined, a separate service class such as ShippingPriceCalculator pays off. The carrier then stays an integration point with Magento, and the price logic itself remains independently testable.


<?php
declare(strict_types=1);

namespace Mironsoft\CustomShipping\Service;

use Magento\Quote\Model\Quote\Address\RateRequest;

/**
 * Calculates shipping rates based on quote conditions.
 */
final class ShippingPriceCalculator
{
    /**
     * Calculates the final shipping price.
     */
    public function calculate(RateRequest $request, float $basePrice): float
    {
        $weight = (float) $request->getPackageWeight();
        $subtotal = (float) $request->getPackageValueWithDiscount();

        if ($subtotal >= 150.00) {
            return 0.0;
        }

        if ($weight > 10.0) {
            return $basePrice + 5.0;
        }

        return $basePrice;
    }
}

With this kind of separation you can extend a Custom Shipping Rate in Magento 2 step by step: free shipping thresholds, surcharges, exclusions, API based real time prices, or dependencies on product attributes. The checkout stays the same, but the rule engine becomes cleaner. That is exactly what remains maintainable in the long run, compared to a carrier with endless if else blocks.

6. Common mistakes

The typical mistakes with custom shipping methods are fairly predictable. First, the carrier ID and the XML configuration are inconsistent. Second, false gets returned too early or too late. Third, the carrier class mixes price logic, API calls and quote evaluation directly inside a single method. Fourth, configuration values get hardcoded. Fifth, nobody checks whether certain destination countries or cart combinations should even allow the method.

Another common mistake is a lack of debugging against real request data. The RateRequest contains weight, values, destination country and other information, but not always in the form you would spontaneously expect. A Custom Shipping Rate in Magento 2 should therefore never be developed only against a minimal cart. Different product combinations, virtual products, discount cases and multistore contexts all need to be checked.

Performance can also become relevant. If a shipping method queries multiple external APIs on every checkout step, the frontend becomes noticeably sluggish. In such cases you need caching, asynchronous pre calculation, or a more deliberate architecture than simply "call the API inside collectRates()".

Especially in a B2B context it is also worth looking at multistore and multi currency cases. A shipping method may need different carrier titles, base prices or free shipping thresholds per website. If these differences are considered early in the configuration, the solution stays consistent and administrable even across multiple shops within one instance.

7. Custom carrier vs. Table Rates

Not every shipping logic needs a custom carrier right away. Table Rates are often sufficient for linear, tabular pricing rules. A Custom Shipping Rate in Magento 2 really pays off once rules become dynamic or depend on information that can hardly be modeled cleanly with Table Rates anymore.

Approach Well suited for Limit
Table Rates Simple zone, price or weight based tables Weak for dynamic or API based rules
Custom carrier Complex business logic, integrations, multiple conditions More development and testing effort
External shipping extension Standardized carrier integrations Less flexible for project specific special rules

The decision should not be ideological. If Table Rates are enough, use them. If the business logic goes beyond that, a Custom Shipping Rate in Magento 2 is the clean way forward. What matters is that the logic stays server side and modular.

Mironsoft

Magento 2 checkout, shipping and modular shipping logic

Need to model custom shipping costs cleanly in Magento?

We build Magento 2 carrier modules with configurable shipping logic, clean price calculation, checkout integration and maintainable architecture for complex shipping cases.

Carrier

Custom shipping methods with a clean `collectRates()` setup

Rule logic

Weight, zones, product attributes, thresholds and API based prices

Checkout

Magento 2.4.8, PHP 8.4 and clean extension without quick fixes

9. Summary

A Custom Shipping Rate in Magento 2 is the right path once the standard shipping methods no longer cover the business logic cleanly. The core consists of a carrier class, configuration and a clear price calculation via collectRates(). With an additional service layer, the logic stays testable and extendable.

What matters is that shipping rules do not grow as an unordered pile of special cases. A clean module structure, admin configuration and a clear separation between carrier and price logic keep the shipping method maintainable in the long run. That is exactly when a custom shipping method in Magento truly makes sense.

Custom Shipping Rate Magento 2: the essentials at a glance

Carrier

Implement a custom shipping method via `AbstractCarrier`, `CarrierInterface` and `collectRates()`.

Configuration

Do not hardcode title, active status and base price. Make them maintainable via `config.xml` and `system.xml`.

Price logic

Move complex rules into services instead of bundling everything directly inside the carrier class.

Checkout

Test realistically with different carts, countries, weights and special cases.

10. FAQ: Custom Shipping Rate in Magento 2

1 What is a Custom Shipping Rate in Magento 2?
A custom shipping method with individual price and availability logic implemented through a custom carrier.
2 When do you need a custom carrier?
When shipping rules become complex and depend on weight, zones, product data or external APIs.
3 Which method is central?
collectRates() calculates which shipping methods are available and at what price.
4 What does collectRates() return?
A RateResult with one or more methods, or `false` if the shipping option is not available.
5 Does the method need system.xml?
In practice, yes, so that active status, title and prices can be maintained in the admin.
6 Price and cost: what is the difference?
setPrice() is the customer price, setCost() can represent internal costs or margin logic.
7 Should price logic go directly into collectRates()?
Only for very simple cases. Complex rules belong better in a separate service class.
8 When are Table Rates enough?
For simple tabular rules by country, weight or cart value.
9 What is a typical mistake?
Inconsistent carrier IDs, hardcoded prices in the code, and missing tests with real carts.
10 How do you test the method properly?
With different countries, weights, product types, discounts and cart constellations.