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).
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 |
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 |
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:
[
{
"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, and _date_fulfilled (stored as GMT and returned in ISO 8601 format, e.g. 2024-01-16T11:45:00Z). 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
GET /wp-json/wc/v3/orders/<id>/fulfillments
- cURL
- JavaScript
- PHP
- Python
- Ruby
- JSON Response
curl https://example.com/wp-json/wc/v3/orders/723/fulfillments \
-u consumer_key:consumer_secret
WooCommerce.get( 'orders/723/fulfillments' )
.then( ( response ) => {
console.log( response.data );
} )
.catch( ( error ) => {
console.log( error.response.data );
} );
<?php print_r($woocommerce->get('orders/723/fulfillments')); ?>
print(wcapi.get("orders/723/fulfillments").json())
woocommerce.get("orders/723/fulfillments").parsed_response
[
{
"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"
}
]
}
]
Create a fulfillment
On create, meta_data is itself a required parameter (it must contain at least the _items entry). Omitting the meta_data wrapper returns a 400 with the code rest_missing_callback_param.
POST /wp-json/wc/v3/orders/<id>/fulfillments
- cURL
- JavaScript
- PHP
- Python
- Ruby
- JSON Response
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"
}
]
}'
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 );
} );
<?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));
?>
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())
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
{
"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"
}
]
}
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
GET /wp-json/wc/v3/orders/<id>/fulfillments/<fulfillment_id>
- cURL
- JavaScript
- PHP
- Python
- Ruby
- JSON Response
curl https://example.com/wp-json/wc/v3/orders/723/fulfillments/2 \
-u consumer_key:consumer_secret
WooCommerce.get( 'orders/723/fulfillments/2' )
.then( ( response ) => {
console.log( response.data );
} )
.catch( ( error ) => {
console.log( error.response.data );
} );
<?php print_r($woocommerce->get('orders/723/fulfillments/2')); ?>
print(wcapi.get("orders/723/fulfillments/2").json())
woocommerce.get("orders/723/fulfillments/2").parsed_response
{
"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"
}
]
}
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. As a safeguard against accidental data loss, if the request's meta_data is non-empty but every entry is malformed (no valid key), the existing metadata is left untouched rather than cleared.
PUT /wp-json/wc/v3/orders/<id>/fulfillments/<fulfillment_id>
- cURL
- JavaScript
- PHP
- Python
- Ruby
- JSON Response
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"
}
]
}'
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 );
} );
<?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));
?>
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())
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
{
"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"
}
]
}
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
DELETE /wp-json/wc/v3/orders/<id>/fulfillments/<fulfillment_id>
- cURL
- JavaScript
- PHP
- Python
- Ruby
- JSON Response
curl -X DELETE "https://example.com/wp-json/wc/v3/orders/723/fulfillments/2?notify_customer=true" \
-u consumer_key:consumer_secret
WooCommerce.delete( 'orders/723/fulfillments/2', {
notify_customer: true,
} )
.then( ( response ) => {
console.log( response.data );
} )
.catch( ( error ) => {
console.log( error.response.data );
} );
<?php print_r($woocommerce->delete('orders/723/fulfillments/2', ['notify_customer' => true])); ?>
print(wcapi.delete("orders/723/fulfillments/2", params={"notify_customer": True}).json())
woocommerce.delete("orders/723/fulfillments/2", notify_customer: true).parsed_response
{
"message": "Fulfillment deleted successfully."
}
Deleting a fulfilled fulfillment with notify_customer=true triggers the woocommerce_fulfillment_deleted_notification action.
Get fulfillment metadata
GET /wp-json/wc/v3/orders/<id>/fulfillments/<fulfillment_id>/metadata
- cURL
- JavaScript
- PHP
- Python
- Ruby
- JSON Response
curl https://example.com/wp-json/wc/v3/orders/723/fulfillments/2/metadata \
-u consumer_key:consumer_secret
WooCommerce.get( 'orders/723/fulfillments/2/metadata' )
.then( ( response ) => {
console.log( response.data );
} )
.catch( ( error ) => {
console.log( error.response.data );
} );
<?php print_r($woocommerce->get('orders/723/fulfillments/2/metadata')); ?>
print(wcapi.get("orders/723/fulfillments/2/metadata").json())
woocommerce.get("orders/723/fulfillments/2/metadata").parsed_response
[
{
"id": 3,
"key": "_items",
"value": [
{ "item_id": 456, "qty": 2 }
]
},
{
"id": 4,
"key": "_tracking_number",
"value": "1Z999AA1234567890"
},
{
"id": 5,
"key": "_shipment_provider",
"value": "ups"
}
]
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."
PUT /wp-json/wc/v3/orders/<id>/fulfillments/<fulfillment_id>/metadata
- cURL
- JavaScript
- PHP
- Python
- Ruby
- JSON Response
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"
}
]
}'
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 );
} );
<?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));
?>
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())
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
[
{
"id": 3,
"key": "_items",
"value": [
{ "item_id": 456, "qty": 2 }
]
},
{
"id": 4,
"key": "_tracking_number",
"value": "1Z999AA9876543210"
},
{
"id": 5,
"key": "_shipment_provider",
"value": "fedex"
}
]
Delete fulfillment metadata
Deletes a single metadata entry by key and returns the fulfillment's remaining metadata.
DELETE /wp-json/wc/v3/orders/<id>/fulfillments/<fulfillment_id>/metadata
- cURL
- JavaScript
- PHP
- Python
- Ruby
- JSON Response
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"
}'
WooCommerce.delete( 'orders/723/fulfillments/2/metadata', {
meta_key: '_shipment_provider',
} )
.then( ( response ) => {
console.log( response.data );
} )
.catch( ( error ) => {
console.log( error.response.data );
} );
<?php print_r($woocommerce->delete('orders/723/fulfillments/2/metadata', ['meta_key' => '_shipment_provider'])); ?>
print(wcapi.delete("orders/723/fulfillments/2/metadata", params={"meta_key": "_shipment_provider"}).json())
woocommerce.delete("orders/723/fulfillments/2/metadata", meta_key: "_shipment_provider").parsed_response
[
{
"id": 3,
"key": "_items",
"value": [
{ "item_id": 456, "qty": 2 }
]
},
{
"id": 4,
"key": "_tracking_number",
"value": "1Z999AA9876543210"
}
]
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.
GET /wp-json/wc/v3/orders/<id>/fulfillments/lookup
- cURL
- JavaScript
- PHP
- Python
- Ruby
- JSON Response
curl "https://example.com/wp-json/wc/v3/orders/723/fulfillments/lookup?tracking_number=1234567890123456" \
-u consumer_key:consumer_secret
WooCommerce.get( 'orders/723/fulfillments/lookup', {
tracking_number: '1234567890123456',
} )
.then( ( response ) => {
console.log( response.data );
} )
.catch( ( error ) => {
console.log( error.response.data );
} );
<?php print_r($woocommerce->get('orders/723/fulfillments/lookup', ['tracking_number' => '1234567890123456'])); ?>
print(wcapi.get("orders/723/fulfillments/lookup", params={"tracking_number": "1234567890123456"}).json())
woocommerce.get("orders/723/fulfillments/lookup", tracking_number: "1234567890123456").parsed_response
When the tracking number format matches more than one carrier, a possibilities map is included instead of a single answer:
{
"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:
{
"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:
[]
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:
{
"tracking_number": "<first 50 characters>",
"shipping_provider": "",
"tracking_url": ""
}
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 a400with the coderest_missing_callback_param. - Empty
tracking_number(for example?tracking_number=): the request reaches the endpoint and returns a400with the codewoocommerce_rest_tracking_number_missing.
Error responses
Errors follow the standard WooCommerce REST API error shape:
{
"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 missingtracking_number.woocommerce_rest_tracking_number_missing- Thetracking_numberparameter was supplied but is empty.- Standard authentication/authorization errors. Creating, updating, and deleting fulfillments requires the
manage_woocommercecapability. A customer who owns the order (but lacksmanage_woocommerce) has read-only access: they can list and retrieve the order's fulfillments and metadata, but write requests return an authorization error.