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
- Slowness in checkout: a synchronous call to a courier API inside
collectRatesmultiplies 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. - Forgetting
getAllowedMethods: admin order creation and shipment tracking depend on it. - Currency: quote amounts are in base currency; convert explicitly if your rate source quotes in another.
- 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.