Magento 2 Shipping Methods: Building a Custom Carrier

Magento 2 Shipping Methods: Building a Custom Carrier

January 6, 2026 · By Magento Company
Magento 2 Shipping Methods: Building a Custom Carrier

Shipping logic is where Magento customisation meets commercial reality: free-shipping thresholds, per-country rates, courier API quotes, click-and-collect. Magento’s carrier model handles all of these through one clean interface. Here is how to build a custom carrier that behaves properly in checkout, admin and the API.

The Carrier Model

A carrier is a class extending AbstractCarrier and implementing CarrierInterface, with one essential method:

namespace Acme\Shipping\Model\Carrier;

use Magento\Quote\Model\Quote\Address\RateRequest;
use Magento\Shipping\Model\Carrier\AbstractCarrier;
use Magento\Shipping\Model\Carrier\CarrierInterface;
use Magento\Shipping\Model\Rate\ResultFactory;
use Magento\Quote\Model\Quote\Address\RateResult\MethodFactory;

class Regional extends AbstractCarrier implements CarrierInterface
{
    protected $_code = 'acme_regional';

    public function __construct(
        \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
        \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory,
        \Psr\Log\LoggerInterface $logger,
        private ResultFactory $rateResultFactory,
        private MethodFactory $rateMethodFactory,
        array $data = []
    ) {
        parent::__construct($scopeConfig, $rateErrorFactory, $logger, $data);
    }

    public function collectRates(RateRequest $request)
    {
        if (!$this->getConfigFlag('active')) {
            return false;
        }

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

        $method = $this->rateMethodFactory->create();
        $method->setCarrier($this->_code);
        $method->setCarrierTitle($this->getConfigData('title'));
        $method->setMethod('standard');
        $method->setMethodTitle($this->getConfigData('name'));
        $method->setPrice($this->calculatePrice($request));
        $method->setCost($method->getPrice());

        $result->append($method);
        return $result;
    }

    public function getAllowedMethods(): array
    {
        return ['standard' => $this->getConfigData('name')];
    }
}

collectRates receives everything about the request - destination, cart weight, subtotal, items - and returns rate methods. The checkout calls it for every active carrier on every address or cart change, so keep it fast: cache API quotes, and bail early with return false when the carrier cannot serve the destination.

Configuration and Registration

Standard wiring: etc/config.xml for defaults, etc/adminhtml/system.xml for the admin config section (active, title, per-country availability, sort order), and etc/module.xml. Follow the core carriers’ config structure and merchants can manage your carrier without developer help.

The Pitfalls That Bite

  1. Slowness in checkout: a synchronous call to a courier API inside collectRates multiplies by every quote request. Cache by destination+weight hash, degrade gracefully when the API is down, and always offer a fallback rate rather than an empty shipping step.
  2. Forgetting getAllowedMethods: admin order creation and shipment tracking depend on it.
  3. Currency: quote amounts are in base currency; convert explicitly if your rate source quotes in another.
  4. Virtual quotes: carts without shipping address (downloadable-only) skip shipping entirely - do not crash on missing fields.

Beyond Rates

For tracking, implement isTrackingAvailable() and getTrackingInfo(). For labels and collections, you are building on the shipment API - at that point study Magento\Fedex\Model\Carrier as the most complete core example.

A custom carrier is a few hundred lines and one method that matters. Invest your effort in collectRates performance and failure handling - those decide whether your checkout feels solid at 10,000 sessions a day.

Shipping Development Magento 2