Skip to content
Lots

Lots

Lots let you track inventory by expiration lot. In the Public API, lot management is handled by these mutations:

  • product_update to mark a SKU as lot-tracked
  • lot_create to create a lot for a SKU
  • lot_update to change one lot
  • lots_update to bulk-toggle active status
  • lot_assign_to_location to assign a lot to a location
  • lot_delete to 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_update once when onboarding a SKU that should require lot tracking.
  • Use account.lot_tracking_settings to confirm account-level lot tracking behavior before troubleshooting SKU-level issues.
  • Use item_locations(sku: ..., has_inventory: true) to find the right location_id before 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_update to deactivate recalled, expired, or quarantined lots in bulk.
  • Use lot_assign_to_location after 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: true before it can behave as a lot-tracked item in inventory workflows.
  • Account-level lot_tracking_settings.is_active must also be true for 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_create does not assign inventory or a location by itself. Use lot_assign_to_location for the location step.
  • lot_assign_to_location requires both the lot and location IDs to be valid IDs that the API user can access. For item-location results, send location_id, not the item-location id.
  • 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_update only supports lots_ids and is_active; it is not a bulk edit endpoint for expiration dates or names.
  • lot_update and lot_delete use lot_id, not id, in the Public API input.
  • lot_delete cannot remove a lot that has already been received.

Reference Links