WooCommerce Shipping Zones and Custom Shipping Methods

WooCommerce’s shipping zone system lets you define geographic areas and assign shipping methods to each zone — but real-world requirements often demand dynamic rates based on weight, dimensions, or a live carrier API. Implementing a custom shipping method as a WC_Shipping_Method subclass integrates cleanly with the zone system while giving you full control over rate calculation logic.

Problem: WooCommerce's built-in shipping zones cover basic flat rate and free shipping scenarios, but complex logistics — weight-based tiers, carrier-calculated rates, rules that differ by product category — require a custom shipping method.

Solution: Create a custom shipping method by extending WC_Shipping_Method — override calculate_shipping() to add rates with $this->add_rate(), define admin settings fields in init_form_fields(), and register the method with the woocommerce_shipping_methods filter. Access package weight and destination from the $package argument to implement rule-based rate logic.


The code below registers a custom shipping method class that calculates rates from a live carrier API (with transient caching), integrates with WooCommerce shipping zones, and adds an admin settings panel for the API credentials.


id                 = 'my_carrier';
        $this->instance_id        = absint( $instance_id );
        $this->method_title       = __( 'My Carrier', 'my-plugin' );
        $this->method_description = __( 'Live rates from My Carrier API.', 'my-plugin' );
        $this->supports           = [ 'shipping-zones', 'instance-settings' ];
        $this->title              = $this->get_option( 'title', __( 'My Carrier', 'my-plugin' ) );

        $this->init();
    }

    public function init(): void {
        $this->init_form_fields();
        $this->init_settings();
        add_action( 'woocommerce_update_options_shipping_' . $this->id, [ $this, 'process_admin_options' ] );
    }

    public function init_form_fields(): void {
        $this->instance_form_fields = [
            'title'   => [
                'title'   => __( 'Method title', 'my-plugin' ),
                'type'    => 'text',
                'default' => __( 'My Carrier', 'my-plugin' ),
            ],
            'api_key' => [
                'title' => __( 'API Key', 'my-plugin' ),
                'type'  => 'password',
            ],
            'markup'  => [
                'title'       => __( 'Markup (%)', 'my-plugin' ),
                'type'        => 'number',
                'default'     => '0',
                'description' => __( 'Add a percentage markup to carrier rates.', 'my-plugin' ),
            ],
        ];
    }

    public function calculate_shipping( array $package = [] ): void {
        $api_key = $this->get_option( 'api_key' );
        if ( ! $api_key ) {
            return;
        }

        $cache_key = 'carrier_rates_' . md5( serialize( $package ) );
        $rates     = get_transient( $cache_key );

        if ( false === $rates ) {
            $rates = $this->fetch_carrier_rates( $package, $api_key );
            if ( ! is_wp_error( $rates ) ) {
                set_transient( $cache_key, $rates, 10 * MINUTE_IN_SECONDS );
            } else {
                return;
            }
        }

        $markup = ( 1 + ( (float) $this->get_option( 'markup', 0 ) / 100 ) );
        foreach ( $rates as $service ) {
            $this->add_rate( [
                'id'    => $this->id . '_' . sanitize_key( $service['code'] ),
                'label' => esc_html( $service['name'] ),
                'cost'  => round( (float) $service['price'] * $markup, 2 ),
                'meta_data' => [ 'estimated_days' => $service['days'] ],
            ] );
        }
    }

    private function fetch_carrier_rates( array $package, string $api_key ): array|\WP_Error {
        $response = wp_remote_post( 'https://api.mycarrier.example.com/rates', [
            'headers' => [ 'X-API-Key' => $api_key, 'Content-Type' => 'application/json' ],
            'body'    => wp_json_encode( [
                'destination' => $package['destination'],
                'weight_kg'   => array_reduce( $package['contents'], fn( $w, $i ) => $w + (float) $i['data']->get_weight() * $i['quantity'], 0 ),
            ] ),
            'timeout' => 8,
        ] );
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        return json_decode( wp_remote_retrieve_body( $response ), true )['rates'] ?? [];
    }
}


NOTE: The transient cache key uses md5(serialize($package)) — the $package array includes the cart contents, destination, and user data, so cache hits only occur when all three are identical; for shared-hosting environments where many customers share the same destination, consider a coarser cache key based only on destination postcode and total weight to improve cache hit rates.