Adding a custom shipping method in Magento 2 can be relatively easy depending on the functionality that is needed. In this tutorial, we will show you how to add a simple basic flat rate custom shipping method in Magento 2.

Create a new module

The first step would be to create a new module to handle the new custom shipping method.

Register the new module

We need to register the new module by creating a registration.php file.

app/code/Vendor/ModuleName/registration.php

You will need to replace Vendor/ModuleName with your vendor and module's name. For the sake of this article, we will replace it with MeTutorials/CustomShipping.

app/code/MeTutorials/CustomShipping/registration.php

Next, let's add the following code to the registration.php file.

<?php

use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(
    ComponentRegistrar::MODULE,
    'MeTutorials_CustomShipping',
    __DIR__
);

Create the module's composer.json file

Next, add the following code to our composer.json file located at: app/code/MeTutorials/CustomShipping/composer.json

{
    "name": "me-tutorials/custom-shipping",
    "description": "Custom shipping module",
    "require": {
        "php": "~7.1.3||~7.2.0",
        "magento/framework": "102.0.*",
        "magento/module-backend": "101.0.*",
        "magento/module-catalog": "103.0.*",
        "magento/module-config": "101.1.*",
        "magento/module-directory": "100.3.*",
        "magento/module-quote": "101.1.*",
        "magento/module-sales": "102.0.*",
        "magento/module-sales-rule": "101.1.*",
        "magento/module-shipping": "100.3.*",
        "magento/module-store": "101.0.*"
    },
    "type": "magento2-module",
    "license": [
        "OSL-3.0",
        "AFL-3.0"
    ],
    "autoload": {
        "files": [
            "registration.php"
        ],
        "psr-4": {
            "MeTutorials\\CustomShipping\\": ""
        }
    },
    "version": "1.0.0"
}

Create the module's module.xml file

Next, add the following to the module's module.xml file located here: app/code/Vendor/CustomShipping/etc/module.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="MeTutorials_CustomShipping" >
        <sequence>
            <module name="Magento_Store"/>
            <module name="Magento_Sales"/>
            <module name="Magento_Quote"/>
            <module name="Magento_SalesRule"/>
        </sequence>
    </module>
</config>

Create the module's configuration file

This will allow the admin to control the settings of the custom shipping method from the Magento 2 backend. Let's create the following file and add the following code.

app/code/MeTutorials/CustomShipping/etc/adminhtml/system.xml
<?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" translate="label" type="text" sortOrder="320" showInDefault="1" showInWebsite="1" showInStore="1">
            <group id="customshipping" translate="label" type="text" sortOrder="200" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Custom Shipping</label>
                <field id="active" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="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" canRestore="1">
                    <label>Title</label>
                </field>
                <field id="name" translate="label" type="text" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
                    <label>Method Name</label>
                </field>
                <field id="shipping_cost" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="0" >
                    <label>Shipping Cost</label>
                    <validate>validate-number validate-zero-or-greater</validate>
                </field>
                <field id="sallowspecific" translate="label" type="select" sortOrder="60" showInDefault="1" showInWebsite="1" showInStore="0" canRestore="1">
                    <label>Ship to Applicable Countries</label>
                    <frontend_class>shipping-applicable-country</frontend_class>
                    <source_model>Magento\Shipping\Model\Config\Source\Allspecificcountries</source_model>
                </field>
                <field id="specificcountry" translate="label" type="multiselect" sortOrder="70" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Ship to Specific Countries</label>
                    <source_model>Magento\Directory\Model\Config\Source\Country</source_model>
                    <can_be_empty>1</can_be_empty>
                </field>
                <field id="showmethod" translate="label" type="select" sortOrder="80" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Show Method if Not Applicable</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                    <frontend_class>shipping-skip-hide</frontend_class>
                </field>
                <field id="specificerrmsg" translate="label" type="textarea" sortOrder="90" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
                    <label>Displayed Error Message</label>
                </field>
                <field id="sort_order" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0">
                    <label>Sort Order</label>
                </field>
            </group>
        </section>
    </system>
</config>

Specify default values and declare our Carrier Model

Next, we can specify some default values and tell Magento which carrier model (MeTutorials\CustomShipping\Model\Carrier\CustomShipping) to use for our new custom shipping method. Simply create the following file and add the following code.

app/code/MeTutorials/CustomShipping/etc/config.xml
<?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>
            <customshipping>
                <active>0</active>
                <title>Custom Shipping Title</title>
                <name>Custom Shipping Method Name</name>
                <shipping_cost>10</shipping_cost>
                <sallowspecific>0</sallowspecific>
                <sort_order>15</sort_order>
                <model>MeTutorials\CustomShipping\Model\Carrier\CustomShipping</model>
            </customshipping>
        </carriers>
    </default>
</config>

Create our new Carrier Model

Next, let's create our new carrier model located at app/code/MeTutorials/CustomShipping/Model/Carrier/Customshipping.php and insert the following code.

<?php

namespace MeTutorials\CustomShipping\Model\Carrier;

use Magento\Quote\Model\Quote\Address\RateRequest;
use Magento\Shipping\Model\Carrier\AbstractCarrier;
use Magento\Shipping\Model\Carrier\CarrierInterface;

class CustomShipping extends AbstractCarrier implements CarrierInterface
{
    const SHIPPING_METHOD_CUSTOM_CODE = 'customshipping';

    /**
     * @var string
     */
    protected $_code = self::SHIPPING_METHOD_CUSTOM_CODE;

    /**
     * @var bool
     */
    protected $_isFixed = true;

    /**
     * @var \Magento\Shipping\Model\Rate\ResultFactory
     */
    private $rateResultFactory;

    /**
     * @var \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory
     */
    private $rateMethodFactory;

    /**
     * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
     * @param \Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory $rateErrorFactory
     * @param \Psr\Log\LoggerInterface $logger
     * @param \Magento\Shipping\Model\Rate\ResultFactory $rateResultFactory
     * @param \Magento\Quote\Model\Quote\Address\RateResult\MethodFactory $rateMethodFactory
     * @param array $data
     */
    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,
        array $data = []
    ) {
        parent::__construct($scopeConfig, $rateErrorFactory, $logger, $data);

        $this->rateResultFactory = $rateResultFactory;
        $this->rateMethodFactory = $rateMethodFactory;
    }

    /**
     * Custom Shipping Rates Collector
     *
     * @param RateRequest $request
     * @return \Magento\Shipping\Model\Rate\Result|bool
     */
    public function collectRates(RateRequest $request)
    {
        if (!$this->getConfigFlag('active')) {
            return false;
        }

        /** @var \Magento\Shipping\Model\Rate\Result $result */
        $result = $this->rateResultFactory->create();

        /** @var \Magento\Quote\Model\Quote\Address\RateResult\Method $method */
        $method = $this->rateMethodFactory->create();
        $method->setCarrier($this->_code);
        $method->setCarrierTitle($this->getConfigData('title'));
        $method->setMethod($this->_code);
        $method->setMethodTitle($this->getConfigData('name'));

        $shippingCost = (float)$this->getConfigData('shipping_cost');

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

        $result->append($method);

        return $result;
    }

    /**
     * @return array
     */
    public function getAllowedMethods()
    {
        return [$this->_code => $this->getConfigData('name')];
    }
}

As you can see here, our new carrier model inherits the CarrierInterface interface which allows the getAllowedMethods() method to use all of the available shipping methods. The collectRates() method returns the \Magento\Shipping\Model\Rate\Result object on checkout if it is available. If it is not available, it will return false.

Enable our new module

php bin/magento module:enable MeTutorials_CustomShipping
php bin/magento setup:upgrade

Configure the settings

Finally, navigate to Stores > Configuration > Sales > Shipping Methods. You should see your new Custom Shipping method and will be able to configure it here as well.

 

In Conclusion

As you can see from the above example, adding a new custom shipping method to Magento 2 can be fairly straightforward. The above example can also be extended upon even further for more functionality depending on your needs.

If this article has helped you, please let us know below. We love to hear from all our readers! Feel free to download our free Magento 2 Custom Shipping Method extension. To learn more about custom shipping methods feel free to take a look at Magento's documentation as well which can be seen here.