Skip to content

Shipping Types

Shipping types calculate shipping costs during checkout. Meloncart ships with a Table Rate shipping type and several carrier integrations (Australia Post, Canada Post, FedEx, DHL Express, UPS), but you can build your own to integrate with additional carrier APIs or implement custom pricing logic. This guide covers everything you need to create a shipping type from scratch.

How It Works

Shipping types follow the driver pattern. A ShippingMethod model stores the configuration (name, handling fee, country restrictions, etc.), and a shipping type class provides the behavior — specifically, the getQuote() method that calculates the shipping cost.

When a customer enters their shipping address during checkout:

  1. Meloncart queries all enabled shipping methods that match the destination and cart weight
  2. Each method calls its shipping type's getQuote() method
  3. The method adds handling fees and calculates taxes
  4. Available options are presented to the customer

Directory Structure

plugins/acme/shipping/
├── Plugin.php
└── shippingtypes/
    ├── MyShipping.php           ← shipping type class
    └── myshipping/
        ├── fields.yaml          ← configuration fields
        └── _setup_help.php      ← optional

The shipping type class lives directly inside shippingtypes/, and its configuration directory is a lowercase version of the class name alongside it. The fields.yaml defines backend configuration fields, and _setup_help.php is an optional partial displayed in a Help tab.

Creating a Shipping Type

Extend Meloncart\Shop\Classes\ShippingTypeBase and implement two required pieces: driverDetails() and getQuote().

driverDetails

Returns metadata about your shipping type:

php
public function driverDetails()
{
    return [
        'name' => 'My Shipping',
        'description' => 'Calculate shipping via My Carrier API.'
    ];
}

getQuote

The core method. Receives shipping destination and cart details, returns a price or null if not available.

php
public function getQuote(array $options)
{
    // Return price in cents, or null if not available
}

Quote Options

The getQuote() method receives an array describing the destination and cart contents:

OptionTypeDescription
countryIdint|nullRainLab\Location\Models\Country ID
countryCodestring|nullTwo-letter country code (e.g., US)
stateIdint|nullRainLab\Location\Models\State ID
stateCodestring|nullState/province code (e.g., CA)
zipstring|nullPostal/ZIP code
citystring|nullCity name
totalPriceintCart subtotal in base currency units (cents)
totalVolumefloatTotal volume of items
totalWeightfloatTotal weight in the store's configured weight unit
totalItemsintNumber of items in the cart
orderItemsarrayArray of cart item objects (with product and quantity)
isBusinessboolWhether the address is a business

Quote Return Format

The getQuote() method supports three return formats:

Single Price

Return a numeric value for a single shipping rate. Prices are always in base currency units (e.g., cents for USD — 500 = $5.00).

php
// $5.00 flat rate
return 500;

Multiple Options

Return an array to offer child options (e.g., Standard vs Express). Each option needs an id and a quote in base currency units:

php
return [
    'Standard (5-7 days)' => ['id' => 'standard', 'quote' => 500],
    'Express (2-3 days)' => ['id' => 'express', 'quote' => 1500],
    'Overnight' => ['id' => 'overnight', 'quote' => 3500]
];

Child options appear as sub-choices under the shipping method in the checkout UI. The id must be unique within the method.

Not Available

Return null when shipping is not available for the given destination:

php
if (!$countryId) {
    return null;
}

Minimal Example: Flat Rate

php
<?php namespace Acme\Shipping\ShippingTypes;

use Meloncart\Shop\Classes\ShippingTypeBase;

class FlatRate extends ShippingTypeBase
{
    public $driverFields = 'fields.yaml';

    public function driverDetails()
    {
        return [
            'name' => 'Flat Rate',
            'description' => 'Charge a flat shipping rate for all orders.'
        ];
    }

    public function getQuote(array $options)
    {
        extract(array_merge([
            'countryId' => null,
        ], $options));

        if (!$countryId) {
            return null;
        }

        return $this->getHostObject()->flat_rate;
    }
}

With fields.yaml:

yaml
fields:
    flat_rate:
        label: Flat Rate Amount
        comment: Shipping rate in dollars (e.g., 5.00)
        type: currency
        span: auto

Full Example: Weight-Based Zones

This example shows a more realistic shipping type that calculates rates based on weight and destination zone:

php
<?php namespace Acme\Shipping\ShippingTypes;

use Meloncart\Shop\Classes\ShippingTypeBase;
use RainLab\Location\Models\Country;

class WeightZone extends ShippingTypeBase
{
    public $driverFields = 'fields.yaml';

    public function driverDetails()
    {
        return [
            'name' => 'Weight Zone Shipping',
            'description' => 'Calculate shipping based on weight and destination zone.'
        ];
    }

    public function getQuote(array $options)
    {
        extract(array_merge([
            'countryId' => null,
            'totalWeight' => 0,
        ], $options));

        if (!$countryId) {
            return null;
        }

        $host = $this->getHostObject();
        $country = Country::findByKey($countryId);
        if (!$country) {
            return null;
        }

        // Determine the shipping zone
        $zone = $this->getZoneForCountry($country->code);
        if (!$zone) {
            return null;
        }

        // Calculate rate: base + per-kg rate
        $baseRate = $zone['base_rate'];
        $perKgRate = $zone['per_kg_rate'];
        $rate = $baseRate + ($totalWeight * $perKgRate);

        // Offer standard and express
        return [
            'Standard' => [
                'id' => 'standard',
                'quote' => (int) round($rate)
            ],
            'Express' => [
                'id' => 'express',
                'quote' => (int) round($rate * $host->express_multiplier)
            ]
        ];
    }

    protected function getZoneForCountry($countryCode)
    {
        $zones = $this->getHostObject()->zones ?: [];

        foreach ($zones as $zone) {
            $countries = array_map('trim', explode(',', $zone['countries'] ?? ''));
            if (in_array($countryCode, $countries) || in_array('*', $countries)) {
                return $zone;
            }
        }

        return null;
    }
}

Full Example: Carrier API Integration

This example shows how to integrate with an external shipping API. It uses ShippingSetting to read the shipping origin and unit configuration, and the Http facade with October's callback pattern for API calls.

php
<?php namespace Acme\Shipping\ShippingTypes;

use Http;
use Log;
use Meloncart\Shop\Classes\ShippingTypeBase;
use Meloncart\Shop\Models\ShippingSetting;
use RainLab\Location\Models\Country;
use RainLab\Location\Models\State;

class CarrierApi extends ShippingTypeBase
{
    public $driverFields = 'fields.yaml';

    public function driverDetails()
    {
        return [
            'name' => 'Carrier API',
            'description' => 'Real-time shipping rates from carrier API.'
        ];
    }

    public function getQuote(array $options)
    {
        extract(array_merge([
            'countryId' => null,
            'stateId' => null,
            'zip' => null,
            'totalWeight' => 0,
        ], $options));

        if (!$countryId || !$zip) {
            return null;
        }

        $host = $this->getHostObject();
        if (!$host->api_token) {
            return null;
        }

        $country = Country::findByKey($countryId);
        $state = State::findByKey($stateId);

        // Read origin address and units from Shipping Settings
        $settings = ShippingSetting::instance();
        $originCountry = Country::findByKey($settings->country_id);

        try {
            $response = Http::post(
                'https://api.carrier.com/v1/rates',
                function($http) use ($host, $settings, $originCountry, $country, $state, $zip, $totalWeight) {
                    $http->header('Content-Type', 'application/json');
                    $http->header('Authorization', 'Bearer ' . $host->api_token);
                    $http->setOption(CURLOPT_POSTFIELDS, json_encode([
                        'origin_zip' => $settings->zip,
                        'origin_country' => $originCountry?->code,
                        'dest_country' => $country?->code,
                        'dest_state' => $state?->code,
                        'dest_zip' => $zip,
                        'weight' => $totalWeight,
                        'weight_unit' => $settings->weight_unit,
                    ]));
                }
            );

            if ($response->code !== 200) {
                Log::warning('Carrier API error: HTTP ' . $response->code);
                return null;
            }

            // Build child options from API response
            $data = json_decode($response->body, true);
            $result = [];

            foreach ($data['rates'] ?? [] as $rate) {
                $result[$rate['service_name']] = [
                    'id' => $rate['service_code'],
                    'quote' => (int) round($rate['total_price'] * 100),
                ];
            }

            return $result ?: null;
        }
        catch (\Exception $ex) {
            Log::warning('Carrier API exception: ' . $ex->getMessage());
            return null;
        }
    }
}

Registration

Register your shipping type in your plugin's Plugin.php:

php
public function registerShippingTypes()
{
    return [
        \Acme\Shipping\ShippingTypes\FlatRate::class => 'flat-rate',
        \Acme\Shipping\ShippingTypes\WeightZone::class => 'weight-zone',
    ];
}

The array key is the shipping type class, and the value is a unique alias used internally.

Configuration Fields

The fields.yaml file defines backend form fields. These values are stored in the config_data JSON column on the ShippingMethod model and accessible as properties via getHostObject().

yaml
fields:
    origin_zip:
        label: Origin ZIP Code
        type: text
        tab: Configuration

    api_token:
        label: API Token
        type: sensitive
        tab: Configuration

Access configuration values in your shipping type:

php
$host = $this->getHostObject();
$originZip = $host->origin_zip;
$apiToken = $host->api_token;

Dynamic Dropdown Options

For datatable fields (like the Table Rate's rate table), you can provide dynamic dropdown options by implementing getDataTableOptions():

php
public function getDataTableOptions($attribute, $field, $data)
{
    if ($field === 'country') {
        return Country::applyEnabled()->lists('name', 'code');
    }

    if ($field === 'state') {
        return State::whereHas('country', function ($q) use ($data) {
            $q->where('code', $data['country'] ?? '');
        })->lists('name', 'code');
    }

    return [];
}

Shipping Settings

Carrier-based shipping types need the shipping origin address and units of measurement configured in Settings → Shipping & Measurements. Access these values through the ShippingSetting singleton:

php
use Meloncart\Shop\Models\ShippingSetting;
use RainLab\Location\Models\Country;
use RainLab\Location\Models\State;

$settings = ShippingSetting::instance();

// Origin address
$originCountry = Country::findByKey($settings->country_id);
$originState = State::findByKey($settings->state_id);
$originCity = $settings->city;
$originZip = $settings->zip;
$originAddress = $settings->address_line1;

// Units of measurement
$weightUnit = $settings->weight_unit;       // 'lb' or 'kg'
$dimensionUnit = $settings->dimension_unit; // 'in' or 'cm'

// Sender details (for shipping labels)
$senderName = $settings->sender_first_name . ' ' . $settings->sender_last_name;
$senderCompany = $settings->sender_company;
$senderPhone = $settings->sender_phone;

The totalWeight passed to getQuote() is always in the store's configured weight unit. If a carrier API requires a specific unit (e.g., Australia Post requires kilograms), convert from the store's unit:

php
$weight = $totalWeight;
if ($settings->weight_unit === 'lb') {
    $weight = $totalWeight * 0.453592; // Convert to kg
}

Shipping Method Model

Your shipping type is attached to a ShippingMethod model that provides these built-in features without any code in your driver:

FeatureDescription
Handling feeFixed amount added to every quote automatically
Weight limitsMin/max weight filters — methods outside range are excluded
Country restrictionsLimit to specific countries (methods with no countries apply to all)
User group restrictionsLimit to specific user groups
Taxable shippingWhether tax is calculated on the shipping cost
Quote cachingQuotes are cached per-request using an option hash

These features are configured in the backend form and applied by the ShippingMethod model before returning quotes to the checkout.

How Quotes Are Processed

When ShippingMethod::listApplicable() runs during checkout:

  1. Queries enabled methods matching weight, country, and user group filters
  2. Calls your getQuote() method
  3. Adds the handling fee to the returned quote
  4. Adds per-product shipping costs (from product extras)
  5. Calculates shipping taxes if the method is taxable
  6. Returns methods with quote, quoteOriginal, and quoteFinal properties set

You don't need to handle handling fees, taxes, or per-product costs in your getQuote() method — those are applied automatically.

Carrier Shipping Labels

Shipping types can optionally support carrier label generation — producing labels with barcodes, postage, and tracking numbers directly from the carrier's API. This is separate from the template-based shipping labels that merchants design themselves.

Enabling Label Support

Override supportsShippingLabels() to indicate your shipping type supports carrier labels:

php
public function supportsShippingLabels(): bool
{
    return true;
}

Generating Labels

Implement generateShippingLabels() to call the carrier API and return label data. Returns null if labels cannot be generated, or an array of label results:

php
use Meloncart\Shop\Models\Order;

public function generateShippingLabels(Order $order, array $options = [])
{
    // Call carrier API to generate label
    $response = $this->callCarrierLabelApi($order, $options);

    if (!$response) {
        return null;
    }

    return [
        [
            'tracking_number' => $response['tracking'],
            'label_content' => base64_encode($response['label_data']),
            'label_format' => 'pdf',
        ],
    ];
}

Each item in the returned array represents one label with:

KeyTypeDescription
tracking_numberstringCarrier tracking number
label_contentstringBase64-encoded label data
label_formatstringFormat: pdf, png, gif, or zpl

Label Form Fields

Some carriers require additional parameters when generating labels (e.g., container type, insurance options, image format). Override buildLabelFormFields() to define a form that collects these parameters:

php
public function buildLabelFormFields(Order $order): array
{
    return [
        'image_type' => [
            'label' => 'Label Format',
            'type' => 'dropdown',
            'options' => ['pdf' => 'PDF', 'png' => 'PNG'],
            'default' => 'pdf',
        ],
        'container' => [
            'label' => 'Container Type',
            'type' => 'dropdown',
            'options' => [
                'VARIABLE' => 'Variable',
                'FLAT_RATE_BOX' => 'Flat Rate Box',
                'FLAT_RATE_ENVELOPE' => 'Flat Rate Envelope',
            ],
        ],
    ];
}

The field definitions follow the standard form field format. The collected values are passed as the $options array to generateShippingLabels().

Default Label Parameters

Override getDefaultLabelParameters() to pre-populate label form fields with sensible defaults:

php
public function getDefaultLabelParameters(Order $order): array
{
    return [
        'image_type' => 'pdf',
        'container' => 'VARIABLE',
    ];
}

Setup Help Partial

Create a _setup_help.php file in your shipping type's config directory to display setup instructions in a Help tab on the backend form:

php
<!-- shippingtypes/carriershipping/_setup_help.php -->
<div class="callout fade in callout-info no-subheader">
    <div class="header">
        <i class="icon-info"></i>
        <h3>Getting Started</h3>
    </div>
    <div class="content">
        <ol>
            <li>Sign up for an API account at carrier.com</li>
            <li>Copy your API token from the dashboard</li>
            <li>Enter your origin ZIP code and API token in the Configuration tab</li>
        </ol>
    </div>
</div>

Lifecycle Hooks

MethodWhen Called
initDriverHost($host)When the driver is first attached to a ShippingMethod model. Set default values.
validateDriverHost($host)Before the shipping method is saved. Throw ValidationException for invalid config.
php
public function initDriverHost($host)
{
    if (!$host->exists) {
        $host->name = 'My Shipping';
    }
}

public function validateDriverHost($host)
{
    if ($host->max_weight && $host->min_weight && $host->min_weight > $host->max_weight) {
        throw new \ValidationException([
            'max_weight' => 'Max weight must be greater than min weight.'
        ]);
    }
}

Reference

ShippingTypeBase Methods

MethodReturnsDescription
driverDetails()arrayShipping type metadata (name, description)
getQuote(array $options)int|array|nullCalculate shipping cost
getHostObject()ShippingMethodAccess the shipping method model and config
getPartialPath()stringPath to the config directory
getDataTableOptions($attr, $field, $data)arrayDynamic options for datatable dropdowns
supportsShippingLabels()boolWhether carrier label generation is supported
generateShippingLabels($order, $options)array|nullGenerate carrier labels for an order
buildLabelFormFields($order)arrayForm field definitions for label generation parameters
getDefaultLabelParameters($order)arrayDefault values for label generation parameters
initDriverHost($host)voidInitialize driver on model
validateDriverHost($host)voidValidate config before save

Built-in Shipping Types

TypeAliasDescription
TableRateShippingtable-rateConfigurable rate table with location, weight, volume, subtotal, and item count matching
AustraliaPostShippingaustralia-postAustralia Post PAC API for domestic and international parcel rates. Supports carrier labels via the Shipping & Tracking API (eParcel contract required).
CanadaPostShippingcanada-postCanada Post Rating API for domestic, US, and international rates
FedExShippingfedexFedEx REST API with OAuth 2.0 for domestic and international rates
DhlExpressShippingdhl-expressDHL Express MyDHL API for international express rates
UpsShippingupsUPS Rating REST API with OAuth 2.0, supports negotiated rates
UspsShippinguspsUSPS REST API v3 with OAuth 2.0 for domestic and international rates. Supports carrier labels with Enterprise Payment Account.