wc-shipping-providers

Register a custom WooCommerce shipping provider — extend AbstractShippingProvider and add it via the woocommerce_fulfillment_shipping_providers filter (WC 10.1+). Distinct from shipping methods (rate at checkout) — a provider is a carrier identity (UPS, FedEx, DHL, your custom carrier) used by the Fulfillments system for tracking URLs, tracking-number pattern detection, country-pair support, and the orders-list filter. Required methods get_key / get_name / get_icon / get_tracking_url, with optional shipping-from / shipping-to / try_parse_tracking_number. Registration accepts class names (resolved via the DI container) or pre-built instances, keyed by get_key. Use when integrating a private carrier, fulfillment service, or any tracking-aware shipping identity. Triggers on AbstractShippingProvider, woocommerce_fulfillment_shipping_providers, FulfillmentUtils::get_shipping_providers, "shipping provider" in WooCommerce context, or carrier tracking integration.

Best use case

wc-shipping-providers is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Register a custom WooCommerce shipping provider — extend AbstractShippingProvider and add it via the woocommerce_fulfillment_shipping_providers filter (WC 10.1+). Distinct from shipping methods (rate at checkout) — a provider is a carrier identity (UPS, FedEx, DHL, your custom carrier) used by the Fulfillments system for tracking URLs, tracking-number pattern detection, country-pair support, and the orders-list filter. Required methods get_key / get_name / get_icon / get_tracking_url, with optional shipping-from / shipping-to / try_parse_tracking_number. Registration accepts class names (resolved via the DI container) or pre-built instances, keyed by get_key. Use when integrating a private carrier, fulfillment service, or any tracking-aware shipping identity. Triggers on AbstractShippingProvider, woocommerce_fulfillment_shipping_providers, FulfillmentUtils::get_shipping_providers, "shipping provider" in WooCommerce context, or carrier tracking integration.

Teams using wc-shipping-providers should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/wc-shipping-providers/SKILL.md --create-dirs "https://raw.githubusercontent.com/nvdigitalsolutions/mcp-ai-wpoos/main/addons/pro/includes/bundled-skills/wc-shipping-providers/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/wc-shipping-providers/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How wc-shipping-providers Compares

Feature / Agentwc-shipping-providersStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Register a custom WooCommerce shipping provider — extend AbstractShippingProvider and add it via the woocommerce_fulfillment_shipping_providers filter (WC 10.1+). Distinct from shipping methods (rate at checkout) — a provider is a carrier identity (UPS, FedEx, DHL, your custom carrier) used by the Fulfillments system for tracking URLs, tracking-number pattern detection, country-pair support, and the orders-list filter. Required methods get_key / get_name / get_icon / get_tracking_url, with optional shipping-from / shipping-to / try_parse_tracking_number. Registration accepts class names (resolved via the DI container) or pre-built instances, keyed by get_key. Use when integrating a private carrier, fulfillment service, or any tracking-aware shipping identity. Triggers on AbstractShippingProvider, woocommerce_fulfillment_shipping_providers, FulfillmentUtils::get_shipping_providers, "shipping provider" in WooCommerce context, or carrier tracking integration.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# WooCommerce: custom shipping providers (Fulfillments)

For plugins that integrate a **carrier** with WooCommerce — UPS / FedEx / DHL / a national post / a private courier. A "shipping provider" in WC 10.1+ is a carrier identity tied into the Fulfillments system: tracking URL generation, tracking-number pattern detection, country-pair support, and admin filtering on the orders list. **Different from a shipping method**, which is a checkout-time rate calculator.

WC 10.7 ships ~70 built-in providers (DHL, FedEx, UPS, USPS, Royal Mail, La Poste, Deutsche Post, Australia Post, Canada Post, dozens of national posts, courier services). Adding a new one is a small abstract-class extension + one filter callback.

## Misconception this skill corrects

> "A shipping provider is a shipping method. I'll extend `WC_Shipping_Method`."

These are two different concepts:

| Concept | Class | When | Purpose |
|---|---|---|---|
| Shipping **method** | `WC_Shipping_Method` (sibling skill `wc-shipping-method`) | Checkout time | Calculate the rate the customer pays for shipping. |
| Shipping **provider** | `AbstractShippingProvider` (this skill) | Post-purchase / fulfillment time | Generate tracking URLs, parse tracking numbers, identify carriers on the admin orders list. |

The same store can use Free Shipping (method) at checkout and DHL (provider) for the actual fulfillment. They don't overlap or compete — they sit at different lifecycle stages.

## When to use this skill

Trigger when ANY of the following is true:

- Integrating a courier / carrier with WC Fulfillments — even a small national one not in the built-in list.
- Writing a plugin that needs to generate carrier-specific tracking URLs from tracking numbers.
- Building a "tracking number autocomplete" feature (parse the entered number → infer the carrier).
- The diff or file contains: `AbstractShippingProvider`, `woocommerce_fulfillment_shipping_providers`, `FulfillmentUtils::get_shipping_providers`, `Automattic\WooCommerce\Admin\Features\Fulfillments\Providers`.
- The user says "shipping provider" in a WC context — confirm whether they actually mean shipping method (checkout rate) or shipping provider (fulfillment carrier identity).

## API surface — the abstract class

`Automattic\WooCommerce\Admin\Features\Fulfillments\Providers\AbstractShippingProvider` ([src/Admin/Features/Fulfillments/Providers/AbstractShippingProvider.php](AbstractShippingProvider.php)):

**Required (abstract):**

```php
abstract public function get_key(): string;
abstract public function get_name(): string;
abstract public function get_icon(): string;
abstract public function get_tracking_url( string $tracking_number ): string;
```

**Optional (default implementations exist):**

```php
public function get_shipping_from_countries(): array { return array(); }
public function get_shipping_to_countries(): array   { return array(); }
public function can_ship_from( string $country_code ): bool;
public function can_ship_to( string $country_code ): bool;
public function can_ship_from_to( string $shipping_from, string $shipping_to ): bool;
public function try_parse_tracking_number( string $tracking_number, string $shipping_from, string $shipping_to ): ?array;
```

`try_parse_tracking_number` is the carrier-detection hook: given a tracking number string and a country pair, return either `null` (this isn't our format) or an array describing the parsed result (used by WC for autocompletion and ambiguity scoring).

## Minimal scaffold — register a custom provider

```php
namespace MyPlugin\Shipping;

use Automattic\WooCommerce\Admin\Features\Fulfillments\Providers\AbstractShippingProvider;

class MyCourierShippingProvider extends AbstractShippingProvider {

    public function get_key(): string {
        return 'mycourier';
    }

    public function get_name(): string {
        return 'My Courier';
    }

    public function get_icon(): string {
        // Absolute URL to a small carrier logo (typical: 64x64 PNG).
        return plugins_url( 'assets/images/mycourier.png', MYPLUGIN_PLUGIN_FILE );
    }

    public function get_tracking_url( string $tracking_number ): string {
        return sprintf(
            'https://track.mycourier.example/?tn=%s',
            rawurlencode( $tracking_number )
        );
    }

    // Optional — restrict to specific country pairs
    public function get_shipping_from_countries(): array {
        return array( 'HU' );
    }

    public function get_shipping_to_countries(): array {
        return array( 'HU', 'SK', 'AT', 'RO' );
    }

    // Optional — pattern-detect "is this a MyCourier tracking number?"
    //
    // Verified return shape (FulfillmentsManager.php lines 510-527):
    //   - 'url'             — string, the tracking URL. NOTE: it's 'url', not 'tracking_url'.
    //                         The Manager reads $result['url'] and exposes it as the
    //                         outer 'tracking_url' field on the public response.
    //   - 'ambiguity_score' — int, HIGHER means more confident match. Built-in providers
    //                         use values like 70 (loose match), 85-90 (typical strong
    //                         match), 92-98 (very specific format). Manager picks the
    //                         provider with the HIGHEST score when multiple match.
    public function try_parse_tracking_number(
        string $tracking_number,
        string $shipping_from,
        string $shipping_to
    ): ?array {
        if ( ! preg_match( '/^MC[0-9]{10}HU$/', $tracking_number ) ) {
            return null; // not our format
        }

        return array(
            'url'             => $this->get_tracking_url( $tracking_number ),
            'ambiguity_score' => 90, // strong match — exact regex with country anchor
        );
    }
}
```

Then register it via the filter:

```php
add_filter( 'woocommerce_fulfillment_shipping_providers', static function ( array $providers ): array {
    // Either pass a class name string (resolved via WC's DI container)…
    $providers[] = MyCourierShippingProvider::class;

    // …or pass a pre-built instance.
    // $providers[] = new MyCourierShippingProvider();

    return $providers;
} );
```

That's the entire surface. The provider now appears in the Fulfillments admin (provider dropdown when adding a tracking number to an order) and in the orders-list "Shipping provider" filter.

## How `get_shipping_providers()` resolves the filter

`FulfillmentUtils::get_shipping_providers()` ([src/Admin/Features/Fulfillments/FulfillmentUtils.php:417](FulfillmentUtils.php)) iterates the filter result and accepts either:

- An `AbstractShippingProvider` instance — used directly.
- A class name string — resolved via `wc_get_container()->get( $class )` (WC's DI container). Failure is silent (`continue` on Throwable).

It rejects entries that are neither — strings that aren't class names, scalars, anonymous arrays, etc. The final array is keyed by `get_key()`. **Two providers with the same key collide silently** — last-registered wins.

Because of the DI resolution, classes can have constructor dependencies (e.g. an HTTP client for live tracking lookups) that the container will inject — useful when the provider needs more than just the four abstract methods.

## Registering in the right place

The `woocommerce_fulfillment_shipping_providers` filter fires every time `FulfillmentUtils::get_shipping_providers()` is called — admin order detail render, orders-list filter, REST endpoint. Add the filter on `plugins_loaded` or your Plugin class's runtime registration phase.

```php
add_action( 'plugins_loaded', static function () {
    require_once __DIR__ . '/includes/MyCourierShippingProvider.php';
    add_filter( 'woocommerce_fulfillment_shipping_providers', /* ... */ );
}, 20 );
```

## Critical rules

- **`AbstractShippingProvider` ≠ `WC_Shipping_Method`.** Different concept, different lifecycle stage, different class hierarchy. If the goal is "show a rate at checkout", you need `WC_Shipping_Method` (sibling skill).
- **`get_key()` is the unique identifier** — keep it stable. Renaming it is a breaking change for orders that already store the key on their fulfillment records.
- **`get_tracking_url` MUST URL-encode the tracking number** to handle special characters and prevent URL injection.
- **`try_parse_tracking_number` returns `null` on no-match**, NOT an empty array. WC's detection logic distinguishes "didn't match" (null) from "matched with empty data" (array).
- **`ambiguity_score`: HIGHER = MORE CONFIDENT.** Verified in `FulfillmentsManager::get_best_parsing_result()` (line 561) — `$result['ambiguity_score'] > $best_score` picks the highest. Built-in carriers use 70 (loose / fallback patterns), 85-90 (typical strong matches), 92-98 (very specific formats). Use a high score for exact-regex matches, low for permissive heuristics that could match multiple carriers.
- **The result's `'url'` key is the tracking URL** — NOT `'tracking_url'`. The `FulfillmentsManager` reads `$result['url']` (line 522) and re-exposes it on the outer response as `tracking_url`. If you key it as `tracking_url` inside your provider's parse result, the carrier is recognised but the outer `tracking_url` field stays empty.
- **Two providers with the same `get_key()` collide silently.** Pick a slug-style key prefixed with your plugin / company name to avoid collisions with the built-ins (e.g. `mycompany-courier`, not just `mycourier`).
- **Country lists in `get_shipping_from_countries()` / `get_shipping_to_countries()` are ISO-3166-1 alpha-2 codes** — `'HU'`, `'DE'`, `'US'`. Same convention as `wc_get_country_locale()`.
- **The icon should be 64x64 or similar small PNG/SVG**, served over HTTPS. Used in admin UI carrier selection.
- **Use the DI container or a pre-built instance** — both work. For providers with no constructor dependencies, the class-string form is simplest.

## Common mistakes

```php
// WRONG — extending the wrong base class
class MyCourier extends WC_Shipping_Method { /* ... */ }
// This is a checkout-time rate calculator, not a fulfillment provider.

// WRONG — bare key collision with a built-in
public function get_key(): string {
    return 'dhl'; // already used by built-in DHLShippingProvider
}

// RIGHT — prefixed
public function get_key(): string {
    return 'mycompany-internal-courier';
}

// WRONG — returning empty array from try_parse_tracking_number when no match
public function try_parse_tracking_number( $tn, $from, $to ): ?array {
    if ( /* no match */ ) {
        return array(); // 🔴 WC treats this as "matched with empty data"
    }
}

// RIGHT — return null on no-match
public function try_parse_tracking_number( $tn, $from, $to ): ?array {
    if ( /* no match */ ) {
        return null;
    }
    return array( 'url' => $this->get_tracking_url( $tn ), 'ambiguity_score' => 90 );
}

// WRONG — wrong key name; carrier is detected but tracking URL stays empty
return array(
    'tracking_url'    => $this->get_tracking_url( $tn ), // 🔴 should be 'url'
    'ambiguity_score' => 90,
);

// WRONG — score inverted (treating 0 as "highest confidence")
return array(
    'url'             => $this->get_tracking_url( $tn ),
    'ambiguity_score' => 0, // 🔴 0 means LOWEST; this provider loses to any provider that returns ≥1
);

// RIGHT — high score for confident match
return array(
    'url'             => $this->get_tracking_url( $tn ),
    'ambiguity_score' => 90,
);

// WRONG — registering on rest_api_init or admin_init (too late for some uses)
add_action( 'rest_api_init', function () {
    add_filter( 'woocommerce_fulfillment_shipping_providers', /* ... */ );
} );

// RIGHT — registering on plugins_loaded so it's ready for any caller
add_action( 'plugins_loaded', function () {
    add_filter( 'woocommerce_fulfillment_shipping_providers', /* ... */ );
}, 20 );

// WRONG — non-URL-safe tracking number in URL
return 'https://track.example/?tn=' . $tracking_number;

// RIGHT — URL-encode user input
return 'https://track.example/?tn=' . rawurlencode( $tracking_number );

// WRONG — returning provider instances from get_icon
public function get_icon(): string {
    return MYPLUGIN_PLUGIN_PATH . 'assets/icon.png'; // server filesystem path, not a URL
}

// RIGHT — public URL
public function get_icon(): string {
    return plugins_url( 'assets/icon.png', MYPLUGIN_PLUGIN_FILE );
}
```

## Reading provider data at runtime

```php
use Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentUtils;

$providers = FulfillmentUtils::get_shipping_providers();
// array<string, AbstractShippingProvider> keyed by provider get_key()

$dhl = $providers['dhl'] ?? null;
if ( $dhl ) {
    $url = $dhl->get_tracking_url( $tracking_number );
}

// Or programmatically iterate to find a carrier that matches a tracking number:
foreach ( $providers as $provider ) {
    $parsed = $provider->try_parse_tracking_number( $tn, 'HU', 'DE' );
    if ( $parsed !== null ) {
        // Found a match
        break;
    }
}
```

The returned providers are fully resolved instances — no further `wc_get_container()` calls needed.

## Cross-references

- Run **`wc-shipping-method`** when the goal is a checkout-time rate calculator (the typical "I want to add a custom shipping option" request).
- Run **`wc-hpos-compatibility`** if your provider stores per-order metadata — order writes go through `WC_Order::update_meta_data`, not direct postmeta.
- Run **`wp-plugin-bootstrap`** for the `Requires Plugins: woocommerce` declaration and the surrounding plugin file.

## What this skill does NOT cover

- The Fulfillments REST API endpoints (`/wc/v4/orders/<id>/fulfillments`, etc.) — adjacent topic; covered by the REST v4 skill if your plugin reads/writes fulfillments programmatically.
- Label generation, address printing, postage purchase flows — those are carrier-specific integrations the provider class doesn't standardize.
- The Fulfillments meta-box in the order edit screen — admin UI, separate concern.
- Live rate-fetching from the carrier API (used by some plugins to show real-time shipping rates) — that's a `WC_Shipping_Method` concern, not a provider concern.
- Webhook receivers from carriers (status updates, delivery confirmations) — also outside the provider class; usually a custom REST route on your end.

## References

- `AbstractShippingProvider`: [wp-content/plugins/woocommerce/src/Admin/Features/Fulfillments/Providers/AbstractShippingProvider.php](AbstractShippingProvider.php).
- Provider registry & filter: [wp-content/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentUtils.php:417](FulfillmentUtils.php) — `get_shipping_providers()` + the `woocommerce_fulfillment_shipping_providers` filter (`@since 10.1.0`).
- Static class-name → instance map of built-in providers: [wp-content/plugins/woocommerce/src/Admin/Features/Fulfillments/ShippingProviders.php](ShippingProviders.php) — ~70 entries, useful as a reference for naming conventions and key style.
- Reference implementation (DHL): [wp-content/plugins/woocommerce/src/Admin/Features/Fulfillments/Providers/DHLShippingProvider.php](DHLShippingProvider.php).

Related Skills

wc-shipping-method

5
from nvdigitalsolutions/mcp-ai-wpoos

Registers a custom WooCommerce shipping method with explicit control over which fields appear in the per-zone settings modal — extend WC_Shipping_Method, declare your fields in init_form_fields (and ONLY those fields, no unset / DOM hacks / CSS hides), set the $supports array to control whether the modal opens (omit 'instance-settings' to suppress it entirely), register via the woocommerce_shipping_methods filter, load the class on woocommerce_shipping_init. Corrects the "this is React, removing fields is hard" misconception — the zone-method modal is Backbone with a PHP-rendered settings_html string; the field list is wholly PHP-controlled. Use when scaffolding a shipping method or when you want a feature-flag-only modal without WC defaults. Triggers on WC_Shipping_Method, woocommerce_shipping_methods, woocommerce_shipping_init, init_form_fields with shipping context, $supports shipping-zones, calculate_shipping, add_rate, or "remove default fields from shipping method".

webapp-testing

5
from nvdigitalsolutions/mcp-ai-wpoos

Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.

web-artifacts-builder

5
from nvdigitalsolutions/mcp-ai-wpoos

Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.

valyu

5
from nvdigitalsolutions/mcp-ai-wpoos

Search the live web and 36+ specialised data sources including SEC filings, PubMed, ChEMBL, clinical trials, FRED economic indicators, and patent databases. Use when current, authoritative, or paywalled data is required.

ui-ux-pro-max

5
from nvdigitalsolutions/mcp-ai-wpoos

AI-powered design intelligence with 67 UI styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 15+ tech stacks. Generates complete design systems for any product type with industry-specific reasoning rules.

theme-factory

5
from nvdigitalsolutions/mcp-ai-wpoos

Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.

slack-gif-creator

5
from nvdigitalsolutions/mcp-ai-wpoos

Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like "make me a GIF of X doing Y for Slack."

skill-creator

5
from nvdigitalsolutions/mcp-ai-wpoos

Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, update or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy.

shannon

5
from nvdigitalsolutions/mcp-ai-wpoos

Autonomous AI security pen testing. Executes real exploits against web applications to find SQL injection, XSS, SSRF, authentication flaws, and IDOR vulnerabilities. Reports only confirmed, reproducible findings — no false positives.

remotion

5
from nvdigitalsolutions/mcp-ai-wpoos

Create programmatic videos using React and Remotion. Translate natural language descriptions into working Remotion components for product demos, release announcements, explainer videos, and animated content.

planetscale

5
from nvdigitalsolutions/mcp-ai-wpoos

Design schemas and write queries for PlanetScale serverless MySQL using branch-based workflows. Ensures index coverage, avoids foreign key anti-patterns, and treats every schema change as a reviewable, reversible deploy request.

mcp-builder

5
from nvdigitalsolutions/mcp-ai-wpoos

Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).