Lots
Lots let you track inventory by expiration lot. In the Public API, lot management is handled by these mutations:
product_updateto mark a SKU as lot-trackedlot_createto create a lot for a SKUlot_updateto change one lotlots_updateto bulk-toggle active statuslot_assign_to_locationto assign a lot to a locationlot_deleteto delete one lot
In this guide we will use a vitamin SKU, VITAMIN-C-90CT, with a lot named VC-2026-10-A.
Note
The SKU should already exist, and it should be configured for lot tracking when you intend to manage physical lot-tracked inventory for it. If you are a 3PL managing a customer account, pass customer_account_id when creating the lot.
1. Check Account and SKU Lot Tracking
Before creating lots, check that lot tracking is active for the account and enabled on the SKU.
query GetLotTrackingSettings {
account {
request_id
complexity
data {
id
lot_tracking_settings {
is_active
priority
picking_disabled_period_in_days
verify_lot_when_packing
exclude_expired_quantity_from_available
}
}
}
}If lot_tracking_settings.is_active is not true, enable lot tracking for the account in ShipHero before relying on lot-tracked allocation, picking, or packing behavior.
Then query the product and check needs_lot_tracking.
query GetLotTrackedProduct($sku: String!, $customer_account_id: String) {
product(sku: $sku, customer_account_id: $customer_account_id) {
request_id
complexity
data {
id
sku
name
needs_lot_tracking
warehouse_products {
warehouse_id
locations(first: 10) {
edges {
node {
id
location_id
quantity
expiration_lot {
id
name
expires_at
}
location {
id
name
}
}
}
}
}
}
}
}{
"sku": "VITAMIN-C-90CT"
}If account lot tracking is active and needs_lot_tracking is already true, continue to the lot creation step.
2. Make the SKU Lot-Tracked
Use product_update to set needs_lot_tracking to true.
mutation EnableLotTracking($data: UpdateProductInput!) {
product_update(data: $data) {
request_id
complexity
product {
id
sku
needs_lot_tracking
}
}
}{
"data": {
"sku": "VITAMIN-C-90CT",
"needs_lot_tracking": true
}
}For 3PL workflows, include customer_account_id in the same payload.
If you are creating the SKU for the first time, you can also set needs_lot_tracking: true in product_create.
3. Inspect Existing Lots
Use expiration_lots to check whether the lot already exists for the SKU.
query GetLots($sku: String!) {
expiration_lots(sku: $sku) {
request_id
complexity
data(first: 10) {
edges {
node {
id
name
sku
expires_at
is_active
locations(first: 10) {
edges {
node {
id
name
}
}
}
}
}
}
}
}{
"sku": "VITAMIN-C-90CT"
}You can also filter expiration_lots by po_id when you are reviewing lots connected to a purchase order.
4. Create a Lot
Use lot_create to create the expiration lot.
mutation CreateLot($data: CreateLotInput!) {
lot_create(data: $data) {
request_id
complexity
lot {
id
name
sku
expires_at
is_active
}
}
}{
"data": {
"name": "VC-2026-10-A",
"sku": "VITAMIN-C-90CT",
"expires_at": "2026-10-31T00:00:00+00:00",
"is_active": true
}
}Save the returned lot.id. You will use it when assigning the lot to a location, updating it, or deleting it.
5. Find the SKU Locations You Can Assign
Use item_locations to find the location IDs where the SKU exists. This is usually the safest way to decide what location_id to send to lot_assign_to_location, because it shows the SKU, quantity, current lot, and location details together.
query GetSkuLocations($sku: [String], $warehouse_id: String, $customer_account_id: String) {
item_locations(
sku: $sku
warehouse_id: $warehouse_id
customer_account_id: $customer_account_id
has_inventory: true
) {
request_id
complexity
data(first: 25) {
edges {
node {
id
location_id
sku
quantity
expiration_lot {
id
name
expires_at
}
location {
id
name
pickable
sellable
}
}
}
}
}
}{
"sku": ["VITAMIN-C-90CT"],
"warehouse_id": "V2FyZWhvdXNlOjgwNzU="
}Use node.location_id as the location_id for lot_assign_to_location.
If you know the location name but do not yet have an item-location row for the SKU, use locations to resolve the location ID:
query GetWarehouseLocations($warehouse_id: String!, $name: String) {
locations(warehouse_id: $warehouse_id, name: $name) {
request_id
complexity
data(first: 10) {
edges {
node {
id
name
warehouse_id
pickable
sellable
}
}
}
}
}{
"warehouse_id": "V2FyZWhvdXNlOjgwNzU=",
"name": "A-01-01"
}Use node.id from the locations result as the location_id.
When you assign a lot to a valid location where the SKU exists in that warehouse but does not yet have an item-location row, ShipHero creates the item-location with quantity 0.
6. Assign the Lot to a Location
Use lot_assign_to_location to connect the lot to a location.
mutation AssignLotToLocation($data: AssignLotToLocationInput!) {
lot_assign_to_location(data: $data) {
request_id
complexity
warehouse_product {
id
sku
warehouse_id
on_hand
}
}
}{
"data": {
"lot_id": "TG90OjEyMzQ1",
"location_id": "QmluOjY3ODkw"
}
}This mutation assigns the lot to the SKU’s item location. If the location already has unlotted units of the same SKU, ShipHero absorbs those units into the lot so the lot quantity and item-location quantity stay consistent.
If the location already has inventory assigned to a different lot, the mutation fails instead of mixing lots.
7. Update One Lot
Use lot_update when one lot needs a corrected name, SKU, expiration date, or active status.
mutation UpdateLot($data: UpdateLotInput!) {
lot_update(data: $data) {
request_id
complexity
lot {
id
name
sku
expires_at
is_active
}
}
}{
"data": {
"lot_id": "TG90OjEyMzQ1",
"expires_at": "2026-11-30T00:00:00+00:00",
"is_active": true
}
}Use this for corrections to a specific lot record.
8. Bulk Deactivate or Reactivate Lots
Use lots_update when you need to mark several lots active or inactive at once.
mutation UpdateLots($data: UpdateLotsInput!) {
lots_update(data: $data) {
request_id
complexity
ok
}
}{
"data": {
"lots_ids": [
"TG90OjEyMzQ1",
"TG90OjY3ODkw"
],
"is_active": false
}
}lots_update only bulk-updates is_active. Use lot_update when you need to change names, SKUs, or expiration dates.
9. Delete a Lot
Use lot_delete when a lot record should be removed.
mutation DeleteLot($data: DeleteLotInput!) {
lot_delete(data: $data) {
request_id
complexity
lot {
id
name
sku
}
}
}{
"data": {
"lot_id": "TG90OjEyMzQ1"
}
}After deleting or deactivating lots, query expiration_lots again to confirm the current state.
Common Usage Patterns
- Use
product_updateonce when onboarding a SKU that should require lot tracking. - Use
account.lot_tracking_settingsto confirm account-level lot tracking behavior before troubleshooting SKU-level issues. - Use
item_locations(sku: ..., has_inventory: true)to find the rightlocation_idbefore assigning a lot. - Create a lot when receiving a new expiring batch, then assign it to the receiving location.
- Use
expiration_lots(sku: ...)before creating a lot so external systems do not create duplicates. - Use
lots_updateto deactivate recalled, expired, or quarantined lots in bulk. - Use
lot_assign_to_locationafter inventory exists in a bin when you need to stamp existing unlotted units with a lot.
What Can Break This Flow
- A SKU must be marked
needs_lot_tracking: truebefore it can behave as a lot-tracked item in inventory workflows. - Account-level
lot_tracking_settings.is_activemust also betruefor lot tracking behavior to apply across allocation, picking, and packing. - Lot names must be unique for the same SKU and account. Creating or renaming a lot to an existing SKU/name pair fails.
lot_createdoes not assign inventory or a location by itself. Uselot_assign_to_locationfor the location step.lot_assign_to_locationrequires both the lot and location IDs to be valid IDs that the API user can access. For item-location results, sendlocation_id, not the item-locationid.- The SKU must exist in the location’s warehouse. If ShipHero cannot find the SKU in that warehouse, assignment fails.
- A location that already has inventory under a different lot cannot be reassigned to the new lot.
- Existing unlotted inventory in the location is absorbed into the assigned lot.
lots_updateonly supportslots_idsandis_active; it is not a bulk edit endpoint for expiration dates or names.lot_updateandlot_deleteuselot_id, notid, in the Public API input.lot_deletecannot remove a lot that has already been received.