Skip to content

Inventory

The Inventory entity provides access to P21 inventory master records and location-level quantity data. P21 separates inventory into two conceptual layers: the item master (inv_mast), which defines the product globally, and location records (inv_loc), which track quantities and attributes per warehouse/location.


Endpoints

Method URL Description
GET /entity/inventory/ Get all inventory items (ArrayOfInventoryItem)
POST /entity/inventory/ Create a new inventory item
GET /entity/inventory/{companyId}_{itemId} Get a single item master with location data
PUT /entity/inventory/{companyId}_{itemId} Update an inventory item
GET /entity/inventory/new Get a blank item with defaults populated
GET /entity/inventory/ping Health check

Compound Key

GET https://{server}/api/entity/inventory/01_WIDGET-100

The item_id is the product/SKU identifier. It is alphanumeric and case-sensitive in most P21 configurations.


Data Model — Master vs. Location

Understanding the two-layer model is essential for working with P21 inventory data correctly.

inv_mast (Item Master)
│   item_id, item_desc, unit_of_measure, product_group_id
│   standard_cost, list_price, weight
│   hazmat_flag, serialized_flag, lot_controlled
└── inv_loc[] (Location Records — one per warehouse/location)
        location_id, warehouse_id
        qty_on_hand, qty_allocated, qty_on_order, qty_available

A single item_id exists once in inv_mast, but can have N records in inv_loc — one per warehouse or location where the item is stocked or managed. When you GET an inventory item through the Entity API, the response includes the master record with all its location records nested under an InventoryLocations collection.


Field Reference

Item Master Fields (inv_mast)

Field Type Description
item_id string Unique item/SKU identifier
item_desc string Item description
unit_of_measure string Base unit of measure (e.g., EA, CS, LB)
product_group_id string Product group/category classification
standard_cost decimal Standard cost per unit
list_price decimal List/catalog price per unit
weight decimal Item weight per unit
hazmat_flag string Hazardous materials flag (Y / N)
serialized_flag string Serialized item tracking flag (Y / N)
lot_controlled string Lot/batch control flag (Y / N)
date_last_modified datetime Timestamp of last modification
company_id string Company identifier

Location Quantity Fields (inv_loc)

Field Type Description
location_id string Location identifier within the warehouse
warehouse_id string Warehouse identifier
qty_on_hand decimal Physical quantity currently on hand
qty_allocated decimal Quantity allocated to open orders
qty_on_order decimal Quantity on open purchase orders
qty_available decimal qty_on_hand - qty_allocated (available to promise)

qty_available is derived

qty_available is a calculated field: qty_on_hand - qty_allocated. It represents the quantity that can be promised to new orders. Do not attempt to set this field directly — it is maintained by P21 as orders are allocated and fulfilled.


XML Examples

GET — Item Master with Location Data

<InventoryItem xmlns="http://www.epicor.com/entity">
  <company_id>01</company_id>
  <item_id>WIDGET-100</item_id>
  <item_desc>Standard Widget 100mm</item_desc>
  <unit_of_measure>EA</unit_of_measure>
  <product_group_id>WIDGETS</product_group_id>
  <standard_cost>8.25</standard_cost>
  <list_price>12.50</list_price>
  <weight>0.35</weight>
  <hazmat_flag>N</hazmat_flag>
  <serialized_flag>N</serialized_flag>
  <lot_controlled>N</lot_controlled>
  <date_last_modified>2025-09-22T10:00:00</date_last_modified>
  <InventoryLocations>
    <InventoryLocation>
      <warehouse_id>MAIN</warehouse_id>
      <location_id>A-01-01</location_id>
      <qty_on_hand>500.00</qty_on_hand>
      <qty_allocated>150.00</qty_allocated>
      <qty_on_order>200.00</qty_on_order>
      <qty_available>350.00</qty_available>
    </InventoryLocation>
    <InventoryLocation>
      <warehouse_id>EAST</warehouse_id>
      <location_id>B-03-02</location_id>
      <qty_on_hand>75.00</qty_on_hand>
      <qty_allocated>20.00</qty_allocated>
      <qty_on_order>0.00</qty_on_order>
      <qty_available>55.00</qty_available>
    </InventoryLocation>
  </InventoryLocations>
</InventoryItem>

GET — New Item Template

GET https://{server}/api/entity/inventory/new

Use the template response to discover all required fields and defaults before creating a new item. Required fields vary by P21 configuration (e.g., some installations require product_group_id; others do not).

POST — Create Item (Request Body)

<InventoryItem xmlns="http://www.epicor.com/entity">
  <company_id>01</company_id>
  <item_id>WIDGET-300</item_id>
  <item_desc>Heavy Duty Widget 300mm</item_desc>
  <unit_of_measure>EA</unit_of_measure>
  <product_group_id>WIDGETS</product_group_id>
  <standard_cost>22.00</standard_cost>
  <list_price>34.99</list_price>
  <weight>1.20</weight>
  <hazmat_flag>N</hazmat_flag>
  <serialized_flag>N</serialized_flag>
  <lot_controlled>N</lot_controlled>
</InventoryItem>

C# Examples

Get an Inventory Item

public async Task<InventoryItem> GetInventoryItemAsync(string companyId, string itemId)
{
    var url = $"{_baseUrl}/entity/inventory/{companyId}_{itemId}";
    var response = await _httpClient.GetAsync(url);
    response.EnsureSuccessStatusCode();

    var xml = await response.Content.ReadAsStringAsync();
    var serializer = new XmlSerializer(typeof(InventoryItem));
    using var reader = new StringReader(xml);
    return (InventoryItem)serializer.Deserialize(reader);
}

Get Available Quantity Across All Warehouses

public async Task<decimal> GetTotalAvailableQtyAsync(string companyId, string itemId)
{
    var item = await GetInventoryItemAsync(companyId, itemId);

    return item.InventoryLocations?
        .Sum(loc => loc.qty_available) ?? 0m;
}

Get Available Quantity for a Specific Warehouse

public async Task<decimal> GetWarehouseAvailableQtyAsync(
    string companyId, string itemId, string warehouseId)
{
    var item = await GetInventoryItemAsync(companyId, itemId);

    var location = item.InventoryLocations?
        .FirstOrDefault(loc =>
            string.Equals(loc.warehouse_id, warehouseId,
                StringComparison.OrdinalIgnoreCase));

    return location?.qty_available ?? 0m;
}

Create a New Inventory Item

public async Task<InventoryItem> CreateInventoryItemAsync(InventoryItemCreateRequest request)
{
    // Step 1: Get template with defaults
    var templateResponse = await _httpClient.GetAsync($"{_baseUrl}/entity/inventory/new");
    templateResponse.EnsureSuccessStatusCode();
    var templateXml = await templateResponse.Content.ReadAsStringAsync();

    var serializer = new XmlSerializer(typeof(InventoryItem));
    using var templateReader = new StringReader(templateXml);
    var template = (InventoryItem)serializer.Deserialize(templateReader);

    // Step 2: Populate fields
    template.item_id          = request.ItemId;
    template.item_desc        = request.Description;
    template.unit_of_measure  = request.Uom;
    template.product_group_id = request.ProductGroupId;
    template.standard_cost    = request.StandardCost;
    template.list_price       = request.ListPrice;
    template.weight           = request.Weight;
    template.hazmat_flag      = request.IsHazmat ? "Y" : "N";
    template.serialized_flag  = request.IsSerialized ? "Y" : "N";
    template.lot_controlled   = request.IsLotControlled ? "Y" : "N";

    // Step 3: Serialize and POST
    using var writer = new StringWriter();
    serializer.Serialize(writer, template);
    var xml = writer.ToString();

    var content = new StringContent(xml, Encoding.UTF8, "application/xml");
    var response = await _httpClient.PostAsync($"{_baseUrl}/entity/inventory/", content);
    response.EnsureSuccessStatusCode();

    var responseXml = await response.Content.ReadAsStringAsync();
    using var reader = new StringReader(responseXml);
    return (InventoryItem)serializer.Deserialize(reader);
}

Notes and Common Pitfalls

Do not write quantities directly

qty_on_hand, qty_allocated, and qty_on_order are maintained by P21 transaction processing (receiving, order allocation, shipping). Do not attempt to set these via PUT. Use the appropriate P21 transaction endpoints (receipts, adjustments) to change on-hand quantities.

Checking availability before order entry

Before creating an order line, call GET on the inventory item and sum qty_available across the relevant warehouse locations. This gives you an available-to-promise figure, though it is a point-in-time snapshot and can change between your check and order creation.

Serialized and lot-controlled items

Items with serialized_flag=Y or lot_controlled=Y require additional data at the time of order fulfillment and receipt. These items may have additional nested collections (serial numbers, lot records) that appear in the response for items with those flags set. Plan for this in your data model.

Location vs. warehouse

warehouse_id identifies the warehouse (e.g., MAIN, EAST). location_id identifies the bin or zone within that warehouse (e.g., A-01-01). Not all P21 installations use granular bin-level locations — some use a single default location per warehouse.