Commit ac751fb1d20 for woocommerce

commit ac751fb1d20f109dd04cdd823021062e66d03bff
Author: Faisal Ahammad <faisalahammad24@gmail.com>
Date:   Fri Oct 16 20:32:53 2026 +0600

    docs: add REST API docs for order fulfillments (#66281)

    * docs: add REST API docs for order fulfillments

    - Add docs/apis/rest-api/v3/order-fulfillments.mdx covering all 9
      fulfillment endpoints (list/create/get/update/delete fulfillment,
      metadata get/update/delete, tracking number lookup)
    - Note the endpoints only exist when the fulfillments feature flag
      (woocommerce_feature_fulfillments_enabled) is on
    - Bump sidebar_position on the following v3 docs pages by one to make
      room for the new page right after Order Refunds

    Fixes #59422

    * Fix fulfillments REST API docs review feedback

    Wrap metadata update payloads in meta_data, add the _items entry to
    the retrieve example, and document the tracking lookup no-match and
    over-50-character responses.

    * docs: clarify tracking number errors in fulfillments

    Split the tracking number lookup error note into two cases: a missing parameter returns 400 rest_missing_callback_param before the endpoint runs, and a supplied but empty value returns 400 woocommerce_rest_tracking_number_missing. Update the error code list to match.

    Also include the retained _items entry in the delete metadata response example so it matches the endpoint response.

    Addresses CodeRabbit review feedback.
    Refs #66281

    * docs: fix fulfillment qty type and _items note

    - Document `_items[].qty` as a positive number, since the data store accepts int or float values.
    - Clarify that the metadata update and delete endpoints cannot remove `_items`.

    Addresses PR feedback.

    Refs #66281

    * docs: document fulfillment write request contract

    Add a note to the Order Fulfillments REST API page. On create and update, only status, is_fulfilled and meta_data from the body are applied. The order ID and fulfillment ID come from the URL, and a body id, entity_type or entity_id is ignored.

diff --git a/docs/apis/rest-api/v3/data.mdx b/docs/apis/rest-api/v3/data.mdx
index 4d9421ddaf8..c7622d39e25 100644
--- a/docs/apis/rest-api/v3/data.mdx
+++ b/docs/apis/rest-api/v3/data.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 30
+sidebar_position: 31
 sidebar_label: 'Data'
 ---

diff --git a/docs/apis/rest-api/v3/order-fulfillments.mdx b/docs/apis/rest-api/v3/order-fulfillments.mdx
new file mode 100644
index 00000000000..da9549f68c0
--- /dev/null
+++ b/docs/apis/rest-api/v3/order-fulfillments.mdx
@@ -0,0 +1,1074 @@
+---
+sidebar_position: 7
+sidebar_label: 'Order Fulfillments'
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Order fulfillments
+
+The order fulfillments API allows you to create, view, update, and delete fulfillments for an order, along with their metadata (tracking numbers, shipment provider, and any custom data you want to attach).
+
+:::info
+
+These endpoints are only available when the **Order Fulfillments** feature is enabled (`woocommerce_feature_fulfillments_enabled` option). If the feature is disabled, these routes are not registered and requests to them will return a `404 rest_no_route` error.
+
+:::
+
+## Order fulfillment properties
+
+| Attribute      | Type      | Description                                                                                                                       |
+| -------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------- |
+| `id`           | integer   | Unique identifier for the fulfillment. `READ-ONLY`                                                                 |
+| `entity_type`  | string    | The type of entity this fulfillment belongs to. Currently always `WC_Order`. `READ-ONLY`                          |
+| `entity_id`    | string    | The ID of the entity this fulfillment belongs to, i.e. the order ID. `READ-ONLY`                                   |
+| `status`       | string    | Status of the fulfillment, e.g. `unfulfilled` or `fulfilled`. Custom statuses can also be used. Default is `unfulfilled`.           |
+| `is_fulfilled` | boolean   | Whether the fulfillment is fulfilled. Default is `false`.                                                                          |
+| `date_updated` | date-time | The date the fulfillment was last updated, as GMT, in ISO 8601 format (e.g. `2024-01-15T11:45:00Z`). `READ-ONLY`   |
+| `date_deleted` | date-time | The date the fulfillment was deleted, as GMT, or `null` if it hasn't been deleted. `READ-ONLY`                     |
+| `meta_data`    | array     | Meta data. See [Order fulfillment - Meta data properties](#order-fulfillment---meta-data-properties)                                |
+
+The following parameters are also accepted when creating or updating a fulfillment, but are not part of the stored resource:
+
+| Attribute         | Type    | Description                                                                                                                            |
+| ----------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
+| `notify_customer` | boolean | Whether to notify the customer about this change. Default is `false`. `WRITE-ONLY`                                     |
+| `customer_note`   | string  | An optional note from the merchant, included in the update notification email. Only used when updating a fulfillment. `WRITE-ONLY` |
+
+:::note
+
+When you create or update a fulfillment, only `status`, `is_fulfilled`, and `meta_data` from the JSON body are applied to the fulfillment. The order ID comes from the URL path. On update, the fulfillment ID also comes from the URL path. On create, the server assigns it. A top-level `id`, `entity_type`, or `entity_id` in the body is silently ignored, so a request body cannot change which order or fulfillment is written to. These three fields are still returned in responses. `notify_customer` and `customer_note` (update only) are request parameters that control notifications. They are not stored on the fulfillment.
+
+:::
+
+### Order fulfillment - Meta data properties
+
+| Attribute | Type    | Description                                                                     |
+| --------- | ------- | -------------------------------------------------------------------------------- |
+| `id`      | integer | Meta ID. `0` for entries that haven't been saved yet. `READ-ONLY` |
+| `key`     | string  | Meta key.                                                                       |
+| `value`   | mixed   | Meta value. Can be a string, number, array, or object.                         |
+
+The `_items` meta key is required and must be an array of objects describing which order line items are being fulfilled:
+
+```json
+[
+	{
+		"item_id": 456,
+		"qty": 2
+	}
+]
+```
+
+| Field     | Type    | Description                                    |
+| --------- | ------- | ---------------------------------------------- |
+| `item_id` | integer | The order line item ID.                        |
+| `qty`     | number  | Positive quantity of the item being fulfilled. |
+
+Other common (optional) meta keys: `_tracking_number`, `_shipment_provider`, `_tracking_url`. Meta keys prefixed with an underscore are private and only shown to merchants and in customer emails; unprefixed keys are also shown to merchants and customers alongside the fulfillment.
+
+## List fulfillments for an order
+
+```http
+GET /wp-json/wc/v3/orders/<id>/fulfillments
+```
+
+<Tabs>
+  <TabItem value="curl" label="cURL">
+
+```shell
+curl https://example.com/wp-json/wc/v3/orders/723/fulfillments \
+	-u consumer_key:consumer_secret
+```
+
+  </TabItem>
+  <TabItem value="js" label="JavaScript">
+
+```javascript
+WooCommerce.get( 'orders/723/fulfillments' )
+	.then( ( response ) => {
+		console.log( response.data );
+	} )
+	.catch( ( error ) => {
+		console.log( error.response.data );
+	} );
+```
+
+  </TabItem>
+  <TabItem value="php" label="PHP">
+
+```php
+<?php print_r($woocommerce->get('orders/723/fulfillments')); ?>
+```
+
+  </TabItem>
+  <TabItem value="python" label="Python">
+
+```python
+print(wcapi.get("orders/723/fulfillments").json())
+```
+
+  </TabItem>
+  <TabItem value="ruby" label="Ruby">
+
+```ruby
+woocommerce.get("orders/723/fulfillments").parsed_response
+```
+
+  </TabItem>
+  <TabItem value="response" label="JSON Response">
+
+```json
+[
+	{
+		"id": 1,
+		"entity_type": "WC_Order",
+		"entity_id": "723",
+		"status": "fulfilled",
+		"is_fulfilled": true,
+		"date_updated": "2024-01-15T10:30:00Z",
+		"date_deleted": null,
+		"meta_data": [
+			{
+				"id": 1,
+				"key": "_items",
+				"value": [
+					{ "item_id": 456, "qty": 2 }
+				]
+			},
+			{
+				"id": 2,
+				"key": "_tracking_number",
+				"value": "1Z999AA1234567890"
+			},
+			{
+				"id": 3,
+				"key": "_shipment_provider",
+				"value": "ups"
+			}
+		]
+	}
+]
+```
+
+  </TabItem>
+</Tabs>
+
+## Create a fulfillment
+
+```http
+POST /wp-json/wc/v3/orders/<id>/fulfillments
+```
+
+<Tabs>
+  <TabItem value="curl" label="cURL">
+
+```shell
+curl -X POST "https://example.com/wp-json/wc/v3/orders/723/fulfillments?notify_customer=true" \
+	-u consumer_key:consumer_secret \
+	-H "Content-Type: application/json" \
+	-d '{
+  "status": "fulfilled",
+  "is_fulfilled": true,
+  "meta_data": [
+    {
+      "key": "_items",
+      "value": [
+        { "item_id": 456, "qty": 2 }
+      ]
+    },
+    {
+      "key": "_tracking_number",
+      "value": "1Z999AA1234567890"
+    },
+    {
+      "key": "_shipment_provider",
+      "value": "ups"
+    }
+  ]
+}'
+```
+
+  </TabItem>
+  <TabItem value="js" label="JavaScript">
+
+```javascript
+const data = {
+	status: 'fulfilled',
+	is_fulfilled: true,
+	meta_data: [
+		{
+			key: '_items',
+			value: [ { item_id: 456, qty: 2 } ],
+		},
+		{
+			key: '_tracking_number',
+			value: '1Z999AA1234567890',
+		},
+		{
+			key: '_shipment_provider',
+			value: 'ups',
+		},
+	],
+};
+
+WooCommerce.post( 'orders/723/fulfillments?notify_customer=true', data )
+	.then( ( response ) => {
+		console.log( response.data );
+	} )
+	.catch( ( error ) => {
+		console.log( error.response.data );
+	} );
+```
+
+  </TabItem>
+  <TabItem value="php" label="PHP">
+
+```php
+<?php
+$data = [
+    'status' => 'fulfilled',
+    'is_fulfilled' => true,
+    'meta_data' => [
+        [ 'key' => '_items', 'value' => [ [ 'item_id' => 456, 'qty' => 2 ] ] ],
+        [ 'key' => '_tracking_number', 'value' => '1Z999AA1234567890' ],
+        [ 'key' => '_shipment_provider', 'value' => 'ups' ],
+    ],
+];
+
+print_r($woocommerce->post('orders/723/fulfillments?notify_customer=true', $data));
+?>
+```
+
+  </TabItem>
+  <TabItem value="python" label="Python">
+
+```python
+data = {
+    "status": "fulfilled",
+    "is_fulfilled": True,
+    "meta_data": [
+        {"key": "_items", "value": [{"item_id": 456, "qty": 2}]},
+        {"key": "_tracking_number", "value": "1Z999AA1234567890"},
+        {"key": "_shipment_provider", "value": "ups"},
+    ],
+}
+
+print(wcapi.post("orders/723/fulfillments?notify_customer=true", data).json())
+```
+
+  </TabItem>
+  <TabItem value="ruby" label="Ruby">
+
+```ruby
+data = {
+  status: "fulfilled",
+  is_fulfilled: true,
+  meta_data: [
+    { key: "_items", value: [ { item_id: 456, qty: 2 } ] },
+    { key: "_tracking_number", value: "1Z999AA1234567890" },
+    { key: "_shipment_provider", value: "ups" }
+  ]
+}
+
+woocommerce.post("orders/723/fulfillments?notify_customer=true", data).parsed_response
+```
+
+  </TabItem>
+  <TabItem value="response" label="JSON Response">
+
+```json
+{
+	"id": 2,
+	"entity_type": "WC_Order",
+	"entity_id": "723",
+	"status": "fulfilled",
+	"is_fulfilled": true,
+	"date_updated": "2024-01-15T11:45:00Z",
+	"date_deleted": null,
+	"meta_data": [
+		{
+			"id": 3,
+			"key": "_items",
+			"value": [
+				{ "item_id": 456, "qty": 2 }
+			]
+		},
+		{
+			"id": 4,
+			"key": "_tracking_number",
+			"value": "1Z999AA1234567890"
+		},
+		{
+			"id": 5,
+			"key": "_shipment_provider",
+			"value": "ups"
+		}
+	]
+}
+```
+
+  </TabItem>
+</Tabs>
+
+Setting `notify_customer=true` when the fulfillment is created as fulfilled triggers the `woocommerce_fulfillment_created_notification` action, which sends the default fulfillment email.
+
+## Retrieve a fulfillment
+
+```http
+GET /wp-json/wc/v3/orders/<id>/fulfillments/<fulfillment_id>
+```
+
+<Tabs>
+  <TabItem value="curl" label="cURL">
+
+```shell
+curl https://example.com/wp-json/wc/v3/orders/723/fulfillments/2 \
+	-u consumer_key:consumer_secret
+```
+
+  </TabItem>
+  <TabItem value="js" label="JavaScript">
+
+```javascript
+WooCommerce.get( 'orders/723/fulfillments/2' )
+	.then( ( response ) => {
+		console.log( response.data );
+	} )
+	.catch( ( error ) => {
+		console.log( error.response.data );
+	} );
+```
+
+  </TabItem>
+  <TabItem value="php" label="PHP">
+
+```php
+<?php print_r($woocommerce->get('orders/723/fulfillments/2')); ?>
+```
+
+  </TabItem>
+  <TabItem value="python" label="Python">
+
+```python
+print(wcapi.get("orders/723/fulfillments/2").json())
+```
+
+  </TabItem>
+  <TabItem value="ruby" label="Ruby">
+
+```ruby
+woocommerce.get("orders/723/fulfillments/2").parsed_response
+```
+
+  </TabItem>
+  <TabItem value="response" label="JSON Response">
+
+```json
+{
+	"id": 2,
+	"entity_type": "WC_Order",
+	"entity_id": "723",
+	"status": "fulfilled",
+	"is_fulfilled": true,
+	"date_updated": "2024-01-15T11:45:00Z",
+	"date_deleted": null,
+	"meta_data": [
+		{
+			"id": 3,
+			"key": "_items",
+			"value": [
+				{ "item_id": 456, "qty": 2 }
+			]
+		},
+		{
+			"id": 4,
+			"key": "_tracking_number",
+			"value": "1Z999AA1234567890"
+		},
+		{
+			"id": 5,
+			"key": "_shipment_provider",
+			"value": "ups"
+		}
+	]
+}
+```
+
+  </TabItem>
+</Tabs>
+
+## Update a fulfillment
+
+Any `meta_data` entries not included in the request are removed from the fulfillment, so send the full set of metadata you want the fulfillment to have.
+
+```http
+PUT /wp-json/wc/v3/orders/<id>/fulfillments/<fulfillment_id>
+```
+
+<Tabs>
+  <TabItem value="curl" label="cURL">
+
+```shell
+curl -X PUT "https://example.com/wp-json/wc/v3/orders/723/fulfillments/2?notify_customer=true" \
+	-u consumer_key:consumer_secret \
+	-H "Content-Type: application/json" \
+	-d '{
+  "status": "fulfilled",
+  "is_fulfilled": true,
+  "customer_note": "Your package is on its way!",
+  "meta_data": [
+    {
+      "key": "_items",
+      "value": [
+        { "item_id": 456, "qty": 2 }
+      ]
+    },
+    {
+      "key": "_tracking_number",
+      "value": "1Z999AA1234567890"
+    },
+    {
+      "key": "_shipment_provider",
+      "value": "ups"
+    },
+    {
+      "key": "_delivery_date",
+      "value": "2024-01-16"
+    }
+  ]
+}'
+```
+
+  </TabItem>
+  <TabItem value="js" label="JavaScript">
+
+```javascript
+const data = {
+	status: 'fulfilled',
+	is_fulfilled: true,
+	customer_note: 'Your package is on its way!',
+	meta_data: [
+		{
+			key: '_items',
+			value: [ { item_id: 456, qty: 2 } ],
+		},
+		{
+			key: '_tracking_number',
+			value: '1Z999AA1234567890',
+		},
+		{
+			key: '_shipment_provider',
+			value: 'ups',
+		},
+		{
+			key: '_delivery_date',
+			value: '2024-01-16',
+		},
+	],
+};
+
+WooCommerce.put( 'orders/723/fulfillments/2?notify_customer=true', data )
+	.then( ( response ) => {
+		console.log( response.data );
+	} )
+	.catch( ( error ) => {
+		console.log( error.response.data );
+	} );
+```
+
+  </TabItem>
+  <TabItem value="php" label="PHP">
+
+```php
+<?php
+$data = [
+    'status' => 'fulfilled',
+    'is_fulfilled' => true,
+    'customer_note' => 'Your package is on its way!',
+    'meta_data' => [
+        [ 'key' => '_items', 'value' => [ [ 'item_id' => 456, 'qty' => 2 ] ] ],
+        [ 'key' => '_tracking_number', 'value' => '1Z999AA1234567890' ],
+        [ 'key' => '_shipment_provider', 'value' => 'ups' ],
+        [ 'key' => '_delivery_date', 'value' => '2024-01-16' ],
+    ],
+];
+
+print_r($woocommerce->put('orders/723/fulfillments/2?notify_customer=true', $data));
+?>
+```
+
+  </TabItem>
+  <TabItem value="python" label="Python">
+
+```python
+data = {
+    "status": "fulfilled",
+    "is_fulfilled": True,
+    "customer_note": "Your package is on its way!",
+    "meta_data": [
+        {"key": "_items", "value": [{"item_id": 456, "qty": 2}]},
+        {"key": "_tracking_number", "value": "1Z999AA1234567890"},
+        {"key": "_shipment_provider", "value": "ups"},
+        {"key": "_delivery_date", "value": "2024-01-16"},
+    ],
+}
+
+print(wcapi.put("orders/723/fulfillments/2?notify_customer=true", data).json())
+```
+
+  </TabItem>
+  <TabItem value="ruby" label="Ruby">
+
+```ruby
+data = {
+  status: "fulfilled",
+  is_fulfilled: true,
+  customer_note: "Your package is on its way!",
+  meta_data: [
+    { key: "_items", value: [ { item_id: 456, qty: 2 } ] },
+    { key: "_tracking_number", value: "1Z999AA1234567890" },
+    { key: "_shipment_provider", value: "ups" },
+    { key: "_delivery_date", value: "2024-01-16" }
+  ]
+}
+
+woocommerce.put("orders/723/fulfillments/2?notify_customer=true", data).parsed_response
+```
+
+  </TabItem>
+  <TabItem value="response" label="JSON Response">
+
+```json
+{
+	"id": 2,
+	"entity_type": "WC_Order",
+	"entity_id": "723",
+	"status": "fulfilled",
+	"is_fulfilled": true,
+	"date_updated": "2024-01-16T09:30:00Z",
+	"date_deleted": null,
+	"meta_data": [
+		{
+			"id": 3,
+			"key": "_items",
+			"value": [
+				{ "item_id": 456, "qty": 2 }
+			]
+		},
+		{
+			"id": 4,
+			"key": "_tracking_number",
+			"value": "1Z999AA1234567890"
+		},
+		{
+			"id": 5,
+			"key": "_shipment_provider",
+			"value": "ups"
+		},
+		{
+			"id": 6,
+			"key": "_delivery_date",
+			"value": "2024-01-16"
+		}
+	]
+}
+```
+
+  </TabItem>
+</Tabs>
+
+`notify_customer=true` triggers `woocommerce_fulfillment_created_notification` if the fulfillment is being marked as fulfilled for the first time, or `woocommerce_fulfillment_updated_notification` for subsequent updates to an already-fulfilled fulfillment. `customer_note` is only used for the update notification email.
+
+## Delete a fulfillment
+
+```http
+DELETE /wp-json/wc/v3/orders/<id>/fulfillments/<fulfillment_id>
+```
+
+<Tabs>
+  <TabItem value="curl" label="cURL">
+
+```shell
+curl -X DELETE "https://example.com/wp-json/wc/v3/orders/723/fulfillments/2?notify_customer=true" \
+	-u consumer_key:consumer_secret
+```
+
+  </TabItem>
+  <TabItem value="js" label="JavaScript">
+
+```javascript
+WooCommerce.delete( 'orders/723/fulfillments/2', {
+	notify_customer: true,
+} )
+	.then( ( response ) => {
+		console.log( response.data );
+	} )
+	.catch( ( error ) => {
+		console.log( error.response.data );
+	} );
+```
+
+  </TabItem>
+  <TabItem value="php" label="PHP">
+
+```php
+<?php print_r($woocommerce->delete('orders/723/fulfillments/2', ['notify_customer' => true])); ?>
+```
+
+  </TabItem>
+  <TabItem value="python" label="Python">
+
+```python
+print(wcapi.delete("orders/723/fulfillments/2", params={"notify_customer": True}).json())
+```
+
+  </TabItem>
+  <TabItem value="ruby" label="Ruby">
+
+```ruby
+woocommerce.delete("orders/723/fulfillments/2", notify_customer: true).parsed_response
+```
+
+  </TabItem>
+  <TabItem value="response" label="JSON Response">
+
+```json
+{
+	"message": "Fulfillment deleted successfully."
+}
+```
+
+  </TabItem>
+</Tabs>
+
+Deleting a fulfilled fulfillment with `notify_customer=true` triggers the `woocommerce_fulfillment_deleted_notification` action.
+
+## Get fulfillment metadata
+
+```http
+GET /wp-json/wc/v3/orders/<id>/fulfillments/<fulfillment_id>/metadata
+```
+
+<Tabs>
+  <TabItem value="curl" label="cURL">
+
+```shell
+curl https://example.com/wp-json/wc/v3/orders/723/fulfillments/2/metadata \
+	-u consumer_key:consumer_secret
+```
+
+  </TabItem>
+  <TabItem value="js" label="JavaScript">
+
+```javascript
+WooCommerce.get( 'orders/723/fulfillments/2/metadata' )
+	.then( ( response ) => {
+		console.log( response.data );
+	} )
+	.catch( ( error ) => {
+		console.log( error.response.data );
+	} );
+```
+
+  </TabItem>
+  <TabItem value="php" label="PHP">
+
+```php
+<?php print_r($woocommerce->get('orders/723/fulfillments/2/metadata')); ?>
+```
+
+  </TabItem>
+  <TabItem value="python" label="Python">
+
+```python
+print(wcapi.get("orders/723/fulfillments/2/metadata").json())
+```
+
+  </TabItem>
+  <TabItem value="ruby" label="Ruby">
+
+```ruby
+woocommerce.get("orders/723/fulfillments/2/metadata").parsed_response
+```
+
+  </TabItem>
+  <TabItem value="response" label="JSON Response">
+
+```json
+[
+	{
+		"id": 3,
+		"key": "_items",
+		"value": [
+			{ "item_id": 456, "qty": 2 }
+		]
+	},
+	{
+		"id": 4,
+		"key": "_tracking_number",
+		"value": "1Z999AA1234567890"
+	},
+	{
+		"id": 5,
+		"key": "_shipment_provider",
+		"value": "ups"
+	}
+]
+```
+
+  </TabItem>
+</Tabs>
+
+## Update fulfillment metadata
+
+Like updating a fulfillment, this replaces the full metadata set: any existing meta keys not present in the request body are removed.
+
+The metadata must be wrapped in an object under the `meta_data` key. The endpoint requires the `meta_data` parameter, so sending a bare array returns a `400 rest_missing_callback_param` error. Because the request replaces the full set, keep `_items` in the payload. The metadata update and delete endpoints cannot remove `_items`; saving without it fails with "The fulfillment should contain at least one item."
+
+```http
+PUT /wp-json/wc/v3/orders/<id>/fulfillments/<fulfillment_id>/metadata
+```
+
+<Tabs>
+  <TabItem value="curl" label="cURL">
+
+```shell
+curl -X PUT https://example.com/wp-json/wc/v3/orders/723/fulfillments/2/metadata \
+	-u consumer_key:consumer_secret \
+	-H "Content-Type: application/json" \
+	-d '{
+  "meta_data": [
+    {
+      "key": "_items",
+      "value": [
+        { "item_id": 456, "qty": 2 }
+      ]
+    },
+    {
+      "key": "_tracking_number",
+      "value": "1Z999AA9876543210"
+    },
+    {
+      "key": "_shipment_provider",
+      "value": "fedex"
+    }
+  ]
+}'
+```
+
+  </TabItem>
+  <TabItem value="js" label="JavaScript">
+
+```javascript
+const data = {
+	meta_data: [
+		{
+			key: '_items',
+			value: [ { item_id: 456, qty: 2 } ],
+		},
+		{ key: '_tracking_number', value: '1Z999AA9876543210' },
+		{ key: '_shipment_provider', value: 'fedex' },
+	],
+};
+
+WooCommerce.put( 'orders/723/fulfillments/2/metadata', data )
+	.then( ( response ) => {
+		console.log( response.data );
+	} )
+	.catch( ( error ) => {
+		console.log( error.response.data );
+	} );
+```
+
+  </TabItem>
+  <TabItem value="php" label="PHP">
+
+```php
+<?php
+$data = [
+    'meta_data' => [
+        [ 'key' => '_items', 'value' => [ [ 'item_id' => 456, 'qty' => 2 ] ] ],
+        [ 'key' => '_tracking_number', 'value' => '1Z999AA9876543210' ],
+        [ 'key' => '_shipment_provider', 'value' => 'fedex' ],
+    ],
+];
+
+print_r($woocommerce->put('orders/723/fulfillments/2/metadata', $data));
+?>
+```
+
+  </TabItem>
+  <TabItem value="python" label="Python">
+
+```python
+data = {
+    "meta_data": [
+        {"key": "_items", "value": [{"item_id": 456, "qty": 2}]},
+        {"key": "_tracking_number", "value": "1Z999AA9876543210"},
+        {"key": "_shipment_provider", "value": "fedex"},
+    ]
+}
+
+print(wcapi.put("orders/723/fulfillments/2/metadata", data).json())
+```
+
+  </TabItem>
+  <TabItem value="ruby" label="Ruby">
+
+```ruby
+data = {
+  meta_data: [
+    { key: "_items", value: [ { item_id: 456, qty: 2 } ] },
+    { key: "_tracking_number", value: "1Z999AA9876543210" },
+    { key: "_shipment_provider", value: "fedex" }
+  ]
+}
+
+woocommerce.put("orders/723/fulfillments/2/metadata", data).parsed_response
+```
+
+  </TabItem>
+  <TabItem value="response" label="JSON Response">
+
+```json
+[
+	{
+		"id": 3,
+		"key": "_items",
+		"value": [
+			{ "item_id": 456, "qty": 2 }
+		]
+	},
+	{
+		"id": 4,
+		"key": "_tracking_number",
+		"value": "1Z999AA9876543210"
+	},
+	{
+		"id": 5,
+		"key": "_shipment_provider",
+		"value": "fedex"
+	}
+]
+```
+
+  </TabItem>
+</Tabs>
+
+## Delete fulfillment metadata
+
+Deletes a single metadata entry by key and returns the fulfillment's remaining metadata.
+
+```http
+DELETE /wp-json/wc/v3/orders/<id>/fulfillments/<fulfillment_id>/metadata
+```
+
+<Tabs>
+  <TabItem value="curl" label="cURL">
+
+```shell
+curl -X DELETE https://example.com/wp-json/wc/v3/orders/723/fulfillments/2/metadata \
+	-u consumer_key:consumer_secret \
+	-H "Content-Type: application/json" \
+	-d '{
+  "meta_key": "_shipment_provider"
+}'
+```
+
+  </TabItem>
+  <TabItem value="js" label="JavaScript">
+
+```javascript
+WooCommerce.delete( 'orders/723/fulfillments/2/metadata', {
+	meta_key: '_shipment_provider',
+} )
+	.then( ( response ) => {
+		console.log( response.data );
+	} )
+	.catch( ( error ) => {
+		console.log( error.response.data );
+	} );
+```
+
+  </TabItem>
+  <TabItem value="php" label="PHP">
+
+```php
+<?php print_r($woocommerce->delete('orders/723/fulfillments/2/metadata', ['meta_key' => '_shipment_provider'])); ?>
+```
+
+  </TabItem>
+  <TabItem value="python" label="Python">
+
+```python
+print(wcapi.delete("orders/723/fulfillments/2/metadata", params={"meta_key": "_shipment_provider"}).json())
+```
+
+  </TabItem>
+  <TabItem value="ruby" label="Ruby">
+
+```ruby
+woocommerce.delete("orders/723/fulfillments/2/metadata", meta_key: "_shipment_provider").parsed_response
+```
+
+  </TabItem>
+  <TabItem value="response" label="JSON Response">
+
+```json
+[
+	{
+		"id": 3,
+		"key": "_items",
+		"value": [
+			{ "item_id": 456, "qty": 2 }
+		]
+	},
+	{
+		"id": 4,
+		"key": "_tracking_number",
+		"value": "1Z999AA9876543210"
+	}
+]
+```
+
+  </TabItem>
+</Tabs>
+
+## Look up a tracking number
+
+Given a tracking number, tries to determine the shipment provider and tracking URL. Useful when you have a tracking number but don't know (or want to guess) which carrier it belongs to.
+
+```http
+GET /wp-json/wc/v3/orders/<id>/fulfillments/lookup
+```
+
+<Tabs>
+  <TabItem value="curl" label="cURL">
+
+```shell
+curl "https://example.com/wp-json/wc/v3/orders/723/fulfillments/lookup?tracking_number=1234567890123456" \
+	-u consumer_key:consumer_secret
+```
+
+  </TabItem>
+  <TabItem value="js" label="JavaScript">
+
+```javascript
+WooCommerce.get( 'orders/723/fulfillments/lookup', {
+	tracking_number: '1234567890123456',
+} )
+	.then( ( response ) => {
+		console.log( response.data );
+	} )
+	.catch( ( error ) => {
+		console.log( error.response.data );
+	} );
+```
+
+  </TabItem>
+  <TabItem value="php" label="PHP">
+
+```php
+<?php print_r($woocommerce->get('orders/723/fulfillments/lookup', ['tracking_number' => '1234567890123456'])); ?>
+```
+
+  </TabItem>
+  <TabItem value="python" label="Python">
+
+```python
+print(wcapi.get("orders/723/fulfillments/lookup", params={"tracking_number": "1234567890123456"}).json())
+```
+
+  </TabItem>
+  <TabItem value="ruby" label="Ruby">
+
+```ruby
+woocommerce.get("orders/723/fulfillments/lookup", tracking_number: "1234567890123456").parsed_response
+```
+
+  </TabItem>
+  <TabItem value="response" label="JSON Response">
+
+When the tracking number format matches more than one carrier, a `possibilities` map is included instead of a single answer:
+
+```json
+{
+	"tracking_number": "1234567890123456",
+	"shipping_provider": "fedex",
+	"tracking_url": "https://www.fedex.com/fedextrack/?tracknum=1234567890123456",
+	"possibilities": {
+		"fedex": {
+			"url": "https://www.fedex.com/fedextrack/?tracknum=1234567890123456",
+			"ambiguity_score": 95
+		},
+		"ups": {
+			"url": "https://www.ups.com/track?tracknum=1234567890123456",
+			"ambiguity_score": 85
+		}
+	}
+}
+```
+
+When the format unambiguously matches a single carrier:
+
+```json
+{
+	"tracking_number": "1Z999AA1234567890",
+	"shipping_provider": "ups",
+	"tracking_url": "https://www.ups.com/track?tracknum=1Z999AA1234567890"
+}
+```
+
+When no carrier can parse the tracking number, the response is an empty array with a `200` status:
+
+```json
+[]
+```
+
+When the tracking number is longer than 50 characters, the response returns the first 50 characters and empty `shipping_provider` and `tracking_url` values, with a `200` status:
+
+```json
+{
+	"tracking_number": "<first 50 characters>",
+	"shipping_provider": "",
+	"tracking_url": ""
+}
+```
+
+  </TabItem>
+</Tabs>
+
+#### Available parameters
+
+| Parameter          | Type    | Description                      |
+| ------------------ | ------- | --------------------------------- |
+| `tracking_number`  | string  | Required. The tracking number to look up. |
+
+The two error cases are different and return different codes:
+
+-   Missing `tracking_number`: WordPress rejects the request before the endpoint runs, because the parameter is registered as required. The response is a `400` with the code `rest_missing_callback_param`.
+-   Empty `tracking_number` (for example `?tracking_number=`): the request reaches the endpoint and returns a `400` with the code `woocommerce_rest_tracking_number_missing`.
+
+## Error responses
+
+Errors follow the standard WooCommerce REST API error shape:
+
+```json
+{
+	"code": "error_code",
+	"message": "Error message description",
+	"data": {
+		"status": 400
+	}
+}
+```
+
+Common error codes for the fulfillments endpoints:
+
+-   `woocommerce_rest_order_invalid_id` - The order ID does not exist.
+-   `woocommerce_rest_order_id_missing` - The order ID is required but was not supplied.
+-   `rest_missing_callback_param` - A required parameter was omitted, for example a missing `tracking_number`.
+-   `woocommerce_rest_tracking_number_missing` - The `tracking_number` parameter was supplied but is empty.
+-   Standard authentication/authorization errors when the current user lacks `manage_woocommerce` and isn't the order's owner.
diff --git a/docs/apis/rest-api/v3/payment-gateways.mdx b/docs/apis/rest-api/v3/payment-gateways.mdx
index 6ea8678e5d5..e5c34bdc2d8 100644
--- a/docs/apis/rest-api/v3/payment-gateways.mdx
+++ b/docs/apis/rest-api/v3/payment-gateways.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 23
+sidebar_position: 24
 sidebar_label: 'Payment Gateways'
 ---

diff --git a/docs/apis/rest-api/v3/product-attribute-terms.mdx b/docs/apis/rest-api/v3/product-attribute-terms.mdx
index 906dec34559..9b8def866e8 100644
--- a/docs/apis/rest-api/v3/product-attribute-terms.mdx
+++ b/docs/apis/rest-api/v3/product-attribute-terms.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 10
+sidebar_position: 11
 sidebar_label: 'Product Attribute Terms'
 ---

diff --git a/docs/apis/rest-api/v3/product-attributes.mdx b/docs/apis/rest-api/v3/product-attributes.mdx
index b19f93728ee..5ec4a3a9bea 100644
--- a/docs/apis/rest-api/v3/product-attributes.mdx
+++ b/docs/apis/rest-api/v3/product-attributes.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 9
+sidebar_position: 10
 sidebar_label: 'Product Attributes'
 ---

diff --git a/docs/apis/rest-api/v3/product-categories.mdx b/docs/apis/rest-api/v3/product-categories.mdx
index 97516807076..889676fb187 100644
--- a/docs/apis/rest-api/v3/product-categories.mdx
+++ b/docs/apis/rest-api/v3/product-categories.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 11
+sidebar_position: 12
 sidebar_label: 'Product Categories'
 ---

diff --git a/docs/apis/rest-api/v3/product-custom-fields.mdx b/docs/apis/rest-api/v3/product-custom-fields.mdx
index 478b8ec6fdd..8b7246f21f9 100644
--- a/docs/apis/rest-api/v3/product-custom-fields.mdx
+++ b/docs/apis/rest-api/v3/product-custom-fields.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 12
+sidebar_position: 13
 sidebar_label: 'Product Custom Fields'
 ---

diff --git a/docs/apis/rest-api/v3/product-reviews.mdx b/docs/apis/rest-api/v3/product-reviews.mdx
index c9e1cc6fa35..b5bc06b8b5f 100644
--- a/docs/apis/rest-api/v3/product-reviews.mdx
+++ b/docs/apis/rest-api/v3/product-reviews.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 15
+sidebar_position: 16
 sidebar_label: 'Product Reviews'
 ---

diff --git a/docs/apis/rest-api/v3/product-shipping-classes.mdx b/docs/apis/rest-api/v3/product-shipping-classes.mdx
index 4a5edc35f98..4d6e67cc73a 100644
--- a/docs/apis/rest-api/v3/product-shipping-classes.mdx
+++ b/docs/apis/rest-api/v3/product-shipping-classes.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 13
+sidebar_position: 14
 sidebar_label: 'Product Shipping Classes'
 ---

diff --git a/docs/apis/rest-api/v3/product-tags.mdx b/docs/apis/rest-api/v3/product-tags.mdx
index a1f1df99815..ca6c9de7e45 100644
--- a/docs/apis/rest-api/v3/product-tags.mdx
+++ b/docs/apis/rest-api/v3/product-tags.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 14
+sidebar_position: 15
 sidebar_label: 'Product Tags'
 ---

diff --git a/docs/apis/rest-api/v3/product-variations.mdx b/docs/apis/rest-api/v3/product-variations.mdx
index 8a1f0885211..62b33dc6a43 100644
--- a/docs/apis/rest-api/v3/product-variations.mdx
+++ b/docs/apis/rest-api/v3/product-variations.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 8
+sidebar_position: 9
 sidebar_label: 'Product Variations'
 ---

diff --git a/docs/apis/rest-api/v3/products.mdx b/docs/apis/rest-api/v3/products.mdx
index 99b5ee98f14..f9e238a7d08 100644
--- a/docs/apis/rest-api/v3/products.mdx
+++ b/docs/apis/rest-api/v3/products.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 7
+sidebar_position: 8
 sidebar_label: 'Products'
 ---

diff --git a/docs/apis/rest-api/v3/refunds.mdx b/docs/apis/rest-api/v3/refunds.mdx
index e0ae4f85384..1ac0bd94bdd 100644
--- a/docs/apis/rest-api/v3/refunds.mdx
+++ b/docs/apis/rest-api/v3/refunds.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 17
+sidebar_position: 18
 sidebar_label: 'Refunds'
 ---

diff --git a/docs/apis/rest-api/v3/reports.mdx b/docs/apis/rest-api/v3/reports.mdx
index 025d95c0f8c..ad3895c5a52 100644
--- a/docs/apis/rest-api/v3/reports.mdx
+++ b/docs/apis/rest-api/v3/reports.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 16
+sidebar_position: 17
 sidebar_label: 'Reports'
 ---

diff --git a/docs/apis/rest-api/v3/setting-options.mdx b/docs/apis/rest-api/v3/setting-options.mdx
index 015a30d32da..44913bec2eb 100644
--- a/docs/apis/rest-api/v3/setting-options.mdx
+++ b/docs/apis/rest-api/v3/setting-options.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 22
+sidebar_position: 23
 sidebar_label: 'Setting Options'
 ---

diff --git a/docs/apis/rest-api/v3/settings.mdx b/docs/apis/rest-api/v3/settings.mdx
index 2b783085aa5..d54dd13fcb5 100644
--- a/docs/apis/rest-api/v3/settings.mdx
+++ b/docs/apis/rest-api/v3/settings.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 21
+sidebar_position: 22
 sidebar_label: 'Settings'
 ---

diff --git a/docs/apis/rest-api/v3/shipping-methods.mdx b/docs/apis/rest-api/v3/shipping-methods.mdx
index ed02fe1f424..48327d85b1d 100644
--- a/docs/apis/rest-api/v3/shipping-methods.mdx
+++ b/docs/apis/rest-api/v3/shipping-methods.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 27
+sidebar_position: 28
 sidebar_label: 'Shipping Methods'
 ---

diff --git a/docs/apis/rest-api/v3/shipping-zone-locations.mdx b/docs/apis/rest-api/v3/shipping-zone-locations.mdx
index a05bba61333..22e24600c1f 100644
--- a/docs/apis/rest-api/v3/shipping-zone-locations.mdx
+++ b/docs/apis/rest-api/v3/shipping-zone-locations.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 25
+sidebar_position: 26
 sidebar_label: 'Shipping Zone Locations'
 ---

diff --git a/docs/apis/rest-api/v3/shipping-zone-methods.mdx b/docs/apis/rest-api/v3/shipping-zone-methods.mdx
index e4e2b8a707b..f2d8cb53224 100644
--- a/docs/apis/rest-api/v3/shipping-zone-methods.mdx
+++ b/docs/apis/rest-api/v3/shipping-zone-methods.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 26
+sidebar_position: 27
 sidebar_label: 'Shipping Zone Methods'
 ---

diff --git a/docs/apis/rest-api/v3/shipping-zones.mdx b/docs/apis/rest-api/v3/shipping-zones.mdx
index 8abbb7c3190..b35d34974d7 100644
--- a/docs/apis/rest-api/v3/shipping-zones.mdx
+++ b/docs/apis/rest-api/v3/shipping-zones.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 24
+sidebar_position: 25
 sidebar_label: 'Shipping Zones'
 ---

diff --git a/docs/apis/rest-api/v3/system-status-tools.mdx b/docs/apis/rest-api/v3/system-status-tools.mdx
index edfabe3a711..32a31dcbbc1 100644
--- a/docs/apis/rest-api/v3/system-status-tools.mdx
+++ b/docs/apis/rest-api/v3/system-status-tools.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 29
+sidebar_position: 30
 sidebar_label: 'System Status Tools'
 ---

diff --git a/docs/apis/rest-api/v3/system-status.mdx b/docs/apis/rest-api/v3/system-status.mdx
index 31d0e0f704b..e78a3112a74 100644
--- a/docs/apis/rest-api/v3/system-status.mdx
+++ b/docs/apis/rest-api/v3/system-status.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 28
+sidebar_position: 29
 sidebar_label: 'System Status'
 ---

diff --git a/docs/apis/rest-api/v3/tax-classes.mdx b/docs/apis/rest-api/v3/tax-classes.mdx
index 4534b49630c..9c83fe3c7da 100644
--- a/docs/apis/rest-api/v3/tax-classes.mdx
+++ b/docs/apis/rest-api/v3/tax-classes.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 19
+sidebar_position: 20
 sidebar_label: 'Tax Classes'
 ---

diff --git a/docs/apis/rest-api/v3/taxes.mdx b/docs/apis/rest-api/v3/taxes.mdx
index 7fae2cc8d1a..bd44e4157ad 100644
--- a/docs/apis/rest-api/v3/taxes.mdx
+++ b/docs/apis/rest-api/v3/taxes.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 18
+sidebar_position: 19
 sidebar_label: 'Taxes'
 ---

diff --git a/docs/apis/rest-api/v3/webhooks.mdx b/docs/apis/rest-api/v3/webhooks.mdx
index 550b92d8c65..9dfd0c82ff3 100644
--- a/docs/apis/rest-api/v3/webhooks.mdx
+++ b/docs/apis/rest-api/v3/webhooks.mdx
@@ -1,5 +1,5 @@
 ---
-sidebar_position: 20
+sidebar_position: 21
 sidebar_label: 'Webhooks'
 ---