ShipStream Knowledge Base
Catalog

Inventory Holds

An Inventory Hold sets aside a specific quantity of inventory at a Location so that ShipStream will not allocate, pick, pack, or manifest it. Held inventory remains physically present and counted as on-hand, but it is excluded from the Unreserved quantity that drives availability — so it cannot back any new Shipment until the hold is released.

Holds are intended for the everyday situations that pull inventory out of the sellable pool without removing it from the warehouse: QC inspections, cycle counts in progress, damaged goods awaiting disposition, recalled lots, expired and near-expiring stock, contamination, customs/bond hold, and pending vendor returns. Each Hold is recorded against a Hold Reason that explains why the inventory is set aside, and every placement and release is logged in the Stock Movement Log with an audit trail.

How Holds Affect Inventory

Every Location, Stock Item, and Product carries a Qty Held quantity alongside the familiar Put-Away, Unreserved, and Reserved buckets. When a hold is placed, the held quantity moves from Unreserved to Qty Held; when the hold is released, it moves back. The total On-Hand count never changes as a side effect of placing or releasing a hold — held units remain physically on the shelf.

The advertised quantity used by integrations and the order processor automatically excludes held inventory, so Shopify, Amazon, and any other connected sales channel will see a lower availability the moment a hold is placed.

ShipStream forces re-allocation when a hold is placed. Any active Reservations against the held quantity are unreserved and the affected Shipments are routed back through Order Allocation so they can find inventory elsewhere — including in another Warehouse if available.

Hold Reasons

Hold Reasons are managed at System -> Operations -> Hold Reasons. ShipStream ships with a fixed set of system Hold Reasons that integrations and automated rules rely on. You can also create your own user-defined Hold Reasons for finer-grained reporting.

System Hold Reasons

These reasons are seeded automatically and cannot be deleted. Their codes are immutable so EDI mappings and automation continue to work across upgrades. The Label, Active flag, Display Group, and Sort Order can still be edited.

  • QC Inspection (qc_inspection) — held while the item undergoes quality control checks.
  • Cycle Count (cycle_count) — held while a count is in progress.
  • Damaged (damaged) — physically damaged units awaiting disposition.
  • Recalled (recalled) — vendor or manufacturer recall.
  • Expired (expired) — lots past their expiration date. Placed automatically by the Automatic Hold on Expiration feature.
  • Near Expiry (near_expiry) — lots within the configured days-before-expiration window.
  • Contaminated (contaminated) — failed an inspection, awaiting destruction or recall.
  • Customs/Bond Hold (bond_hold) — held under customs or bonded-warehouse rules.
  • Pending Disposal (pending_disposal) — staged for disposal.
  • Pending Return to Vendor (pending_return) — staged for an outbound return to the vendor.

User-Defined Hold Reasons

To add a custom reason, click Add Hold Reason on the Hold Reasons grid. User-defined reasons are always nested under one system reason — pick the parent that best matches the EDI category and operational behavior you need. For example, you might create Cosmetic Damage and Functional Damage under the Damaged parent, or Customer Recall and Vendor Recall under Recalled.

The form fields are:

  • Parent Reason - The system Hold Reason this user-defined reason inherits from. Required for user-defined reasons; hidden for system reasons.
  • Active - When set to No, the reason cannot be selected when placing new holds. Existing holds keep working.
  • Code - Internal identifier (alphanumeric and underscore). Used by integrations and EDI mappings. Read-only for system reasons.
  • Label - Human-readable name shown across the Admin UI, Client UI, and grids.
  • Display Group - Short label (max 25 characters) used to group held quantity into named columns on the Product Inventory Report and other inventory grids. Defaults to Hold. Type any value or pick from the datalist (Hold, Review, Expired, Unsellable, plus any value already saved on another reason). Reasons that share a Display Group fold into the same column.
  • Allow Relocation - Whether warehouse staff are allowed to relocate inventory that is on hold for this reason.
    • Yes - Relocation is allowed (with a warning). Held inventory stays on hold at the destination.
    • No - Relocation is blocked.
    • Inherit - User-defined only. Use the parent's setting at runtime.
  • Merchants - User-defined only. Restrict visibility to specific Merchants. Leave empty to expose the reason to every Merchant.
  • Sort Order - Determines ordering in dropdowns and the grid.

A reason cannot be deleted while any Hold record references it. To retire a reason, set its Active flag to No so it disappears from the placement dropdown but historical holds remain intact.
Out of the box every reason uses the Hold Display Group, so the Product Inventory Report shows a single combined held column. Move only the reasons you want to track separately — for example, change Expired and Near Expiry to a Display Group of Expired, and Damaged plus Pending Disposal to Unsellable — to expand the report to a useful set of dedicated columns. The four built-in labels (Hold, Review, Expired, Unsellable) are translated; user-defined Display Group values display as you typed them.

EDI Quantity Qualifier and Adjustment Reason codes for held inventory are configured per-subscription on the SPS Commerce integration — there is no EDI Code field on the Hold Reason itself.

Placing a Location Hold

Use a Location Hold when a specific Location's inventory needs to be set aside — for example, while a counter investigates a discrepancy, or when staff find damaged units in a single location.

  1. Navigate to Catalog -> Inventory Holds and click Place Hold, or open a Location's view page and click Hold Inventory.
  2. Choose a Hold Reason and enter a Quantity. The default quantity is the full Unreserved quantity at that Location.
  3. (Optional) Add a Note explaining the hold. The note is shown in the Holds tab on the Location and on the Inventory Holds grid.
  4. Click Place Hold.

The Quantity moves from Unreserved to Qty Held, the Location is annotated with an On Hold badge, and any active Reservations against the held quantity are unreserved and rerouted via Order Allocation. The hold appears on the Location's Holds tab and on the centralized Inventory Holds grid.

The Hold Inventory button is hidden when the Location has no Unreserved quantity. To hold reserved or picked quantity, use the Lot Hold flow described below or wait for the Shipment to drop those reservations.

Quarantining an Entire Lot

When a recall, contamination event, or near-expiry sweep affects every unit of a Lot, use Quarantine Lot to place a hold on every Location holding that Lot in one action.

  1. Open Catalog -> Lots/Expirations and click the Lot you want to quarantine.
  2. Click Quarantine Lot in the page header. The popup confirms the total units and Locations that will be affected.
  3. Choose a Hold Reason and (optionally) add a Note.
  4. Click Quarantine Lot to apply.

Behind the scenes ShipStream creates one Hold record per Location, each linked to the Lot. The set behaves as a single quarantine for the purpose of release: clicking Release Quarantine on the Lot view releases every active Hold attached to that Lot in one action.

The Quarantine Lot button is hidden when the Lot already carries a quarantine — a single Lot can have only one active quarantine at a time. To change the reason on an existing quarantine, release it first and then quarantine again with the new reason.
A quarantined Lot also affects inbound putaways. If staff put-away more units of a quarantined Lot through a Delivery, the new inventory is automatically placed on hold with the same reason as the original quarantine. The Scanner UI warns the operator before they confirm the put-away.

BOM Lineage Cascade

Lot quarantines can optionally cascade across kitting and de-kitting lineage. This matters most for recalls of food, medical, or hazardous products: quarantining a kit Lot also catches any component Lots that were de-kitted from it, and quarantining a component Lot catches any kit Lots assembled with it.

When cascade is requested, ShipStream traverses the Work Order lineage:

  • Forward — from a kit Lot to every component Lot produced when that kit was de-kitted.
  • Backward — from a component Lot to every kit Lot assembled using it.

Both directions are walked recursively, so a multi-level lineage (de-kit -> re-kit -> de-kit again) is fully covered. Cross-Merchant lineage is intentionally skipped: a recall on one Merchant's Lot will never cascade onto a different Merchant's inventory.

Cascade is currently only available through the API (POST /v1/inventory/holds@lot with cascade_bom=true, or the equivalent Merchant API method). The Admin UI Quarantine Lot popup always quarantines exactly the selected Lot — operators handling a recall that crosses kit/component boundaries should release the cascaded children individually, or trigger the cascade through an integration.

Releasing a Hold

Holds can be released from a Location, from a Lot, or from the centralized grid. In every case the held Quantity moves back to Unreserved and the Stock Movement Log records a release entry.

  • From a Location view page — click Release Hold to release every active hold at that Location.
  • From a Lot view page — click Release Quarantine to release every active hold linked to that Lot across every affected Location.
  • From Catalog -> Inventory Holds — select one or more rows and choose Release Selected Holds from the mass-action menu.
Releasing a hold requires the Catalog -> Inventory Holds -> Release Holds permission. Placing holds requires only Catalog -> Inventory Holds, so it is possible to grant a Role the ability to put inventory on hold without also being able to release it.

Inventory Holds Grid

The centralized Holds grid lives at Catalog -> Inventory Holds. It defaults to showing every active Hold across every Warehouse you have access to, with the following columns:

Hold ID, Status (Active / Released), Group (Display Group), Warehouse (when you have access to multiple), Merchant (when not in single-website mode), Location, SKU, Product Name, Hold Reason, Qty Held, Lot Number, Expiration, Held Since, Held By, Notes.

Filters work the same as any other ShipStream grid. Common filtered views:

  • Status: Released — audit historical holds, including who placed and released them.
  • Group: Expired — see only the inventory currently held under the Expired Display Group, regardless of which underlying Reason was used.
  • Hold Reason: Damaged — drill into one specific reason.
  • Held Since: last 24h — find brand-new holds, useful in a recall response.

Click any row to open the Location view for that hold. Mass actions support Release Selected Holds; CSV and Excel exports are available from the Export menu.

Active Holds on the Product Inventory tab

Each Product's Inventory tab gains an Inventory Report that breaks Qty Held into one column per Display Group between Picked and On Hand, plus a summary box at the top of the report showing the per-group held totals next to Qty On Hand, Qty Backordered, and Qty Advertised. Below the report, an Active Holds mini-grid lists the currently-active holds on that Product (Warehouse, Status, Reason, Lot Number, Expiration, Location, Held At, Held By, Quantity) so you do not have to leave the Product page to see what is held and why.

Holds in the Client Portal

Merchants get a read-only view of holds on their own inventory at Stock -> Inventory Holds in the Client UI. The grid shows the same Hold information as the Admin UI but excludes any internal Admin user names — only Client user attributions appear in the Held By column. A Merchant cannot place new holds, release existing ones, or manage Hold Reasons.

The Client Product Inventory tab also mirrors the admin layout: per-Display-Group breakdown columns between Picked and On Hand, and the per-group totals beside Qty On Hand and Qty Advertised in the summary box. A Holds tab on the Client Lot view shows which Locations of a Lot are currently quarantined and why.

Use User-Defined Hold Reasons with Merchants set to a single Merchant to expose Merchant-specific reason labels (for example, Brand Recall - 2026 Q2) without polluting other Merchants' dropdowns.

Holds in the Scanner UI

Warehouse operators do not place or release holds from the Scanner — that remains an Admin function. The Scanner is hold-aware in three flows:

  • Relocation — When the source Location is on hold for a reason whose Allow Relocation is No, the Scanner blocks the pull with a buzzer and the message "This location is on hold (Reason). Relocation is not permitted.". When the reason allows relocation, the Scanner shows a warning and the operator must move the full on-shelf quantity in one step — partial pulls and partial pushes from a held location are rejected. ShipStream releases the source hold, moves the entire quantity, and re-places the same-reason hold at the destination, merging into any existing same-reason hold there. Pushing into a destination that already carries a hold under a different reason is hard-blocked with a buzzer to prevent silently mixing reasons. See Relocating Held Inventory for the operator workflow.
  • Putaway — Putting away to a Location that already carries held inventory is allowed but warns the operator. Putting away inventory of a quarantined Lot is also allowed (the priority is to clear the dock) and the Scanner warns that the new units will be placed on hold automatically as soon as putaway completes.

  • Cycle Count — When a Location carries an active hold, the location row displays an inline On Hold: Reason (N units) badge and the count comment shows the Unreserved | On Hold | Total on Shelf breakdown so the counter knows what is physically on the shelf versus what is set aside. Counted variances apply to Unreserved only — the held quantity is fixed by the hold itself.

Picking, Packing, and Manifesting do not need any Scanner-specific handling — Order Allocation routes around held inventory so picks never target a held Location, and the model layer blocks Packing in every entry point (Scanner, Admin, Bulk Fulfill) if a Lot becomes quarantined after the Shipment was picked. The Packing rejection message identifies the held Lot and Reason.

If a Lot is quarantined after a Shipment has already been packed but before the Manifest is sealed, the Manifest seal will fail with an explicit message identifying the held Lot. Re-pack the affected Shipment from another Lot, or release the quarantine, before retrying the seal.

Permissions

Hold-related permissions live under two ACL paths:

  • Catalog -> Inventory Holds - Grants access to the Inventory Holds grid, the Holds tab on Location and Lot view pages, and the Hold Inventory / Quarantine Lot buttons.
    • Catalog -> Inventory Holds -> Release Holds (sub-permission) - Grants access to Release Hold, Release Quarantine, and the grid's mass-action release.
  • System -> Operations -> Hold Reasons - Grants access to the Hold Reasons CRUD page.

Configure these on each User Role as appropriate.

API Access

qty_held is exposed on every existing inventory endpoint of the Merchant API and Global API alongside the familiar quantity buckets.

Merchant API additions:

  • inventory.list and inventory.detailed accept a withHeldBreakdown=true flag that adds a qty_held_by_reason rollup (keyed by parent system Reason code) and, when user-defined Reasons are in play, a qty_held_by_user_reason map nested under each parent code. The rollup shape stays stable regardless of which user-defined Reasons you have configured.
  • inventory.lots adds a qty_held per-lot total and an is_on_hold boolean.
  • inventory.holdSearch searches active and released holds with filters for SKU, warehouse, reason code, lot, status, and date range, with standard pagination.
  • inventory.holdReasons returns the list of currently-active, Merchant-scoped Hold Reasons (code, label, display_group) so integrations can render Reason dropdowns without hard-coding the catalog.

Global API additions:

  • qty_held is added to GET /v1/inventory/levels/total and /warehouse.
  • GET /v1/inventory/holds lists holds across all merchants with filters for merchant, warehouse, product, lot, reason, status, and held-at date range.
  • POST /v1/inventory/holds@location and holds@lot place location- and lot-scope holds; the lot endpoint accepts cascade_bom=true to walk BOM lineage.
  • POST /v1/inventory/holds/{id}/release and holds@lot/{lot_id}/release release a single hold or every active hold for a lot.
  • GET /v1/inventory/hold-reasons exposes the cross-merchant Hold Reason catalog.

Hold Reasons are also exposed through System -> Enumerations under the Catalog -> Hold Reasons category for any integration that already consumes the Enumerations feed.

For full endpoint and parameter documentation see the API reference at docs.shipstream.io. Holds endpoints are documented under the Inventory section.

EDI Reporting

The SPS Commerce integration reports held inventory on outbound EDI 846 (Inventory Advice) and EDI 947 (Warehouse Inventory Adjustment Advice) documents. Each subscription carries two reason-code maps — one for 846 Quantity Qualifier codes, one for 947 W15-04 Adjustment Reason codes — that translate ShipStream Hold Reasons into the codes your trading partner expects. See the Hold Reason EDI Mappings section of the SPS Commerce integration page for the full default tables and instructions for overriding the maps per subscription.

See Also