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:
- Meloncart queries all enabled shipping methods that match the destination and cart weight
- Each method calls its shipping type's
getQuote()method - The method adds handling fees and calculates taxes
- 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 ← optionalThe 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:
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.
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:
| Option | Type | Description |
|---|---|---|
countryId | int|null | RainLab\Location\Models\Country ID |
countryCode | string|null | Two-letter country code (e.g., US) |
stateId | int|null | RainLab\Location\Models\State ID |
stateCode | string|null | State/province code (e.g., CA) |
zip | string|null | Postal/ZIP code |
city | string|null | City name |
totalPrice | int | Cart subtotal in base currency units (cents) |
totalVolume | float | Total volume of items |
totalWeight | float | Total weight in the store's configured weight unit |
totalItems | int | Number of items in the cart |
orderItems | array | Array of cart item objects (with product and quantity) |
isBusiness | bool | Whether 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).
// $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:
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:
if (!$countryId) {
return null;
}Minimal Example: Flat Rate
<?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:
fields:
flat_rate:
label: Flat Rate Amount
comment: Shipping rate in dollars (e.g., 5.00)
type: currency
span: autoFull Example: Weight-Based Zones
This example shows a more realistic shipping type that calculates rates based on weight and destination zone:
<?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 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:
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().
fields:
origin_zip:
label: Origin ZIP Code
type: text
tab: Configuration
api_token:
label: API Token
type: sensitive
tab: ConfigurationAccess configuration values in your shipping type:
$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():
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:
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:
$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:
| Feature | Description |
|---|---|
| Handling fee | Fixed amount added to every quote automatically |
| Weight limits | Min/max weight filters — methods outside range are excluded |
| Country restrictions | Limit to specific countries (methods with no countries apply to all) |
| User group restrictions | Limit to specific user groups |
| Taxable shipping | Whether tax is calculated on the shipping cost |
| Quote caching | Quotes 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:
- Queries enabled methods matching weight, country, and user group filters
- Calls your
getQuote()method - Adds the handling fee to the returned quote
- Adds per-product shipping costs (from product extras)
- Calculates shipping taxes if the method is taxable
- Returns methods with
quote,quoteOriginal, andquoteFinalproperties 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:
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:
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:
| Key | Type | Description |
|---|---|---|
tracking_number | string | Carrier tracking number |
label_content | string | Base64-encoded label data |
label_format | string | Format: 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:
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:
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:
<!-- 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
| Method | When 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. |
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
| Method | Returns | Description |
|---|---|---|
driverDetails() | array | Shipping type metadata (name, description) |
getQuote(array $options) | int|array|null | Calculate shipping cost |
getHostObject() | ShippingMethod | Access the shipping method model and config |
getPartialPath() | string | Path to the config directory |
getDataTableOptions($attr, $field, $data) | array | Dynamic options for datatable dropdowns |
supportsShippingLabels() | bool | Whether carrier label generation is supported |
generateShippingLabels($order, $options) | array|null | Generate carrier labels for an order |
buildLabelFormFields($order) | array | Form field definitions for label generation parameters |
getDefaultLabelParameters($order) | array | Default values for label generation parameters |
initDriverHost($host) | void | Initialize driver on model |
validateDriverHost($host) | void | Validate config before save |
Built-in Shipping Types
| Type | Alias | Description |
|---|---|---|
TableRateShipping | table-rate | Configurable rate table with location, weight, volume, subtotal, and item count matching |
AustraliaPostShipping | australia-post | Australia Post PAC API for domestic and international parcel rates. Supports carrier labels via the Shipping & Tracking API (eParcel contract required). |
CanadaPostShipping | canada-post | Canada Post Rating API for domestic, US, and international rates |
FedExShipping | fedex | FedEx REST API with OAuth 2.0 for domestic and international rates |
DhlExpressShipping | dhl-express | DHL Express MyDHL API for international express rates |
UpsShipping | ups | UPS Rating REST API with OAuth 2.0, supports negotiated rates |
UspsShipping | usps | USPS REST API v3 with OAuth 2.0 for domestic and international rates. Supports carrier labels with Enterprise Payment Account. |