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
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/wc-shipping-providers/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How wc-shipping-providers Compares
| Feature / Agent | wc-shipping-providers | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/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
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
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
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
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
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
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
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
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
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
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
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
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).