Commit 757f400b160 for woocommerce

commit 757f400b1604906c132e285a5c720236dd36112d
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date:   Thu Mar 26 17:07:00 2026 +0300

    [WOOPLUG-6360+6047] Add custom shipping providers and provider filter (#63766)

diff --git a/plugins/woocommerce/changelog/63766-add-custom-shipping-providers-and-provider-filter b/plugins/woocommerce/changelog/63766-add-custom-shipping-providers-and-provider-filter
new file mode 100644
index 00000000000..9f8cb463772
--- /dev/null
+++ b/plugins/woocommerce/changelog/63766-add-custom-shipping-providers-and-provider-filter
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add custom shipping providers settings UI and shipping provider filter for the orders list.
\ No newline at end of file
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-manual-entry-form.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-manual-entry-form.tsx
index cf1126d506b..bc4ae6f81a5 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-manual-entry-form.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-manual-entry-form.tsx
@@ -10,7 +10,7 @@ import { __ } from '@wordpress/i18n';
  */
 import { useShipmentFormContext } from '../../context/shipment-form-context';
 import ShipmentProviders from '../../data/shipment-providers';
-import { SearchIcon } from '../../utils/icons';
+import { SearchIcon, TruckIcon } from '../../utils/icons';

 const ShippingProviderListItem = ( {
 	item,
@@ -25,11 +25,13 @@ const ShippingProviderListItem = ( {
 					item.value,
 			].join( ' ' ) }
 		>
-			{ item.icon && (
-				<div className="woocommerce-fulfillment-shipping-provider-list-item-icon">
-					<img src={ item.icon } alt="" aria-hidden="true" />
-				</div>
-			) }
+			<div className="woocommerce-fulfillment-shipping-provider-list-item-icon">
+				{ item.icon ? (
+					<img src={ item.icon } alt={ item.label } />
+				) : (
+					<TruckIcon />
+				) }
+			</div>
 			<div className="woocommerce-fulfillment-shipping-provider-list-item-label">
 				{ item.label }
 			</div>
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-tracking-number-form.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-tracking-number-form.tsx
index 170c9a327e0..5464b0b1a25 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-tracking-number-form.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-tracking-number-form.tsx
@@ -15,7 +15,7 @@ import { addQueryArgs } from '@wordpress/url';
  */
 import { useShipmentFormContext } from '../../context/shipment-form-context';
 import ErrorLabel from '../user-interface/error-label';
-import { EditIcon } from '../../utils/icons';
+import { EditIcon, TruckIcon } from '../../utils/icons';
 import { findShipmentProviderName } from '../../utils/fulfillment-utils';
 import ShipmentProviders from '../../data/shipment-providers';
 import { useFulfillmentContext } from '../../context/fulfillment-context';
@@ -35,18 +35,21 @@ interface TrackingNumberParsingResponse {

 const ShipmentProviderIcon = ( { providerKey }: { providerKey: string } ) => {
 	const provider = ShipmentProviders.find( ( p ) => p.value === providerKey );
-	const icon = provider?.icon;
-	if ( ! provider || ! icon ) {
+	if ( ! provider ) {
 		return null;
 	}

 	return (
 		<div className="woocommerce-fulfillment-shipment-provider-icon">
-			<img
-				src={ icon }
-				alt={ `${ provider.label } shipping provider logo` }
-				key={ providerKey }
-			/>
+			{ provider.icon ? (
+				<img
+					src={ provider.icon }
+					alt={ provider.label }
+					key={ providerKey }
+				/>
+			) : (
+				<TruckIcon />
+			) }
 		</div>
 	);
 };
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-viewer.tsx b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-viewer.tsx
index 7f66a836dfa..cb3e935f508 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-viewer.tsx
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/shipment-viewer.tsx
@@ -52,9 +52,9 @@ export default function ShipmentViewer() {
 			header={
 				isShipmentInformationProvided ? (
 					<>
-						{ shipmentProviderObject ? (
+						{ shipmentProviderObject?.icon ? (
 							<img
-								src={ shipmentProviderObject.icon || '' }
+								src={ shipmentProviderObject.icon }
 								alt={ shipmentProviderObject.label || '' }
 							/>
 						) : (
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/test/shipment-manual-entry-form.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/test/shipment-manual-entry-form.test.js
index 80078935833..fbaed33d78e 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/test/shipment-manual-entry-form.test.js
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/test/shipment-manual-entry-form.test.js
@@ -16,6 +16,7 @@ jest.mock( '../../../context/shipment-form-context', () => ( {

 jest.mock( '../../../utils/icons', () => ( {
 	SearchIcon: () => <span data-testid="search-icon" />,
+	TruckIcon: () => <span data-testid="truck-icon" />,
 } ) );

 jest.mock( '@wordpress/components', () => ( {
diff --git a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/test/shipment-tracking-number-form.test.js b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/test/shipment-tracking-number-form.test.js
index 1ee7e96d436..96523a5767c 100644
--- a/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/test/shipment-tracking-number-form.test.js
+++ b/plugins/woocommerce/client/admin/client/wp-admin-scripts/fulfillments/components/shipment-form/test/shipment-tracking-number-form.test.js
@@ -24,6 +24,7 @@ jest.mock( '../../../context/shipment-form-context', () => ( {

 jest.mock( '../../../utils/icons', () => ( {
 	EditIcon: () => <span data-testid="edit-icon" />,
+	TruckIcon: () => <span data-testid="truck-icon" />,
 } ) );

 jest.mock( '@wordpress/api-fetch' );
diff --git a/plugins/woocommerce/client/legacy/js/admin/wc-shipping-providers.js b/plugins/woocommerce/client/legacy/js/admin/wc-shipping-providers.js
new file mode 100644
index 00000000000..e4068be7f39
--- /dev/null
+++ b/plugins/woocommerce/client/legacy/js/admin/wc-shipping-providers.js
@@ -0,0 +1,223 @@
+/* global shippingProvidersLocalizeScript, ajaxurl */
+( function( $, data, wp, ajaxurl ) {
+	$( function() {
+		if (
+			! document.getElementById( 'tmpl-wc-shipping-provider-row' ) ||
+			! document.getElementById( 'tmpl-wc-shipping-provider-row-blank' )
+		) {
+			return;
+		}
+
+		var $tbody          = $( '.wc-shipping-provider-rows' ),
+			$row_template   = wp.template( 'wc-shipping-provider-row' ),
+			$blank_template = wp.template( 'wc-shipping-provider-row-blank' ),
+
+			// Backbone model
+			ShippingProvider       = Backbone.Model.extend({
+				save: function( changes ) {
+					var self = this;
+					$.ajax({
+						url: ajaxurl + ( ajaxurl.indexOf( '?' ) > 0 ? '&' : '?' ) + 'action=woocommerce_shipping_providers_save_changes',
+						type: 'POST',
+						data: {
+							wc_shipping_providers_nonce : data.wc_shipping_providers_nonce,
+							changes: changes,
+						},
+						dataType: 'json'
+					}).done( function( response ) {
+						if ( response.success ) {
+							if ( response.data.error ) {
+								window.alert( response.data.error );
+							}
+							shippingProvider.set( 'providers', response.data.shipping_providers );
+							shippingProvider.trigger( 'saved:providers' );
+						} else if ( response.data ) {
+							window.alert( response.data );
+						} else {
+							window.alert( data.strings.save_failed );
+						}
+					}).fail( function() {
+						window.alert( data.strings.save_failed );
+					}).always( function() {
+						shippingProviderView.unblock();
+					});
+				}
+			} ),
+
+			// Backbone view
+			ShippingProviderView = Backbone.View.extend({
+				rowTemplate: $row_template,
+				initialize: function() {
+					this.listenTo( this.model, 'saved:providers', this.render );
+					$( document.body ).on( 'click', '.wc-shipping-provider-add-new', { view: this }, this.configureNewShippingProvider );
+					$( document.body ).on( 'wc_backbone_modal_response', { view: this }, this.onConfigureShippingProviderSubmitted );
+					$( document.body ).on( 'wc_backbone_modal_loaded', { view: this }, this.onLoadBackboneModal );
+					$( document.body ).on( 'wc_backbone_modal_validation', this.validateFormArguments );
+				},
+				block: function() {
+					$( this.el ).block({
+						message: null,
+						overlayCSS: {
+							background: '#fff',
+							opacity: 0.6
+						}
+					});
+				},
+				unblock: function() {
+					$( this.el ).unblock();
+				},
+				render: function() {
+					var providers = _.indexBy( this.model.get( 'providers' ), 'term_id' ),
+						view      = this;
+
+					this.$el.empty();
+					this.unblock();
+
+					if ( _.size( providers ) ) {
+						providers = _.sortBy( providers, function( provider ) {
+							return provider.name;
+						} );
+
+						$.each( providers, function( id, rowData ) {
+							view.renderRow( rowData );
+						} );
+					} else {
+						view.$el.append( $blank_template );
+					}
+				},
+				renderRow: function( rowData ) {
+					var view = this;
+					view.$el.append( view.rowTemplate( rowData ) );
+					view.initRow( rowData );
+				},
+				initRow: function( rowData ) {
+					var view = this;
+					var $tr = view.$el.find( 'tr[data-id="' + rowData.term_id + '"]');
+
+					$tr.find( 'select' ).each( function() {
+						var attribute = $( this ).data( 'attribute' );
+						$( this ).find( 'option[value="' + rowData[ attribute ] + '"]' ).prop( 'selected', true );
+					} );
+
+					$tr.find( '.view' ).show();
+					$tr.find( '.edit' ).hide();
+					$tr.find( '.wc-shipping-provider-edit' ).on( 'click', { view: this }, this.onEditRow );
+					$tr.find( '.wc-shipping-provider-delete' ).on( 'click', { view: this }, this.onDeleteRow );
+				},
+				configureNewShippingProvider: function( event ) {
+					event.preventDefault();
+					const term_id = 'new-1-' + Date.now();
+
+					$( this ).WCBackboneModal({
+						template : 'wc-shipping-provider-configure',
+						variable : {
+							term_id,
+							action: 'create',
+						},
+						data : {
+							term_id,
+							action: 'create',
+						}
+					});
+				},
+				onConfigureShippingProviderSubmitted: function( event, target, posted_data ) {
+					if ( target === 'wc-shipping-provider-configure' ) {
+						const view = event.data.view;
+						const model = view.model;
+						const isNewRow = posted_data.term_id.includes( 'new-1-' );
+						const rowData = Object.assign( {}, posted_data );
+
+						if ( isNewRow ) {
+							rowData.newRow = true;
+						}
+
+						view.block();
+
+						model.save( {
+							[ posted_data.term_id ]: rowData
+						} );
+					}
+				},
+				validateFormArguments: function( element, target, formData ) {
+					const requiredFields = [ 'name' ];
+					const formIsComplete = Object.keys( formData ).every( function( key ) {
+						if ( requiredFields.indexOf( key ) === -1 ) {
+							return true;
+						}
+						if ( Array.isArray( formData[ key ] ) ) {
+							return formData[ key ].length && !!formData[ key ][ 0 ];
+						}
+						return !!formData[ key ];
+					} );
+					const createButton = document.getElementById( 'btn-ok' );
+					createButton.disabled = ! formIsComplete;
+					createButton.classList.toggle( 'disabled', ! formIsComplete );
+				},
+				onEditRow: function( event ) {
+					const term_id = $( this ).closest('tr').data('id');
+					const model =  event.data.view.model;
+					const providers = _.indexBy( model.get( 'providers' ), 'term_id' );
+					const rowData = providers[ term_id ];
+
+					event.preventDefault();
+					$( this ).WCBackboneModal({
+						template : 'wc-shipping-provider-configure',
+						variable: Object.assign( { action: 'edit' }, rowData ),
+						data : Object.assign( { action: 'edit' }, rowData )
+					});
+				},
+				onLoadBackboneModal: function( event, target ) {
+					if ( 'wc-shipping-provider-configure' === target ) {
+						const modalContent = $('.wc-backbone-modal-content');
+						const term_id = modalContent.data('id');
+						const model =  event.data.view.model;
+						const providers = _.indexBy( model.get( 'providers' ), 'term_id' );
+						const rowData = providers[ term_id ];
+
+						// Make slug read-only when editing an existing provider.
+						if ( rowData ) {
+							$('.wc-backbone-modal-content').find( 'input[name="slug"]' ).prop( 'readonly', true );
+						}
+
+						if ( rowData ) {
+							$('.wc-backbone-modal-content').find( 'select' ).each( function() {
+								var attribute = $( this ).data( 'attribute' );
+								$( this ).find( 'option[value="' + rowData[ attribute ] + '"]' ).prop( 'selected', true );
+							} );
+						}
+					}
+				},
+				onDeleteRow: function( event ) {
+					var view    = event.data.view,
+						model   = view.model,
+						term_id = $( this ).closest('tr').data('id');
+
+					event.preventDefault();
+
+					var confirmMsg = data.strings.delete_confirmation ||
+						'Are you sure you want to delete this shipping provider?';
+					if ( ! window.confirm( confirmMsg ) ) {
+						return;
+					}
+
+					view.block();
+
+					model.save( {
+						[ term_id ]: {
+							term_id,
+							deleted: 'deleted',
+						}
+					} );
+				},
+			} ),
+			shippingProvider = new ShippingProvider({
+				providers: data.providers
+			} ),
+			shippingProviderView = new ShippingProviderView({
+				model:    shippingProvider,
+				el:       $tbody
+			} );
+
+		shippingProviderView.render();
+	});
+})( jQuery, shippingProvidersLocalizeScript, wp, ajaxurl );
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
index f91a7e812de..858acfb0a06 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-assets.php
@@ -264,6 +264,12 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
 					'dependencies' => array( 'jquery', 'wp-util', 'underscore', 'backbone', 'wc-backbone-modal' ),
 					'version'      => $version,
 				),
+				array(
+					'handle'       => 'wc-shipping-providers',
+					'path'         => $plugin_url . '/assets/js/admin/wc-shipping-providers' . $suffix . '.js',
+					'dependencies' => array( 'jquery', 'wp-util', 'underscore', 'backbone', 'wc-backbone-modal' ),
+					'version'      => $version,
+				),
 				array(
 					'handle'       => 'wc-clipboard',
 					'path'         => $plugin_url . '/assets/js/admin/wc-clipboard' . $suffix . '.js',
diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-shipping.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-shipping.php
index 32132b77fab..7d51580dabb 100644
--- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-shipping.php
+++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-shipping.php
@@ -8,6 +8,7 @@

 use Automattic\Jetpack\Constants;
 use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
+use Automattic\WooCommerce\Utilities\FeaturesUtil;

 defined( 'ABSPATH' ) || exit;

@@ -59,6 +60,10 @@ class WC_Settings_Shipping extends WC_Settings_Page {
 			'classes' => __( 'Classes', 'woocommerce' ),
 		);

+		if ( FeaturesUtil::feature_is_enabled( 'fulfillments' ) ) {
+			$sections['fulfillment-providers'] = __( 'Shipping providers', 'woocommerce' );
+		}
+
 		if ( ! $this->wc_is_installing() ) {
 			// Load shipping methods so we can show any global options they may have.
 			$shipping_methods = $this->get_shipping_methods();
@@ -184,6 +189,9 @@ class WC_Settings_Shipping extends WC_Settings_Page {
 		} elseif ( 'classes' === $current_section ) {
 			$hide_save_button = true;
 			$this->output_shipping_class_screen();
+		} elseif ( 'fulfillment-providers' === $current_section && FeaturesUtil::feature_is_enabled( 'fulfillments' ) ) {
+			$hide_save_button = true;
+			$this->output_shipping_providers_screen();
 		} else {
 			$is_shipping_method = false;
 			foreach ( $shipping_methods as $method ) {
@@ -212,6 +220,11 @@ class WC_Settings_Shipping extends WC_Settings_Page {
 			case 'classes':
 				$this->do_update_options_action();
 				break;
+			case 'fulfillment-providers':
+				if ( FeaturesUtil::feature_is_enabled( 'fulfillments' ) ) {
+					$this->do_update_options_action();
+				}
+				break;
 			case '':
 				break;
 			default:
@@ -357,7 +370,7 @@ class WC_Settings_Shipping extends WC_Settings_Page {
 		);
 		wp_enqueue_script( 'wc-shipping-zone-methods' );

-		include_once dirname( __FILE__ ) . '/views/html-admin-page-shipping-zone-methods.php';
+		include_once __DIR__ . '/views/html-admin-page-shipping-zone-methods.php';
 	}

 	/**
@@ -387,7 +400,7 @@ class WC_Settings_Shipping extends WC_Settings_Page {
 		);
 		wp_enqueue_script( 'wc-shipping-zones' );

-		include_once dirname( __FILE__ ) . '/views/html-admin-page-shipping-zones.php';
+		include_once __DIR__ . '/views/html-admin-page-shipping-zones.php';
 	}

 	/**
@@ -420,7 +433,7 @@ class WC_Settings_Shipping extends WC_Settings_Page {
 			$shipping_method->display_errors();
 		}

-		include_once dirname( __FILE__ ) . '/views/html-admin-page-shipping-zones-instance.php';
+		include_once __DIR__ . '/views/html-admin-page-shipping-zones-instance.php';
 	}

 	/**
@@ -458,7 +471,67 @@ class WC_Settings_Shipping extends WC_Settings_Page {
 			)
 		);

-		include_once dirname( __FILE__ ) . '/views/html-admin-page-shipping-classes.php';
+		include_once __DIR__ . '/views/html-admin-page-shipping-classes.php';
+	}
+
+	/**
+	 * Handles output of the shipping providers settings screen.
+	 *
+	 * @since 10.7.0
+	 */
+	protected function output_shipping_providers_screen(): void {
+		$providers = get_terms(
+			array(
+				'taxonomy'   => 'wc_fulfillment_shipping_provider',
+				'hide_empty' => false,
+			)
+		);
+
+		if ( is_wp_error( $providers ) ) {
+			$providers = array();
+		}
+
+		$shipping_providers = array();
+		foreach ( $providers as $provider ) {
+			$shipping_providers[] = array(
+				'term_id'               => $provider->term_id,
+				'name'                  => $provider->name,
+				'slug'                  => $provider->slug,
+				'tracking_url_template' => get_term_meta( $provider->term_id, 'tracking_url_template', true ),
+				'icon'                  => get_term_meta( $provider->term_id, 'icon', true ),
+			);
+		}
+
+		wp_localize_script(
+			'wc-shipping-providers',
+			'shippingProvidersLocalizeScript',
+			array(
+				'providers'                   => $shipping_providers,
+				'default_shipping_provider'   => array(
+					'term_id'               => 0,
+					'name'                  => '',
+					'slug'                  => '',
+					'tracking_url_template' => '',
+					'icon'                  => '',
+				),
+				'wc_shipping_providers_nonce' => wp_create_nonce( 'wc_shipping_providers_nonce' ),
+				'strings'                     => array(
+					'unload_confirmation_msg' => __( 'Your changed data will be lost if you leave this page without saving.', 'woocommerce' ),
+					'save_failed'             => __( 'Your changes were not saved. Please retry.', 'woocommerce' ),
+					'delete_confirmation'     => __( 'Are you sure you want to delete this shipping provider?', 'woocommerce' ),
+				),
+			)
+		);
+		wp_enqueue_script( 'wc-shipping-providers' );
+
+		$shipping_provider_columns = array(
+			'wc-shipping-provider-name'                  => __( 'Name', 'woocommerce' ),
+			'wc-shipping-provider-slug'                  => __( 'Slug', 'woocommerce' ),
+			'wc-shipping-provider-tracking-url-template' => __( 'Tracking URL template', 'woocommerce' ),
+			'wc-shipping-provider-icon'                  => __( 'Icon URL', 'woocommerce' ),
+		);
+
+		include_once __DIR__ . '/views/html-admin-page-shipping-providers.php';
 	}
 }

diff --git a/plugins/woocommerce/includes/admin/settings/views/html-admin-page-shipping-providers.php b/plugins/woocommerce/includes/admin/settings/views/html-admin-page-shipping-providers.php
new file mode 100644
index 00000000000..326daa8acd9
--- /dev/null
+++ b/plugins/woocommerce/includes/admin/settings/views/html-admin-page-shipping-providers.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ * Shipping providers admin
+ *
+ * @package WooCommerce\Admin\Shipping
+ */
+
+declare( strict_types = 1 );
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+?>
+
+<h2 class="wc-shipping-zones-heading">
+	<span><?php esc_html_e( 'Shipping providers', 'woocommerce' ); ?></span>
+	<a class="page-title-action wc-shipping-provider-add-new" href="#"><?php esc_html_e( 'Add shipping provider', 'woocommerce' ); ?></a>
+</h2>
+
+<p class="wc-shipping-zone-help-text">
+	<?php esc_html_e( 'Add custom shipping providers so they appear in the fulfillment form when creating shipments. Use the tracking URL template to auto-generate tracking links.', 'woocommerce' ); ?>
+</p>
+
+<table class="wc-shipping-classes widefat">
+	<thead>
+		<tr>
+			<?php foreach ( $shipping_provider_columns as $class => $heading ) : // @phpstan-ignore variable.undefined ?>
+				<th class="<?php echo esc_attr( $class ); ?>"><?php echo esc_html( $heading ); ?></th>
+			<?php endforeach; ?>
+			<th />
+		</tr>
+	</thead>
+	<tbody class="wc-shipping-provider-rows wc-shipping-tables-tbody"></tbody>
+</table>
+
+<script type="text/html" id="tmpl-wc-shipping-provider-row-blank">
+	<tr>
+		<td class="wc-shipping-classes-blank-state" colspan="<?php echo absint( count( $shipping_provider_columns ) + 1 ); ?>"><p><?php esc_html_e( 'No custom shipping providers have been created.', 'woocommerce' ); ?></p></td>
+	</tr>
+</script>
+
+<script type="text/html" id="tmpl-wc-shipping-provider-configure">
+<div class="wc-backbone-modal wc-shipping-class-modal">
+		<div class="wc-backbone-modal-content" data-id="{{ data.term_id }}">
+			<section class="wc-backbone-modal-main" role="main">
+				<header class="wc-backbone-modal-header">
+					<h1><?php esc_html_e( 'Add shipping provider', 'woocommerce' ); ?></h1>
+					<button class="modal-close modal-close-link dashicons dashicons-no-alt">
+						<span class="screen-reader-text"><?php esc_html_e( 'Close modal panel', 'woocommerce' ); ?></span>
+					</button>
+				</header>
+				<article>
+				<form action="" method="post">
+					<input type="hidden" name="term_id" value="{{{ data.term_id }}}" />
+					<?php
+					foreach ( $shipping_provider_columns as $class => $heading ) {
+						echo '<div class="wc-shipping-class-modal-input ' . esc_attr( $class ) . '">';
+						switch ( $class ) {
+							case 'wc-shipping-provider-name':
+								?>
+								<div class="view">
+									<?php echo esc_html( $heading ); ?> *
+								</div>
+								<div class="edit">
+									<input type="text" name="name" data-attribute="name" value="{{ data.name }}" placeholder="<?php esc_attr_e( 'e.g. My Local Courier', 'woocommerce' ); ?>" />
+								</div>
+								<div class="wc-shipping-class-modal-help-text"><?php esc_html_e( 'The display name for this shipping provider.', 'woocommerce' ); ?></div>
+								<?php
+								break;
+							case 'wc-shipping-provider-slug':
+								?>
+								<div class="view">
+									<?php echo esc_html( $heading ); ?>
+								</div>
+								<div class="edit">
+									<input type="text" name="slug" data-attribute="slug" value="{{ data.slug }}" placeholder="<?php esc_attr_e( 'e.g. my-local-courier', 'woocommerce' ); ?>" />
+								</div>
+								<div class="wc-shipping-class-modal-help-text"><?php esc_html_e( 'Unique identifier (auto-generated if left blank).', 'woocommerce' ); ?></div>
+								<?php
+								break;
+							case 'wc-shipping-provider-tracking-url-template':
+								?>
+								<div class="view">
+									<?php echo esc_html( $heading ); ?>
+								</div>
+								<div class="edit">
+									<input type="text" name="tracking_url_template" data-attribute="tracking_url_template" value="{{ data.tracking_url_template }}" placeholder="<?php esc_attr_e( 'e.g. https://example.com/track?id=__PLACEHOLDER__', 'woocommerce' ); ?>" />
+								</div>
+								<div class="wc-shipping-class-modal-help-text"><?php esc_html_e( 'Use __PLACEHOLDER__ where the tracking number should appear in the URL.', 'woocommerce' ); ?></div>
+								<?php
+								break;
+							case 'wc-shipping-provider-icon':
+								?>
+								<div class="view">
+									<?php echo esc_html( $heading ); ?>
+								</div>
+								<div class="edit">
+									<input type="text" name="icon" data-attribute="icon" value="{{ data.icon }}" placeholder="<?php esc_attr_e( 'e.g. https://example.com/icon.png', 'woocommerce' ); ?>" />
+								</div>
+								<div class="wc-shipping-class-modal-help-text"><?php esc_html_e( 'Optional URL for the provider icon.', 'woocommerce' ); ?></div>
+								<?php
+								break;
+							default:
+								?>
+								<div class="view wc-shipping-class-hide-sibling-view">
+									<?php echo esc_html( $heading ); ?>
+								</div>
+								<?php
+								/**
+								 * Fires for custom columns in the shipping providers configure modal.
+								 *
+								 * @since 10.7.0
+								 */
+								do_action( 'woocommerce_shipping_providers_column_' . $class );
+								break;
+						}
+						echo '</div>';
+					}
+					?>
+				</form>
+				</article>
+				<footer>
+					<div class="inner">
+						<button id="btn-ok" disabled class="button button-primary button-large disabled">
+							<div class="wc-backbone-modal-action-{{ data.action === 'create' ? 'active' : 'inactive' }}"><?php esc_html_e( 'Create', 'woocommerce' ); ?></div>
+							<div class="wc-backbone-modal-action-{{ data.action === 'edit' ? 'active' : 'inactive' }}"><?php esc_html_e( 'Save', 'woocommerce' ); ?></div>
+						</button>
+					</div>
+				</footer>
+			</section>
+		</div>
+	</div>
+	<div class="wc-backbone-modal-backdrop modal-close"></div>
+</script>
+
+<script type="text/html" id="tmpl-wc-shipping-provider-row">
+	<tr data-id="{{ data.term_id }}">
+		<?php
+		foreach ( $shipping_provider_columns as $class => $heading ) {
+			echo '<td class="' . esc_attr( $class ) . '">';
+			switch ( $class ) {
+				case 'wc-shipping-provider-name':
+					?>
+					<div class="view">
+						{{ data.name }}
+					</div>
+					<?php
+					break;
+				case 'wc-shipping-provider-slug':
+					?>
+					<div class="view">{{ data.slug }}</div>
+					<?php
+					break;
+				case 'wc-shipping-provider-tracking-url-template':
+					?>
+					<div class="view">{{ data.tracking_url_template }}</div>
+					<?php
+					break;
+				case 'wc-shipping-provider-icon':
+					?>
+					<div class="view">{{ data.icon }}</div>
+					<?php
+					break;
+				default:
+					/**
+					 * Fires for custom columns in the shipping providers table row.
+					 *
+					 * @since 10.7.0
+					 */
+					do_action( 'woocommerce_shipping_providers_column_' . $class );
+					break;
+			}
+			echo '</td>';
+		}
+		?>
+		<td class="wc-shipping-zone-actions">
+			<div>
+				<a class="wc-shipping-provider-edit wc-shipping-zone-action-edit" href="#"><?php esc_html_e( 'Edit', 'woocommerce' ); ?></a> | <a href="#" class="wc-shipping-provider-delete wc-shipping-zone-actions"><?php esc_html_e( 'Delete', 'woocommerce' ); ?></a>
+			</div>
+		</td>
+	</tr>
+</script>
diff --git a/plugins/woocommerce/includes/class-wc-ajax.php b/plugins/woocommerce/includes/class-wc-ajax.php
index b68876d8782..7851b3ee695 100644
--- a/plugins/woocommerce/includes/class-wc-ajax.php
+++ b/plugins/woocommerce/includes/class-wc-ajax.php
@@ -211,6 +211,7 @@ class WC_AJAX {
 			'shipping_zone_methods_save_changes',
 			'shipping_zone_methods_save_settings',
 			'shipping_classes_save_changes',
+			'shipping_providers_save_changes',
 			'toggle_gateway_enabled',
 			'load_status_widget',
 			'load_recent_reviews_widget',
@@ -3838,6 +3839,242 @@ class WC_AJAX {
 		);
 	}

+	/**
+	 * Handle AJAX save for custom shipping providers (taxonomy-based).
+	 *
+	 * @since 10.7.0
+	 */
+	public static function shipping_providers_save_changes(): void {
+		if ( ! \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'fulfillments' ) ) {
+			wp_send_json_error( 'feature_disabled' );
+		}
+
+		if ( ! isset( $_POST['wc_shipping_providers_nonce'], $_POST['changes'] ) ) {
+			wp_send_json_error( 'missing_fields' );
+		}
+
+		if ( ! wp_verify_nonce( wp_unslash( $_POST['wc_shipping_providers_nonce'] ), 'wc_shipping_providers_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+			wp_send_json_error( 'bad_nonce' );
+		}
+
+		if ( ! current_user_can( 'manage_woocommerce' ) ) {
+			wp_send_json_error( 'missing_capabilities' );
+		}
+
+		$taxonomy = 'wc_fulfillment_shipping_provider';
+		$changes  = wp_unslash( $_POST['changes'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+
+		if ( ! is_array( $changes ) ) {
+			wp_send_json_error( 'invalid_changes' );
+		}
+
+		// Collect only built-in provider keys (class-based, not custom taxonomy providers).
+		$all_providers = \Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentUtils::get_shipping_providers();
+		$built_in_keys = array();
+		foreach ( $all_providers as $provider ) {
+			if ( is_string( $provider ) && class_exists( $provider ) && is_subclass_of( $provider, \Automattic\WooCommerce\Admin\Features\Fulfillments\Providers\AbstractShippingProvider::class ) ) {
+				try {
+					$instance        = wc_get_container()->get( $provider );
+					$built_in_keys[] = $instance->get_key();
+				} catch ( \Throwable $e ) {
+					continue;
+				}
+			}
+		}
+		$reserved_slug_error = '';
+
+		foreach ( $changes as $term_id => $data ) {
+			if ( ! is_numeric( $term_id ) && ! isset( $data['newRow'] ) ) {
+				continue;
+			}
+			$term_id = absint( $term_id );
+
+			if ( isset( $data['deleted'] ) ) {
+				if ( isset( $data['newRow'] ) ) {
+					continue;
+				}
+				$term_to_delete = get_term( $term_id, $taxonomy );
+				if ( $term_to_delete instanceof \WP_Term && self::is_shipping_provider_in_use( $term_to_delete->slug ) ) {
+					$reserved_slug_error = sprintf(
+						/* translators: %s: provider name */
+						__( 'Cannot delete "%s" because it is used by existing fulfillments. Remove all fulfillments using this provider first.', 'woocommerce' ),
+						$term_to_delete->name
+					);
+					continue;
+				}
+				$delete_result = wp_delete_term( $term_id, $taxonomy );
+				if ( is_wp_error( $delete_result ) || false === $delete_result ) {
+					$reserved_slug_error = is_wp_error( $delete_result )
+						? $delete_result->get_error_message()
+						: __( 'Failed to delete the shipping provider.', 'woocommerce' );
+				}
+				continue;
+			}
+
+			$update_args = array();
+
+			if ( isset( $data['name'] ) && is_string( $data['name'] ) ) {
+				$update_args['name'] = sanitize_text_field( $data['name'] );
+			}
+
+			// Validate and set slug only on new rows. Slug is immutable after creation.
+			if ( isset( $data['newRow'] ) && isset( $data['slug'] ) && is_string( $data['slug'] ) && '' !== $data['slug'] ) {
+				$candidate_slug = sanitize_title( $data['slug'] );
+				if ( in_array( $candidate_slug, $built_in_keys, true ) ) {
+					$reserved_slug_error = sprintf(
+						/* translators: %s: slug value */
+						__( 'The slug "%s" is already used by a built-in shipping provider. Please choose a different slug.', 'woocommerce' ),
+						$candidate_slug
+					);
+					continue;
+				}
+				$update_args['slug'] = $candidate_slug;
+			}
+
+			// Validate tracking URL template: must be a valid http/https URL.
+			// null means "not submitted" (preserve existing), empty string means "clear".
+			$tracking_url_template = null;
+			if ( isset( $data['tracking_url_template'] ) && is_string( $data['tracking_url_template'] ) ) {
+				if ( '' === $data['tracking_url_template'] ) {
+					$tracking_url_template = '';
+				} else {
+					$testable_url = str_replace( '__PLACEHOLDER__', 'test', $data['tracking_url_template'] );
+					if ( filter_var( $testable_url, FILTER_VALIDATE_URL ) && preg_match( '#^https?://#i', $testable_url ) ) {
+						$tracking_url_template = esc_url_raw( $data['tracking_url_template'], array( 'http', 'https' ) );
+					} else {
+						$reserved_slug_error = __( 'The tracking URL template must be a valid HTTP or HTTPS URL.', 'woocommerce' );
+					}
+				}
+			}
+
+			// Validate icon URL: must be a valid http/https URL.
+			$icon_url = null;
+			if ( isset( $data['icon'] ) && is_string( $data['icon'] ) ) {
+				if ( '' === $data['icon'] ) {
+					$icon_url = '';
+				} elseif ( filter_var( $data['icon'], FILTER_VALIDATE_URL ) && preg_match( '#^https?://#i', $data['icon'] ) ) {
+					$icon_url = esc_url_raw( $data['icon'], array( 'http', 'https' ) );
+				} else {
+					$reserved_slug_error = __( 'The icon URL must be a valid HTTP or HTTPS URL.', 'woocommerce' );
+				}
+			}
+
+			if ( isset( $data['newRow'] ) ) {
+				$provider_name = strval( $update_args['name'] ?? '' );
+				$update_args   = array_filter( $update_args );
+				if ( empty( $provider_name ) ) {
+					continue;
+				}
+
+				$inserted_term = wp_insert_term( $provider_name, $taxonomy, $update_args );
+				if ( is_wp_error( $inserted_term ) ) {
+					$reserved_slug_error = $inserted_term->get_error_message();
+					continue;
+				}
+				$term_id = $inserted_term['term_id'];
+
+				// Verify auto-generated slug doesn't collide with built-in keys.
+				$new_term = get_term( $term_id, $taxonomy );
+				if ( ! $new_term instanceof \WP_Term ) {
+					continue;
+				}
+				if ( in_array( $new_term->slug, $built_in_keys, true ) ) {
+					wp_delete_term( $term_id, $taxonomy );
+					$reserved_slug_error = sprintf(
+						/* translators: %s: provider name */
+						__( 'Could not create provider "%s" because its auto-generated slug conflicts with a built-in shipping provider. Please specify a different slug.', 'woocommerce' ),
+						$provider_name
+					);
+					continue;
+				}
+			} else {
+				$update_result = wp_update_term( $term_id, $taxonomy, $update_args );
+				if ( is_wp_error( $update_result ) ) {
+					$reserved_slug_error = $update_result->get_error_message();
+					continue;
+				}
+			}
+
+			if ( $term_id ) {
+				if ( null !== $tracking_url_template ) {
+					update_term_meta( $term_id, 'tracking_url_template', $tracking_url_template );
+				}
+				if ( null !== $icon_url ) {
+					update_term_meta( $term_id, 'icon', $icon_url );
+				}
+			}
+		}
+
+		$terms              = get_terms(
+			array(
+				'taxonomy'   => $taxonomy,
+				'hide_empty' => false,
+			)
+		);
+		$shipping_providers = array();
+
+		if ( ! is_wp_error( $terms ) ) {
+			foreach ( $terms as $term ) {
+				$shipping_providers[] = array(
+					'term_id'               => $term->term_id,
+					'name'                  => $term->name,
+					'slug'                  => $term->slug,
+					'tracking_url_template' => get_term_meta( $term->term_id, 'tracking_url_template', true ),
+					'icon'                  => get_term_meta( $term->term_id, 'icon', true ),
+				);
+			}
+		}
+
+		$response = array(
+			'shipping_providers' => $shipping_providers,
+		);
+
+		if ( ! empty( $reserved_slug_error ) ) {
+			$response['error'] = $reserved_slug_error;
+		}
+
+		wp_send_json_success(
+			$response
+		);
+	}
+
+	/**
+	 * Check if a shipping provider slug is referenced by any fulfillment record.
+	 *
+	 * @since 10.7.0
+	 *
+	 * @param string $provider_slug The provider slug to check.
+	 * @return bool True if the provider is in use.
+	 */
+	private static function is_shipping_provider_in_use( string $provider_slug ): bool {
+		global $wpdb;
+
+		$fulfillments_table = $wpdb->prefix . 'wc_order_fulfillments';
+		$meta_table         = $wpdb->prefix . 'wc_order_fulfillment_meta';
+
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+		$exists = $wpdb->get_var(
+			$wpdb->prepare(
+				"SELECT 1 FROM {$fulfillments_table} f
+				INNER JOIN {$meta_table} m ON f.fulfillment_id = m.fulfillment_id
+				WHERE m.meta_key = '_shipping_provider'
+				AND m.meta_value = %s
+				AND f.date_deleted IS NULL
+				AND m.date_deleted IS NULL
+				LIMIT 1",
+				$provider_slug
+			)
+		);
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+
+		// Fail safe: assume in use if the query itself failed.
+		if ( $wpdb->last_error ) {
+			return true;
+		}
+
+		return null !== $exists;
+	}
+
 	/**
 	 * Toggle payment gateway on or off via AJAX.
 	 *
diff --git a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsController.php b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsController.php
index ab27e5d7bff..3e28b733323 100644
--- a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsController.php
+++ b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsController.php
@@ -75,6 +75,9 @@ class FulfillmentsController {
 		// Create the database tables if they do not exist.
 		$this->maybe_create_db_tables();

+		// Register the custom shipping providers taxonomy.
+		$this->register_custom_shipping_providers_taxonomy();
+
 		// Register the classes that this controller provides.
 		foreach ( $this->provides as $class ) {
 			$class = $container->get( $class );
@@ -84,6 +87,37 @@ class FulfillmentsController {
 		}
 	}

+	/**
+	 * Register the custom shipping providers taxonomy.
+	 */
+	private function register_custom_shipping_providers_taxonomy(): void {
+		if ( taxonomy_exists( 'wc_fulfillment_shipping_provider' ) ) {
+			return;
+		}
+
+		register_taxonomy(
+			'wc_fulfillment_shipping_provider',
+			array(),
+			array(
+				'labels'            => array(
+					'name'          => __( 'Shipping providers', 'woocommerce' ),
+					'singular_name' => __( 'Shipping provider', 'woocommerce' ),
+					'add_new_item'  => __( 'Add new shipping provider', 'woocommerce' ),
+					'edit_item'     => __( 'Edit shipping provider', 'woocommerce' ),
+				),
+				'public'            => false,
+				'show_ui'           => false,
+				'hierarchical'      => false,
+				'show_in_rest'      => false,
+				'show_admin_column' => false, // phpcs:ignore WordPress.Arrays.MultipleStatementAlignment.DoubleArrowNotAligned
+				'show_in_nav_menus' => false, // phpcs:ignore WordPress.Arrays.MultipleStatementAlignment.DoubleArrowNotAligned
+				'show_tagcloud'     => false,
+				'query_var'         => false,
+				'rewrite'           => false,
+			)
+		);
+	}
+
 	/**
 	 * Create the database tables if they do not exist.
 	 *
diff --git a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsManager.php b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsManager.php
index abf7e6c84b6..792b3226e1d 100644
--- a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsManager.php
+++ b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsManager.php
@@ -8,6 +8,7 @@ declare( strict_types=1 );
 namespace Automattic\WooCommerce\Admin\Features\Fulfillments;

 use Automattic\WooCommerce\Admin\Features\Fulfillments\Providers\AbstractShippingProvider;
+use Automattic\WooCommerce\Admin\Features\Fulfillments\Providers\CustomShippingProvider;
 use Automattic\WooCommerce\Utilities\OrderUtil;
 use WC_Order;
 use WC_Order_Refund;
@@ -33,6 +34,7 @@ class FulfillmentsManager {
 	 */
 	public function register() {
 		add_filter( 'woocommerce_fulfillment_shipping_providers', array( $this, 'get_initial_shipping_providers' ), 10, 1 );
+		add_filter( 'woocommerce_fulfillment_shipping_providers', array( $this, 'get_custom_shipping_providers' ), 20, 1 );
 		add_filter( 'woocommerce_fulfillment_translate_meta_key', array( $this, 'translate_fulfillment_meta_key' ), 10, 1 );
 		add_filter( 'woocommerce_fulfillment_parse_tracking_number', array( $this, 'try_parse_tracking_number' ), 10, 3 );

@@ -182,6 +184,49 @@ class FulfillmentsManager {
 		return $shipping_providers;
 	}

+	/**
+	 * Load custom shipping providers from the wc_fulfillment_shipping_provider taxonomy.
+	 *
+	 * @since 10.7.0
+	 *
+	 * @param array $shipping_providers The current list of shipping providers.
+	 * @return array The modified list of shipping providers with custom providers appended.
+	 */
+	public function get_custom_shipping_providers( $shipping_providers ) {
+		if ( ! is_array( $shipping_providers ) ) {
+			$shipping_providers = array();
+		}
+
+		if ( ! taxonomy_exists( 'wc_fulfillment_shipping_provider' ) ) {
+			return $shipping_providers;
+		}
+
+		$terms = get_terms(
+			array(
+				'taxonomy'   => 'wc_fulfillment_shipping_provider',
+				'hide_empty' => false,
+			)
+		);
+
+		if ( is_wp_error( $terms ) || empty( $terms ) ) {
+			return $shipping_providers;
+		}
+
+		foreach ( $terms as $term ) {
+			$icon                  = get_term_meta( $term->term_id, 'icon', true );
+			$tracking_url_template = get_term_meta( $term->term_id, 'tracking_url_template', true );
+
+			$shipping_providers[] = new CustomShippingProvider(
+				$term->slug,
+				$term->name,
+				is_string( $icon ) ? $icon : '',
+				is_string( $tracking_url_template ) ? $tracking_url_template : ''
+			);
+		}
+
+		return $shipping_providers;
+	}
+
 	/**
 	 * Update order fulfillment status after a fulfillment is created, updated, or deleted.
 	 *
diff --git a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsRenderer.php b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsRenderer.php
index bfddf24fc85..50e71e4902e 100644
--- a/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsRenderer.php
+++ b/plugins/woocommerce/src/Admin/Features/Fulfillments/FulfillmentsRenderer.php
@@ -64,14 +64,18 @@ class FulfillmentsRenderer {
 			add_filter( 'handle_bulk_actions-woocommerce_page_wc-orders', array( $this, 'handle_fulfillment_bulk_actions' ), 10, 3 );
 			// For custom orders table, we need to filter the query to include fulfillment status.
 			add_action( 'woocommerce_order_list_table_restrict_manage_orders', array( $this, 'render_fulfillment_filters' ) );
+			add_action( 'woocommerce_order_list_table_restrict_manage_orders', array( $this, 'render_shipping_provider_filter' ) );
 			add_filter( 'woocommerce_order_query_args', array( $this, 'filter_orders_list_table_query' ), 10, 1 );
+			add_filter( 'woocommerce_order_list_table_prepare_items_query_args', array( $this, 'filter_orders_by_shipping_provider' ), 10, 1 );
 		} else {
 			// For legacy orders table, we need to add the bulk actions to the legacy orders table.
 			add_filter( 'bulk_actions-edit-shop_order', array( $this, 'define_fulfillment_bulk_actions' ) );
 			add_filter( 'handle_bulk_actions-edit-shop_order', array( $this, 'handle_fulfillment_bulk_actions' ), 10, 3 );
 			// For legacy orders table, we need to filter the query to include fulfillment status.
 			add_action( 'restrict_manage_posts', array( $this, 'render_fulfillment_filters_legacy' ) );
+			add_action( 'restrict_manage_posts', array( $this, 'render_shipping_provider_filter_legacy' ) );
 			add_action( 'pre_get_posts', array( $this, 'filter_legacy_orders_list_query' ) );
+			add_action( 'pre_get_posts', array( $this, 'filter_legacy_orders_by_shipping_provider' ) );
 		}
 	}

@@ -528,6 +532,190 @@ class FulfillmentsRenderer {
 		}
 	}

+	/**
+	 * Render the shipping provider filter dropdown in the orders table.
+	 *
+	 * @since 10.7.0
+	 */
+	public function render_shipping_provider_filter(): void {
+		if ( ! self::should_render_fulfillment_drawer() ) {
+			return;
+		}
+
+		$providers = FulfillmentUtils::get_shipping_providers_object();
+
+		// This is a read-only filter on the admin orders table, so nonce verification is not required.
+		// phpcs:ignore WordPress.Security.NonceVerification
+		$selected_provider = isset( $_GET['shipping_provider'] ) ? sanitize_text_field( wp_unslash( $_GET['shipping_provider'] ) ) : '';
+		?>
+		<select id="shipping-provider-filter" name="shipping_provider">
+			<option value="" <?php selected( $selected_provider, '' ); ?>><?php esc_html_e( 'Filter by shipping provider', 'woocommerce' ); ?></option>
+			<?php foreach ( $providers as $key => $provider ) : ?>
+				<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $selected_provider, $key ); ?>>
+					<?php echo esc_html( $provider['label'] ?? '' ); ?>
+				</option>
+			<?php endforeach; ?>
+			<option value="__other__" <?php selected( $selected_provider, '__other__' ); ?>><?php esc_html_e( 'Other', 'woocommerce' ); ?></option>
+		</select>
+		<?php
+	}
+
+	/**
+	 * Render the shipping provider filter in the legacy orders table.
+	 *
+	 * @since 10.7.0
+	 */
+	public function render_shipping_provider_filter_legacy(): void {
+		global $typenow;
+
+		if ( 'shop_order' !== $typenow ) {
+			return;
+		}
+
+		$this->render_shipping_provider_filter();
+	}
+
+	/**
+	 * Filter orders by shipping provider for the HPOS orders list.
+	 *
+	 * @since 10.7.0
+	 *
+	 * @param array $args The query arguments for the orders list.
+	 * @return array The modified query arguments.
+	 */
+	public function filter_orders_by_shipping_provider( $args ) {
+		// This is a read-only filter on the admin orders table, so nonce verification is not required.
+		// phpcs:ignore WordPress.Security.NonceVerification
+		if ( ! isset( $_GET['shipping_provider'] ) || empty( $_GET['shipping_provider'] ) ) {
+			return $args;
+		}
+
+		// phpcs:ignore WordPress.Security.NonceVerification
+		$shipping_provider = sanitize_text_field( wp_unslash( $_GET['shipping_provider'] ) );
+		$order_ids         = $this->get_order_ids_by_shipping_provider( $shipping_provider );
+
+		if ( empty( $order_ids ) ) {
+			$args['post__in'] = array( 0 );
+		} elseif ( isset( $args['post__in'] ) && is_array( $args['post__in'] ) ) {
+			$args['post__in'] = array_intersect( $args['post__in'], $order_ids );
+			if ( empty( $args['post__in'] ) ) {
+				$args['post__in'] = array( 0 );
+			}
+		} else {
+			$args['post__in'] = $order_ids;
+		}
+
+		return $args;
+	}
+
+	/**
+	 * Filter legacy orders by shipping provider.
+	 *
+	 * @since 10.7.0
+	 *
+	 * @param \WP_Query $query The WP_Query object.
+	 */
+	public function filter_legacy_orders_by_shipping_provider( $query ): void {
+		if (
+			! is_admin()
+			|| ! $query->is_main_query()
+			|| 'shop_order' !== $query->get( 'post_type' )
+			// This is a read-only filter on the admin orders table, so nonce verification is not required.
+			// phpcs:ignore WordPress.Security.NonceVerification
+			|| ! isset( $_GET['shipping_provider'] )
+			// phpcs:ignore WordPress.Security.NonceVerification
+			|| empty( $_GET['shipping_provider'] )
+		) {
+			return;
+		}
+
+		// phpcs:ignore WordPress.Security.NonceVerification
+		$shipping_provider = sanitize_text_field( wp_unslash( $_GET['shipping_provider'] ) );
+		$order_ids         = $this->get_order_ids_by_shipping_provider( $shipping_provider );
+
+		if ( empty( $order_ids ) ) {
+			$query->set( 'post__in', array( 0 ) );
+		} else {
+			$existing = $query->get( 'post__in' );
+			if ( ! empty( $existing ) && is_array( $existing ) ) {
+				$order_ids = array_intersect( $existing, $order_ids );
+				if ( empty( $order_ids ) ) {
+					$order_ids = array( 0 );
+				}
+			}
+			$query->set( 'post__in', $order_ids );
+		}
+	}
+
+	/**
+	 * Get order IDs that have fulfillments with a specific shipping provider.
+	 *
+	 * @since 10.7.0
+	 *
+	 * @param string $shipping_provider The shipping provider key to filter by.
+	 * @return array Array of order IDs.
+	 */
+	private function get_order_ids_by_shipping_provider( string $shipping_provider ): array {
+		global $wpdb;
+
+		$fulfillments_table = $wpdb->prefix . 'wc_order_fulfillments';
+		$meta_table         = $wpdb->prefix . 'wc_order_fulfillment_meta';
+
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
+		if ( '__other__' === $shipping_provider ) {
+			$known_providers = FulfillmentUtils::get_shipping_providers_object();
+			$known_keys      = array_keys( $known_providers );
+
+			if ( empty( $known_keys ) ) {
+				$results = $wpdb->get_col(
+					$wpdb->prepare(
+						"SELECT DISTINCT f.entity_id
+						FROM {$fulfillments_table} f
+						INNER JOIN {$meta_table} m ON f.fulfillment_id = m.fulfillment_id
+						WHERE m.meta_key = %s
+						AND m.meta_value IS NOT NULL
+						AND m.meta_value != ''
+						AND f.date_deleted IS NULL
+						AND m.date_deleted IS NULL",
+						'_shipping_provider'
+					)
+				);
+			} else {
+				$placeholders = implode( ',', array_fill( 0, count( $known_keys ), '%s' ) );
+				$results      = $wpdb->get_col(
+					$wpdb->prepare(
+						"SELECT DISTINCT f.entity_id
+						FROM {$fulfillments_table} f
+						INNER JOIN {$meta_table} m ON f.fulfillment_id = m.fulfillment_id
+						WHERE m.meta_key = '_shipping_provider'
+						AND m.meta_value NOT IN ({$placeholders})
+						AND m.meta_value IS NOT NULL
+						AND m.meta_value != ''
+						AND f.date_deleted IS NULL
+						AND m.date_deleted IS NULL",
+						...$known_keys
+					)
+				);
+			}
+		} else {
+			$results = $wpdb->get_col(
+				$wpdb->prepare(
+					"SELECT DISTINCT f.entity_id
+					FROM {$fulfillments_table} f
+					INNER JOIN {$meta_table} m ON f.fulfillment_id = m.fulfillment_id
+					WHERE m.meta_key = '_shipping_provider'
+					AND m.meta_value = %s
+					AND f.date_deleted IS NULL
+					AND m.date_deleted IS NULL",
+					$shipping_provider
+				)
+			);
+		}
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
+
+		return array_map( 'absint', $results );
+	}
+
 	/**
 	 * Check if the fulfillment drawer should be rendered (admin only).
 	 *
diff --git a/plugins/woocommerce/src/Admin/Features/Fulfillments/Providers/CustomShippingProvider.php b/plugins/woocommerce/src/Admin/Features/Fulfillments/Providers/CustomShippingProvider.php
new file mode 100644
index 00000000000..f8027cf1714
--- /dev/null
+++ b/plugins/woocommerce/src/Admin/Features/Fulfillments/Providers/CustomShippingProvider.php
@@ -0,0 +1,114 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Admin\Features\Fulfillments\Providers;
+
+/**
+ * Custom shipping provider loaded from the wc_fulfillment_shipping_provider taxonomy.
+ *
+ * Unlike built-in providers, custom providers do not support automatic tracking
+ * number parsing; they rely on user-supplied tracking URL templates.
+ *
+ * @since 10.7.0
+ */
+class CustomShippingProvider extends AbstractShippingProvider {
+
+	/**
+	 * The provider key (taxonomy term slug).
+	 *
+	 * @var string
+	 */
+	private string $key;
+
+	/**
+	 * The provider display name.
+	 *
+	 * @var string
+	 */
+	private string $name;
+
+	/**
+	 * The provider icon URL.
+	 *
+	 * @var string
+	 */
+	private string $icon;
+
+	/**
+	 * The tracking URL template containing __PLACEHOLDER__ for the tracking number.
+	 *
+	 * @var string
+	 */
+	private string $tracking_url_template;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param string $key                  The provider key (term slug).
+	 * @param string $name                 The provider display name.
+	 * @param string $icon                 The provider icon URL.
+	 * @param string $tracking_url_template The tracking URL template.
+	 */
+	public function __construct( string $key, string $name, string $icon, string $tracking_url_template ) {
+		$this->key                   = $key;
+		$this->name                  = $name;
+		$this->icon                  = $icon;
+		$this->tracking_url_template = $tracking_url_template;
+	}
+
+	/**
+	 * Get the key of the shipping provider.
+	 *
+	 * @return string
+	 */
+	public function get_key(): string {
+		return $this->key;
+	}
+
+	/**
+	 * Get the name of the shipping provider.
+	 *
+	 * @return string
+	 */
+	public function get_name(): string {
+		return $this->name;
+	}
+
+	/**
+	 * Get the icon URL of the shipping provider.
+	 *
+	 * @return string
+	 */
+	public function get_icon(): string {
+		return $this->icon;
+	}
+
+	/**
+	 * Get the tracking URL for a given tracking number.
+	 *
+	 * Replaces __PLACEHOLDER__ in the template with the actual tracking number.
+	 *
+	 * @param string $tracking_number The tracking number.
+	 * @return string The tracking URL with the placeholder replaced.
+	 */
+	public function get_tracking_url( string $tracking_number ): string {
+		if ( empty( $this->tracking_url_template ) ) {
+			return '';
+		}
+
+		return str_replace( '__PLACEHOLDER__', rawurlencode( $tracking_number ), $this->tracking_url_template );
+	}
+
+	/**
+	 * Custom providers do not support automatic tracking number parsing.
+	 *
+	 * @param string $tracking_number The tracking number.
+	 * @param string $shipping_from The country code from which the shipment is sent.
+	 * @param string $shipping_to The country code to which the shipment is sent.
+	 * @return array|null Always returns null for custom providers.
+	 *
+	 * phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter
+	 */
+	public function try_parse_tracking_number( string $tracking_number, string $shipping_from, string $shipping_to ): ?array {
+		return null;
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentsManagerTest.php b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentsManagerTest.php
index 7766666d6c2..4ddfbaff2e5 100644
--- a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentsManagerTest.php
+++ b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentsManagerTest.php
@@ -348,6 +348,58 @@ class FulfillmentsManagerTest extends \WC_Unit_Test_Case {
 		$this->assertSame( '0', $fulfillments_after, 'Fulfillments should be deleted after order deletion' );
 	}

+	/**
+	 * @testdox Should register the custom shipping providers filter hook.
+	 */
+	public function test_custom_shipping_providers_hook_registered(): void {
+		$this->assertNotFalse( has_filter( 'woocommerce_fulfillment_shipping_providers', array( $this->manager, 'get_custom_shipping_providers' ) ) );
+	}
+
+	/**
+	 * @testdox Should load custom shipping providers from the taxonomy into the providers list.
+	 */
+	public function test_get_custom_shipping_providers_loads_taxonomy_terms(): void {
+		if ( ! taxonomy_exists( 'wc_fulfillment_shipping_provider' ) ) {
+			register_taxonomy( 'wc_fulfillment_shipping_provider', array() );
+		}
+
+		$term = wp_insert_term( 'Test Custom Provider', 'wc_fulfillment_shipping_provider', array( 'slug' => 'test-custom-provider' ) );
+		$this->assertNotWPError( $term );
+
+		update_term_meta( $term['term_id'], 'tracking_url_template', 'https://example.com/track?id=__PLACEHOLDER__' );
+		update_term_meta( $term['term_id'], 'icon', 'https://example.com/icon.png' );
+
+		$providers = $this->manager->get_custom_shipping_providers( array() );
+
+		$this->assertNotEmpty( $providers );
+
+		$found = false;
+		foreach ( $providers as $provider ) {
+			if ( $provider instanceof \Automattic\WooCommerce\Admin\Features\Fulfillments\Providers\CustomShippingProvider && 'test-custom-provider' === $provider->get_key() ) {
+				$found = true;
+				$this->assertSame( 'Test Custom Provider', $provider->get_name() );
+				$this->assertSame( 'https://example.com/icon.png', $provider->get_icon() );
+				$this->assertSame( 'https://example.com/track?id=ABC123', $provider->get_tracking_url( 'ABC123' ) );
+				break;
+			}
+		}
+
+		$this->assertTrue( $found, 'Custom provider should be loaded from taxonomy' );
+
+		wp_delete_term( $term['term_id'], 'wc_fulfillment_shipping_provider' );
+	}
+
+	/**
+	 * @testdox Should return existing providers when no custom providers exist.
+	 */
+	public function test_get_custom_shipping_providers_returns_existing_when_no_terms(): void {
+		$existing = array( 'some_provider' );
+
+		$result = $this->manager->get_custom_shipping_providers( $existing );
+
+		$this->assertContains( 'some_provider', $result );
+	}
+
 	/**
 	 * Test tracking number parsing without any shipping providers.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/Providers/CustomShippingProviderTest.php b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/Providers/CustomShippingProviderTest.php
new file mode 100644
index 00000000000..a6cf7d272b3
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/Providers/CustomShippingProviderTest.php
@@ -0,0 +1,100 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Admin\Features\Fulfillments\Providers;
+
+use Automattic\WooCommerce\Admin\Features\Fulfillments\Providers\CustomShippingProvider;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the CustomShippingProvider class.
+ */
+class CustomShippingProviderTest extends WC_Unit_Test_Case {
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var CustomShippingProvider
+	 */
+	private $sut;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+		$this->sut = new CustomShippingProvider(
+			'my-courier',
+			'My Local Courier',
+			'https://example.com/icon.png',
+			'https://example.com/track?id=__PLACEHOLDER__'
+		);
+	}
+
+	/**
+	 * @testdox Should return the correct provider key.
+	 */
+	public function test_get_key(): void {
+		$this->assertSame( 'my-courier', $this->sut->get_key() );
+	}
+
+	/**
+	 * @testdox Should return the correct provider name.
+	 */
+	public function test_get_name(): void {
+		$this->assertSame( 'My Local Courier', $this->sut->get_name() );
+	}
+
+	/**
+	 * @testdox Should return the correct icon URL.
+	 */
+	public function test_get_icon(): void {
+		$this->assertSame( 'https://example.com/icon.png', $this->sut->get_icon() );
+	}
+
+	/**
+	 * @testdox Should replace __PLACEHOLDER__ in tracking URL template with the tracking number.
+	 */
+	public function test_get_tracking_url_replaces_placeholder(): void {
+		$result = $this->sut->get_tracking_url( 'ABC123' );
+
+		$this->assertSame( 'https://example.com/track?id=ABC123', $result );
+	}
+
+	/**
+	 * @testdox Should URL-encode the tracking number in the tracking URL.
+	 */
+	public function test_get_tracking_url_encodes_tracking_number(): void {
+		$result = $this->sut->get_tracking_url( 'ABC 123&test' );
+
+		$this->assertSame( 'https://example.com/track?id=ABC%20123%26test', $result );
+	}
+
+	/**
+	 * @testdox Should return empty string when tracking URL template is empty.
+	 */
+	public function test_get_tracking_url_returns_empty_when_no_template(): void {
+		$provider = new CustomShippingProvider( 'test', 'Test', '', '' );
+
+		$result = $provider->get_tracking_url( 'ABC123' );
+
+		$this->assertSame( '', $result );
+	}
+
+	/**
+	 * @testdox Should always return null for try_parse_tracking_number.
+	 */
+	public function test_try_parse_tracking_number_returns_null(): void {
+		$result = $this->sut->try_parse_tracking_number( 'ABC123', 'US', 'CA' );
+
+		$this->assertNull( $result );
+	}
+
+	/**
+	 * @testdox Should return empty arrays for shipping country methods.
+	 */
+	public function test_shipping_countries_return_empty_arrays(): void {
+		$this->assertSame( array(), $this->sut->get_shipping_from_countries() );
+		$this->assertSame( array(), $this->sut->get_shipping_to_countries() );
+	}
+}