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¶
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¶
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.