Skip to main content

Shipping Method API

The WooCommerce Shipping Method API enables you to create custom shipping methods that integrate with shipping zones and provide dynamic rate calculations for your store.

Overview

Shipping methods in WooCommerce extend the WC_Shipping_Method class and can provide rates based on:
  • Cart contents (weight, dimensions, value)
  • Customer location (shipping zones)
  • Product shipping classes
  • Custom business logic
Since WooCommerce 2.6, shipping methods use shipping zones and instances, allowing merchants to configure different settings for the same shipping method in different regions.

Creating a Shipping Method Plugin

Step 1: Plugin Setup

Create your plugin file:
<?php
/**
 * Plugin Name: Custom Shipping Method
 * Description: A custom shipping method for WooCommerce
 * Version: 1.0.0
 * Author: Your Name
 * Text Domain: custom-shipping-method
 */

defined( 'ABSPATH' ) || exit;

// Check if WooCommerce is active
if ( ! in_array( 'woocommerce/woocommerce.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ), true ) ) {
    return;
}

/**
 * Initialize the shipping method.
 */
function init_custom_shipping_method() {
    if ( ! class_exists( 'WC_Shipping_Method' ) ) {
        return;
    }
    
    require_once plugin_dir_path( __FILE__ ) . 'class-wc-shipping-custom.php';
}
add_action( 'woocommerce_shipping_init', 'init_custom_shipping_method' );

/**
 * Add the shipping method to WooCommerce.
 *
 * @param array $methods Existing shipping methods.
 * @return array
 */
function add_custom_shipping_method( $methods ) {
    $methods['custom_shipping'] = 'WC_Shipping_Custom';
    return $methods;
}
add_filter( 'woocommerce_shipping_methods', 'add_custom_shipping_method' );

Step 2: Create the Shipping Method Class

Create class-wc-shipping-custom.php:
<?php

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Custom Shipping Method.
 *
 * @extends WC_Shipping_Method
 */
class WC_Shipping_Custom extends WC_Shipping_Method {
    
    /**
     * Constructor.
     *
     * @param int $instance_id Shipping zone instance ID.
     */
    public function __construct( $instance_id = 0 ) {
        $this->id                 = 'custom_shipping';
        $this->instance_id        = absint( $instance_id );
        $this->method_title       = __( 'Custom Shipping', 'custom-shipping-method' );
        $this->method_description = __( 'Custom shipping method with advanced options', 'custom-shipping-method' );
        
        // Supported features
        $this->supports = array(
            'shipping-zones',
            'instance-settings',
            'instance-settings-modal',
        );
        
        // Initialize
        $this->init();
    }
    
    /**
     * Initialize shipping method.
     */
    public function init() {
        // Load the settings
        $this->init_form_fields();
        $this->init_settings();
        
        // Get settings
        $this->title      = $this->get_option( 'title' );
        $this->tax_status = $this->get_option( 'tax_status' );
        $this->cost       = $this->get_option( 'cost' );
        
        // Save settings
        add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
    }
    
    /**
     * Define settings form fields.
     */
    public function init_form_fields() {
        $this->instance_form_fields = array(
            'title' => array(
                'title'       => __( 'Method Title', 'custom-shipping-method' ),
                'type'        => 'text',
                'description' => __( 'This controls the title customers see during checkout.', 'custom-shipping-method' ),
                'default'     => __( 'Custom Shipping', 'custom-shipping-method' ),
                'desc_tip'    => true,
            ),
            'tax_status' => array(
                'title'   => __( 'Tax Status', 'custom-shipping-method' ),
                'type'    => 'select',
                'class'   => 'wc-enhanced-select',
                'default' => 'taxable',
                'options' => array(
                    'taxable' => __( 'Taxable', 'custom-shipping-method' ),
                    'none'    => __( 'None', 'custom-shipping-method' ),
                ),
            ),
            'cost' => array(
                'title'       => __( 'Cost', 'custom-shipping-method' ),
                'type'        => 'text',
                'placeholder' => '0',
                'description' => __( 'Enter a cost (excluding tax) or leave blank to disable.', 'custom-shipping-method' ),
                'default'     => '0',
                'desc_tip'    => true,
            ),
        );
    }
    
    /**
     * Calculate shipping rates.
     *
     * @param array $package Package information.
     */
    public function calculate_shipping( $package = array() ) {
        $rate = array(
            'id'      => $this->get_rate_id(),
            'label'   => $this->title,
            'cost'    => $this->cost,
            'package' => $package,
        );
        
        $this->add_rate( $rate );
    }
}

Shipping Method Properties

id
string
required
Unique identifier for your shipping method
instance_id
int
Instance ID when used within a shipping zone
method_title
string
required
Title shown in the admin shipping settings
method_description
string
Description shown in the admin
supports
array
Features supported by the method:
  • shipping-zones - Works with shipping zones
  • instance-settings - Has per-instance settings
  • instance-settings-modal - Settings can be edited in a modal
  • settings - Has global settings page
enabled
string
default:"yes"
Whether the method is enabled
title
string
Title displayed to customers at checkout
tax_status
string
default:"taxable"
Tax status: taxable or none

Calculating Shipping Rates

The calculate_shipping() method is called when WooCommerce needs shipping rates for a cart.

Basic Rate Calculation

public function calculate_shipping( $package = array() ) {
    $rate = array(
        'id'       => $this->get_rate_id(),
        'label'    => $this->title,
        'cost'     => $this->cost,
        'calc_tax' => 'per_order',
        'package'  => $package,
    );
    
    $this->add_rate( $rate );
}

Weight-Based Calculation

public function calculate_shipping( $package = array() ) {
    $weight = 0;
    
    // Calculate total weight
    foreach ( $package['contents'] as $item_id => $values ) {
        $weight += $values['data']->get_weight() * $values['quantity'];
    }
    
    // Calculate cost based on weight
    $cost = 0;
    if ( $weight > 0 ) {
        $cost = 5 + ( $weight * 0.5 ); // Base cost + per kg rate
    }
    
    $rate = array(
        'id'      => $this->get_rate_id(),
        'label'   => $this->title,
        'cost'    => $cost,
        'package' => $package,
    );
    
    $this->add_rate( $rate );
}

Cart Value-Based Calculation

public function calculate_shipping( $package = array() ) {
    $cart_total = 0;
    
    // Calculate cart total
    foreach ( $package['contents'] as $item_id => $values ) {
        $cart_total += $values['line_total'];
    }
    
    // Free shipping over threshold
    $free_shipping_threshold = 100;
    $cost = $cart_total >= $free_shipping_threshold ? 0 : 10;
    
    $rate = array(
        'id'      => $this->get_rate_id(),
        'label'   => $this->title,
        'cost'    => $cost,
        'package' => $package,
    );
    
    $this->add_rate( $rate );
}

Working with Shipping Classes

Shipping classes allow different products to have different shipping costs.

Adding Shipping Class Support

public function init_form_fields() {
    $this->instance_form_fields = array(
        'title' => array(
            'title'   => __( 'Method Title', 'custom-shipping-method' ),
            'type'    => 'text',
            'default' => __( 'Custom Shipping', 'custom-shipping-method' ),
        ),
        'cost' => array(
            'title'       => __( 'Base Cost', 'custom-shipping-method' ),
            'type'        => 'text',
            'placeholder' => '0',
            'default'     => '0',
        ),
        'type' => array(
            'title'   => __( 'Calculation Type', 'custom-shipping-method' ),
            'type'    => 'select',
            'class'   => 'wc-enhanced-select',
            'default' => 'class',
            'options' => array(
                'class' => __( 'Per Class: Charge shipping for each class individually', 'custom-shipping-method' ),
                'order' => __( 'Per Order: Charge shipping for the most expensive class', 'custom-shipping-method' ),
            ),
        ),
    );
    
    // Add fields for each shipping class
    $shipping_classes = WC()->shipping()->get_shipping_classes();
    
    if ( ! empty( $shipping_classes ) ) {
        foreach ( $shipping_classes as $shipping_class ) {
            if ( ! isset( $shipping_class->term_id ) ) {
                continue;
            }
            
            $this->instance_form_fields[ 'class_cost_' . $shipping_class->term_id ] = array(
                'title'       => sprintf( __( '"%s" Shipping Class Cost', 'custom-shipping-method' ), esc_html( $shipping_class->name ) ),
                'type'        => 'text',
                'placeholder' => __( 'N/A', 'custom-shipping-method' ),
                'description' => __( 'Enter a cost (excluding tax).', 'custom-shipping-method' ),
                'default'     => '',
                'desc_tip'    => true,
            );
        }
        
        $this->instance_form_fields['no_class_cost'] = array(
            'title'       => __( 'No Shipping Class Cost', 'custom-shipping-method' ),
            'type'        => 'text',
            'placeholder' => __( 'N/A', 'custom-shipping-method' ),
            'description' => __( 'Cost for products without a shipping class.', 'custom-shipping-method' ),
            'default'     => '',
            'desc_tip'    => true,
        );
    }
}

Calculate with Shipping Classes

public function calculate_shipping( $package = array() ) {
    $rate = array(
        'id'      => $this->get_rate_id(),
        'label'   => $this->title,
        'cost'    => $this->get_option( 'cost' ),
        'package' => $package,
    );
    
    // Get shipping classes in the package
    $found_shipping_classes = $this->find_shipping_classes( $package );
    $highest_class_cost = 0;
    
    foreach ( $found_shipping_classes as $shipping_class => $products ) {
        $shipping_class_term = get_term_by( 'slug', $shipping_class, 'product_shipping_class' );
        $class_cost = $shipping_class_term && $shipping_class_term->term_id
            ? $this->get_option( 'class_cost_' . $shipping_class_term->term_id, '' )
            : $this->get_option( 'no_class_cost', '' );
        
        if ( '' === $class_cost ) {
            continue;
        }
        
        if ( 'class' === $this->get_option( 'type' ) ) {
            $rate['cost'] += $class_cost;
        } else {
            $highest_class_cost = max( $highest_class_cost, $class_cost );
        }
    }
    
    if ( 'order' === $this->get_option( 'type' ) ) {
        $rate['cost'] += $highest_class_cost;
    }
    
    $this->add_rate( $rate );
}

/**
 * Find shipping classes in package.
 *
 * @param array $package Package information.
 * @return array
 */
public function find_shipping_classes( $package ) {
    $found_shipping_classes = array();
    
    foreach ( $package['contents'] as $item_id => $values ) {
        if ( $values['data']->needs_shipping() ) {
            $found_class = $values['data']->get_shipping_class();
            
            if ( ! isset( $found_shipping_classes[ $found_class ] ) ) {
                $found_shipping_classes[ $found_class ] = array();
            }
            
            $found_shipping_classes[ $found_class ][ $item_id ] = $values;
        }
    }
    
    return $found_shipping_classes;
}

Advanced Features

Multiple Rates

Offer multiple shipping options from one method:
public function calculate_shipping( $package = array() ) {
    // Standard shipping
    $this->add_rate( array(
        'id'      => $this->get_rate_id() . ':standard',
        'label'   => __( 'Standard Shipping (5-7 days)', 'custom-shipping-method' ),
        'cost'    => 5,
        'package' => $package,
    ) );
    
    // Express shipping
    $this->add_rate( array(
        'id'      => $this->get_rate_id() . ':express',
        'label'   => __( 'Express Shipping (2-3 days)', 'custom-shipping-method' ),
        'cost'    => 15,
        'package' => $package,
    ) );
    
    // Overnight shipping
    $this->add_rate( array(
        'id'      => $this->get_rate_id() . ':overnight',
        'label'   => __( 'Overnight Shipping', 'custom-shipping-method' ),
        'cost'    => 30,
        'package' => $package,
    ) );
}

Real-time API Integration

public function calculate_shipping( $package = array() ) {
    // Get customer location
    $destination = array(
        'country'  => $package['destination']['country'],
        'state'    => $package['destination']['state'],
        'postcode' => $package['destination']['postcode'],
        'city'     => $package['destination']['city'],
    );
    
    // Get package details
    $weight = 0;
    foreach ( $package['contents'] as $item ) {
        $weight += $item['data']->get_weight() * $item['quantity'];
    }
    
    try {
        // Call shipping API
        $response = wp_remote_post( 'https://api.shippingprovider.com/rates', array(
            'body' => array(
                'origin'      => $this->get_origin_address(),
                'destination' => $destination,
                'weight'      => $weight,
                'api_key'     => $this->get_option( 'api_key' ),
            ),
        ) );
        
        if ( is_wp_error( $response ) ) {
            return;
        }
        
        $rates = json_decode( wp_remote_retrieve_body( $response ), true );
        
        foreach ( $rates as $api_rate ) {
            $this->add_rate( array(
                'id'      => $this->get_rate_id() . ':' . $api_rate['service_code'],
                'label'   => $api_rate['service_name'],
                'cost'    => $api_rate['price'],
                'package' => $package,
            ) );
        }
    } catch ( Exception $e ) {
        // Log error but don't show rates if API fails
        error_log( 'Shipping API error: ' . $e->getMessage() );
    }
}

Conditional Availability

public function is_available( $package ) {
    // Not available for international orders
    if ( $package['destination']['country'] !== 'US' ) {
        return false;
    }
    
    // Not available for orders over 50kg
    $weight = 0;
    foreach ( $package['contents'] as $item ) {
        $weight += $item['data']->get_weight() * $item['quantity'];
    }
    
    if ( $weight > 50 ) {
        return false;
    }
    
    return parent::is_available( $package );
}

Shipping Zones Integration

1

Navigate to Shipping Zones

Go to WooCommerce > Settings > Shipping and create or edit a shipping zone.
2

Add Shipping Method

Click “Add shipping method” and select your custom method from the dropdown.
3

Configure Instance Settings

Click “Edit” to configure the method’s settings for that specific zone.
4

Set Method Order

Drag and drop to reorder methods within the zone.
Each instance of your shipping method in a zone can have different settings. For example, different costs for different regions.

Package Information

The $package array passed to calculate_shipping() contains:
array(
    'contents'        => array(), // Cart items
    'contents_cost'   => 0,       // Total cost of items
    'applied_coupons' => array(), // Applied coupon codes
    'user'            => array(
        'ID' => 0,
    ),
    'destination'     => array(
        'country'   => '',
        'state'     => '',
        'postcode'  => '',
        'city'      => '',
        'address'   => '',
        'address_2' => '',
    ),
)

Testing Your Shipping Method

1

Add to Shipping Zone

Add your method to a test shipping zone.
2

Create Test Orders

  • Add products to cart
  • Enter different shipping addresses
  • Verify rates calculate correctly
3

Test Edge Cases

  • Empty cart
  • Very heavy orders
  • International addresses
  • Free shipping thresholds
4

Debug Output

Use WooCommerce logging:
$logger = wc_get_logger();
$logger->debug( 'Calculated shipping cost: ' . $cost, array( 'source' => 'custom-shipping' ) );

Best Practices

  • Performance: Cache external API calls to avoid slow checkout
  • Validation: Always validate and sanitize settings input
  • Error Handling: Gracefully handle API failures
  • Backwards Compatibility: Support both instance and global settings if needed
  • Logging: Use WooCommerce’s logger for debugging

Build docs developers (and LLMs) love