A custom shipping carrier in Magento 2 goes beyond just returning rates – full implementations include package tracking, label generation, and webhook handling for status updates. I show the complete implementation from collectRates through tracking requests to PDF label generation, using a fictional carrier API to demonstrate all the integration points.
Module structure
Vendor/Carrier/
etc/
config.xml
adminhtml/system.xml
module.xml
Model/
Carrier/
VendorCarrier.php - main carrier class
Api/
CarrierClient.php - HTTP client for the carrier API
Tracking/
Result.php
view/adminhtml/
templates/order/
tracking.phtml - custom tracking widget
Core carrier class – collectRates()
<?php
declare(strict_types=1);
namespace Vendor\Carrier\Model\Carrier;
use Magento\Shipping\Model\Carrier\AbstractCarrier;
use Magento\Shipping\Model\Carrier\CarrierInterface;
use Magento\Quote\Model\Quote\Address\RateRequest;
class VendorCarrier extends AbstractCarrier implements CarrierInterface
{
protected $_code = 'vendor_carrier';
protected $_isFixed = false;
protected $_canShipToAll = true;
public function __construct(
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
\Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory,
\Psr\Log\LoggerInterface $logger,
\Magento\Shipping\Model\Rate\ResultFactory $rateResultFactory,
\Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory,
private \Vendor\Carrier\Model\Api\CarrierClient $apiClient,
array $data = []
) {
parent::__construct($scopeConfig, $rateErrorFactory, $logger, $data);
$this->_rateResultFactory = $rateResultFactory;
$this->_rateMethodFactory = $rateMethodFactory;
}
public function collectRates(RateRequest $request): \Magento\Shipping\Model\Rate\Result|bool
{
if (!$this->getConfigFlag('active')) {
return false;
}
try {
$apiRates = $this->apiClient->getRates([
'from_postcode' => $request->getPostcode(),
'to_postcode' => $request->getDestPostcode(),
'weight_kg' => $request->getPackageWeight(),
'declared_value' => $request->getPackageValue(),
]);
} catch (\Exception $e) {
$this->_logger->error('Carrier API error: ' . $e->getMessage());
return false;
}
$result = $this->_rateResultFactory->create();
foreach ($apiRates as $apiRate) {
$method = $this->_rateMethodFactory->create();
$method->setCarrier($this->_code);
$method->setCarrierTitle($this->getConfigData('title'));
$method->setMethod($apiRate['service_code']);
$method->setMethodTitle($apiRate['service_name']);
$method->setPrice($apiRate['price']);
$method->setCost($apiRate['price']);
$result->append($method);
}
return $result;
}
public function getAllowedMethods(): array
{
return [
'standard' => 'Standard Delivery',
'express' => 'Express Delivery',
'economy' => 'Economy Delivery',
];
}
// Enable tracking
public function isTrackingAvailable(): bool { return true; }
// Enable label generation
public function isShippingLabelsAvailable(): bool { return true; }
}
Tracking implementation
<?php
namespace Vendor\Carrier\Model\Carrier;
class VendorCarrier extends AbstractCarrier implements CarrierInterface
{
// ... (previous code)
public function getTrackingInfo(string $tracking): \Magento\Shipping\Model\Tracking\Result
{
$result = $this->_trackFactory->create();
try {
$apiTracking = $this->apiClient->getTracking($tracking);
$status = $this->_trackStatusFactory->create();
$status->setCarrier($this->_code);
$status->setCarrierTitle($this->getConfigData('title'));
$status->setTracking($tracking);
$status->setStatus($apiTracking['current_status']);
$status->setDeliverydate($apiTracking['estimated_delivery'] ?? null);
// Add tracking history
$progressDetail = [];
foreach ($apiTracking['events'] as $event) {
$progressDetail[] = [
'deliverydate' => $event['date'],
'deliverytime' => $event['time'],
'deliverylocation' => $event['location'],
'activity' => $event['description'],
];
}
$status->setProgressdetail($progressDetail);
$result->append($status);
} catch (\Exception $e) {
$error = $this->_trackErrorFactory->create();
$error->setCarrier($this->_code);
$error->setCarrierTitle($this->getConfigData('title'));
$error->setTracking($tracking);
$error->setErrorMessage($e->getMessage());
$result->append($error);
}
return $result;
}
}
Shipping label generation
<?php
namespace Vendor\Carrier\Model\Carrier;
class VendorCarrier extends AbstractCarrier implements CarrierInterface
{
public function requestToShipment(\Magento\Shipping\Model\Shipment\Request $request): \Magento\Framework\DataObject
{
$result = new \Magento\Framework\DataObject();
try {
$order = $request->getOrderShipment()->getOrder();
$address = $order->getShippingAddress();
$apiResponse = $this->apiClient->createShipment([
'recipient' => [
'name' => $address->getFirstname() . ' ' . $address->getLastname(),
'company' => $address->getCompany() ?? '',
'street' => implode(', ', $address->getStreet()),
'city' => $address->getCity(),
'postcode' => $address->getPostcode(),
'country' => $address->getCountryId(),
'phone' => $address->getTelephone(),
],
'packages' => [[
'weight' => $request->getPackageWeight(),
'dimensions' => [
'length' => $request->getPackageParams()->getLength() ?? 30,
'width' => $request->getPackageParams()->getWidth() ?? 20,
'height' => $request->getPackageParams()->getHeight() ?? 15,
],
'declared_value' => $request->getPackageParams()->getInsuredValue() ?? 0,
]],
'service_code' => $request->getShippingMethod(),
'reference' => $order->getIncrementId(),
]);
// Store tracking number
$result->setData([
'tracking_number' => $apiResponse['tracking_number'],
'label_content' => base64_decode($apiResponse['label_base64']), // PDF bytes
'label_format' => 'PDF',
]);
} catch (\Exception $e) {
$result->setData(['error' => true, 'message' => $e->getMessage()]);
}
return $result;
}
}
config.xml – carrier defaults
<config>
<default>
<carriers>
<vendor_carrier>
<active>0</active>
<model>Vendor\Carrier\Model\Carrier\VendorCarrier</model>
<title>VendorCarrier</title>
<name>Standard Delivery</name>
<sallowspecific>0</sallowspecific>
<sort_order>10</sort_order>
<api_url>https://api.vendorcarrier.com/v1</api_url>
</vendor_carrier>
</carriers>
</default>
</config>
Summary
A complete custom shipping carrier in Magento 2 has three main integration points: collectRates (called during checkout to get prices), getTrackingInfo (called from order detail to show tracking), and requestToShipment (called from the shipment create page to generate a label). Each calls the carrier’s external API and maps the response to Magento’s internal objects. The pattern is consistent – once you build one carrier, the second one takes a fraction of the time.
