Skip to main content

Feature preview: Physical inventory

The physical inventory feature preview gives you early access to a new set of inventory APIs in the unstable GraphQL Admin API. Use them to build against the primitives that model how merchants organize a stockroom and fulfill orders from it: bins, counts, bin moves, and fulfillment from bins.

Feature preview

These APIs are under active development and are subject to change. Enable the Physical inventory feature preview on a development store to try them, and share feedback so the stable surface is the one you'd want to build on.

These APIs introduce new inventory primitives alongside the existing inventory APIs. Your current integration keeps working while you try them.


  • Bins. Named storage within a location, such as a shelf or a rack. Create, update, and delete bins, and read the quantities held in each one.
  • Counts. Set the on-hand quantity of an inventory item in a bin with inventoryCountCreate.
  • Bin moves. Move on-hand inventory between bins at a location with inventoryBinMoveCreate.
  • Fulfill from bins. Record which bins a fulfillment was picked from with sourceBinAllocations on fulfillmentCreate.

Anchor to How bins work with your existing location inventoryHow bins work with your existing location inventory

Today you set an item's on-hand quantity at a location with inventorySetQuantities. Bins add a finer grain inside a location: instead of one on-hand number per location, an item's on-hand can be split across the bins where it's physically stored.

Every location has a default bin. A location's total on-hand is the sum of the quantities in every bin at the location, including the default bin:

onHand = sum of quantities in every bin, including the location's default bin

This keeps the preview compatible with the existing location-level APIs:

  • Your existing inventorySetQuantities calls still work. Quantity set at the location level lands in the location's default bin — the other bins are unaffected.
  • The location-level aggregates (available, committed, onHand) are still reported per item at the location. Bins change how the on-hand is distributed underneath, and on-hand quantity in a bin with isAvailableForSale: false doesn't count toward available (see Create, update, and delete bins).
  • To place quantity into a specific bin, use a count (see Set a quantity in a bin) or a bin move (see Move inventory between bins).

Anchor to Enable the feature previewEnable the feature preview

Get early access by enabling the Physical inventory feature preview on a development store.

Caution

Feature previews are still under development and are subject to change.

Only stores with the preview enabled can use the bins and bin moves APIs, or fulfill from bins. For the access requirements of counts, see inventoryCountCreate. On a store that hasn't enabled the preview:

  • Calls to the bins and bin moves APIs return an access error. Reading binQuantities on a fulfillment line item also returns an access error.
  • fulfillmentCreate doesn't require the preview. A request that includes sourceBinAllocations returns a user error, and the fulfillment isn't created.

Anchor to Build against the APIsBuild against the APIs

Send your requests to the unstable version of the GraphQL Admin API. The bins, counts, and bin moves APIs, and the sourceBinAllocations and binQuantities fields for fulfillment from bins, aren't available in stable versions.

Anchor to Create, update, and delete binsCreate, update, and delete bins

Use inventoryBinsEdit to create, update, and delete bins. Pass a list of operations in input. Each entry is exactly one of create, update, or delete.

The mutation applies the whole list as one operation. If any entry fails, then no changes are saved, and userErrors reports each failure with the path to the entry that caused it, such as ["input", "1", "create", "name"].

  • create: Provide the locationId and a name that's unique within the location. barcode (unique within the location), note, and isAvailableForSale are optional. isAvailableForSale defaults to true. One request can create bins at more than one location.
  • update: Provide the bin's id and only the fields that you want to change. Pass null to clear barcode or note.
  • delete: Provide the bin's ID as deletedId. You can delete a bin only when it holds no inventory: every item's quantity in the bin must be zero, including negative quantities. You can't delete the location's default bin.

isAvailableForSale controls whether the on-hand quantity in the bin counts toward the location's available quantity. It doesn't affect whether orders can be fulfilled from the bin.

The following example creates two bins at a location:

mutation {
inventoryBinsEdit(
input: [
{ create: { locationId: "gid://shopify/Location/10", name: "Shelf-A1", barcode: "012345678912" } }
{ create: { locationId: "gid://shopify/Location/10", name: "Shelf-A2" } }
]
) {
createdBins {
id
name
barcode
isAvailableForSale
}
userErrors {
field
message
code
}
}
}
{
"data": {
"inventoryBinsEdit": {
"createdBins": [
{ "id": "gid://shopify/InventoryBin/2", "name": "Shelf-A1", "barcode": "012345678912", "isAvailableForSale": true },
{ "id": "gid://shopify/InventoryBin/3", "name": "Shelf-A2", "barcode": null, "isAvailableForSale": true }
],
"userErrors": []
}
}
}

The following example updates one bin's note and deletes an empty bin in the same request:

mutation {
inventoryBinsEdit(
input: [
{ update: { id: "gid://shopify/InventoryBin/2", note: "Top shelf, left side" } }
{ delete: { deletedId: "gid://shopify/InventoryBin/4" } }
]
) {
updatedBins {
id
name
note
}
deletedBinIds
userErrors {
field
message
code
}
}
}
{
"data": {
"inventoryBinsEdit": {
"updatedBins": [
{ "id": "gid://shopify/InventoryBin/2", "name": "Shelf-A1", "note": "Top shelf, left side" }
],
"deletedBinIds": ["gid://shopify/InventoryBin/4"],
"userErrors": []
}
}
}

If a bin that you're deleting still holds inventory, then the request returns a user error for that entry, and no changes are saved.

To empty a bin before you delete it, move its stock to another bin with a bin move, or set each item's quantity in the bin to zero with a completed count.

Anchor to Read bin and location quantitiesRead bin and location quantities

Read from the location query. inventoryLocationItems returns the per-item aggregates for the location, and inventoryBins returns every bin at the location, including the default bin, with the quantities it holds per item. isDefault is true for the default bin.

Bins only hold inventory items that track quantity.

query {
location(id: "gid://shopify/Location/10") {
id
name
# Per-item aggregates for the location.
inventoryLocationItems(first: 10) {
nodes {
inventoryItem {
id
sku
}
available
committed
onHand
}
}
# Each bin and the quantity it holds per item.
inventoryBins(first: 10) {
nodes {
id
name
barcode
isDefault
inventoryBinItems(first: 10) {
nodes {
inventoryItem {
id
sku
}
quantity
}
}
}
}
}
}
{
"data": {
"location": {
"id": "gid://shopify/Location/10",
"name": "Warehouse A",
"inventoryLocationItems": {
"nodes": [
{
"inventoryItem": { "id": "gid://shopify/InventoryItem/1", "sku": "sku-a" },
"available": 21,
"committed": 2,
"onHand": 23
}
]
},
"inventoryBins": {
"nodes": [
{
"id": "gid://shopify/InventoryBin/1",
"name": "Main",
"barcode": null,
"isDefault": true,
"inventoryBinItems": {
"nodes": [
{ "inventoryItem": { "id": "gid://shopify/InventoryItem/1", "sku": "sku-a" }, "quantity": 5 }
]
}
},
{
"id": "gid://shopify/InventoryBin/2",
"name": "Shelf-A1",
"barcode": "012345678912",
"isDefault": false,
"inventoryBinItems": {
"nodes": [
{ "inventoryItem": { "id": "gid://shopify/InventoryItem/1", "sku": "sku-a" }, "quantity": 10 }
]
}
},
{
"id": "gid://shopify/InventoryBin/3",
"name": "Shelf-A2",
"barcode": null,
"isDefault": false,
"inventoryBinItems": {
"nodes": [
{ "inventoryItem": { "id": "gid://shopify/InventoryItem/1", "sku": "sku-a" }, "quantity": 8 }
]
}
}
]
}
}
}
}

Here onHand (23) is the sum of every bin at the location: 5 units in the default bin (Main), 10 in Shelf-A1, and 8 in Shelf-A2. available and committed stay at the location level.

To read only the default bin, filter the connection with inventoryBins(first: 1, query: "is_default:true").

Inventory items that don't track quantity still appear in inventoryLocationItems, so you can see where they're stocked, but their quantity fields (onHand, available, committed and incoming) are null. Check inventoryItem { tracked } to tell them apart. Writes that set bin quantities, such as inventoryCountCreate, return a user error for them.

Anchor to Set a quantity in a binSet a quantity in a bin

Use inventoryCountCreate to set the on-hand quantity of an item in a bin. Each line item targets an inventory item and a bin. Specify the following arguments, quantity fields, and the required idempotency directive:

  • complete: true posts the count's results as soon as the count is created. Without it, the count stays open and the on-hand quantity doesn't change.
  • actualQuantity is the on-hand quantity to set for that item in that bin.
  • expectedQuantity is the quantity you last read for that item in that bin. It's used to detect a variance and to guard against overwriting a change made since you read.
  • The @idempotent directive requires a unique key (such as a UUID) for each count. Reusing a key returns the original count instead of creating a new one.
mutation {
inventoryCountCreate(
locationId: "gid://shopify/Location/10"
complete: true
lineItems: [
{
inventoryItemId: "gid://shopify/InventoryItem/1"
inventoryBinId: "gid://shopify/InventoryBin/2"
quantity: { actualQuantity: 10, expectedQuantity: 9 }
}
]
) @idempotent(key: "550e8400-e29b-41d4-a716-446655440000") {
userErrors {
field
message
}
}
}
{
"data": {
"inventoryCountCreate": {
"userErrors": []
}
}
}

After the count is created, read the bin's quantities back with the location query to confirm the new on-hand.

Anchor to Move inventory between binsMove inventory between bins

Use inventoryBinMoveCreate to move on-hand inventory between bins at one location. The location's total on-hand doesn't change. The mutation requires the @idempotent directive.

Each item has a from and a to. Each one is either a bin or a systemBinIdentifier:

  • bin: Provide the binId and changeFromQuantity. changeFromQuantity is the on-hand quantity that you expect in the bin before the move. If it doesn't match, then the request returns a user error and nothing moves. The field is required. Pass null to skip the check.
  • systemBinIdentifier: Use ADDED as the origin to add new inventory to a bin, or REMOVED as the destination to take inventory out of a bin. Each item needs at least one bin.

If any item fails, then nothing moves.

The following example moves 4 units from Shelf-A1 to Shelf-A2:

mutation {
inventoryBinMoveCreate(
locationId: "gid://shopify/Location/10"
items: [
{
inventoryItemId: "gid://shopify/InventoryItem/1"
quantity: 4
from: { bin: { binId: "gid://shopify/InventoryBin/2", changeFromQuantity: 10 } }
to: { bin: { binId: "gid://shopify/InventoryBin/3", changeFromQuantity: 8 } }
}
]
) @idempotent(key: "1b2c3d4e-0000-4000-8000-000000000001") {
inventoryBinMove {
id
name
}
userErrors {
field
message
code
}
}
}
{
"data": {
"inventoryBinMoveCreate": {
"inventoryBinMove": {
"id": "gid://shopify/InventoryBinMove/1",
"name": "#BM1001"
},
"userErrors": []
}
}
}

After the move, Shelf-A1 holds 6 units and Shelf-A2 holds 12. Read them back with the location query.

To record which bins a fulfillment was picked from, pass sourceBinAllocations to fulfillmentCreate. The on-hand quantity is taken from those bins.

  • Each allocation names a fulfillment order line item that's listed in fulfillmentOrderLineItems, a binId, and a quantity. If you omit binId, then the location's default bin is used.
  • A line item's allocations must add up to its fulfilled quantity. A line item without allocations is fulfilled from the default bin.
  • The bins must belong to the fulfillment order's location. Allocations work only for fulfillments that are complete when they're created.

The following example fulfills 3 units, 2 from Shelf-A1 and 1 from Shelf-A2:

mutation {
fulfillmentCreate(
fulfillment: {
lineItemsByFulfillmentOrder: [
{
fulfillmentOrderId: "gid://shopify/FulfillmentOrder/1"
fulfillmentOrderLineItems: [{ id: "gid://shopify/FulfillmentOrderLineItem/1", quantity: 3 }]
}
]
sourceBinAllocations: [
{ fulfillmentOrderLineItemId: "gid://shopify/FulfillmentOrderLineItem/1", binId: "gid://shopify/InventoryBin/2", quantity: 2 }
{ fulfillmentOrderLineItemId: "gid://shopify/FulfillmentOrderLineItem/1", binId: "gid://shopify/InventoryBin/3", quantity: 1 }
]
}
) {
fulfillment {
id
fulfillmentLineItems(first: 10) {
nodes {
quantity
binQuantities {
inventoryBin {
id
name
}
quantity
}
}
}
}
userErrors {
field
message
}
}
}
{
"data": {
"fulfillmentCreate": {
"fulfillment": {
"id": "gid://shopify/Fulfillment/1",
"fulfillmentLineItems": {
"nodes": [
{
"quantity": 3,
"binQuantities": [
{ "inventoryBin": { "id": "gid://shopify/InventoryBin/2", "name": "Shelf-A1" }, "quantity": 2 },
{ "inventoryBin": { "id": "gid://shopify/InventoryBin/3", "name": "Shelf-A2" }, "quantity": 1 }
]
}
]
}
},
"userErrors": []
}
}
}

Read binQuantities on a fulfillment line item to see where its units were picked from. If you cancel the fulfillment with fulfillmentCancel, then the inventory goes back to those bins, and inventory from a bin that was deleted goes to the location's default bin. After cancellation, binQuantities is empty, so read it before you cancel if you need that record.


Was this page helpful?