URL class: Inventory — /Windward/WebAPI/Inventory/...
Source of truth: ServerMethodsInventory.pas, CM_Inventory.pas, AO_Inventory.pas, CM_STA.pas, CM_AltSupply.pas, CM_Barcodes.pas, AO_Part_Prices.pas.
The older parts operations on TServerMethodsWebAPI (Get_Parts, Get_Parts_V2, List_Parts, Parts_Read, Parts_Insert, Parts_Update, Get_Stock, Get_Kits, Perform_Stock_Adjustment, the superseding-parts and part-image operations) are covered in The Legacy TServerMethodsWebAPI Surface.
Operations
| Verb | Path | Delphi method | Envelope |
|---|---|---|---|
| GET | /Inventory/Inventory_Handshake | Inventory_Handshake | ad hoc |
| GET | /Inventory/GetInventoryRecordCount | GetInventoryRecordCount | ad hoc |
| GET | /Inventory/GetPriceCodes | GetPriceCodes | ad hoc |
| GET | /Inventory/Inventory/{InventoryId} | Inventory | APIResponse + Inventory |
| GET | /Inventory/InventoryChanges | InventoryChanges | APIResponse + Inventory |
| POST | /Inventory/addInventory | updateaddInventory | APIResponse + Inventory |
POST /Inventory/addInventory
The two things to know before you start
First. A part that cannot be matched or inserted is reported as a success. Unlike every other domain, inventory's no-op branch sets ActionSuccess := true:
{ "RecordId": "0",
"RecordDesc": "Operation = [No Op] Invalid Data provided!",
"ActionResult": "",
"ActionSuccess": true }or, where the matcher built a description:
{ "RecordDesc": "Category 01.02, Part: WIDGET-1, Item: , SuppPart: ",
"ActionResult": "Category *and* one of the part numbers required to insert new record; Inventory record skipped",
"ActionSuccess": true }You must inspect
RecordIdandActionResult, not justActionSuccess, on this endpoint. A skipped part hasRecordIdof"0"and anActionResultcontainingskipped, or aRecordDescbeginningOperation = [No Op].
Second. To insert a new part you must send both SubCategory and PartNumber. Anything less is skipped. See the match cascade in The Upsert Model.
Request shape
{
"ConnectionInfo": { "TerminalNumber": 1 },
"Inventory": [
{
"InventoryId": 0,
"SubCategory": "01.02",
"PartNumber": "WIDGET-1",
"ItemNumber": "",
"SupplierPartNumber": "SUP-991",
"VendorId": 42,
"Description": "Blue widget",
"Description2": "",
"UnitOfMeasure": "EA",
"Wholesale": 12.50,
"Freight": 0.75,
"Duty": 0.00,
"Extra": 0.00,
"Landed": 13.25,
"Weight": 1.2,
"eCommerce": true,
"MarkedDeleted": false,
"Prices": [
{ "Department": 0, "Level": 1, "RegPriceCode": "F", "Regular": 24.99 }
],
"Barcodes": [ { "Barcode": "0123456789012", "SupplierId": 42 } ],
"AltSupply": [ { "SupplierId": 77, "SupplierPartNumber": "ALT-1", "Cost": 12.10 } ],
"STA": [ { "STANumber": "SPRING26", "Startdate": "2026-03-01T00:00:00" } ],
"InventoryFreeFormGroup1": [ { "FreeFormID": 1, "FreeFormData": "24 months" } ],
"InventorySellingComment": "Long selling copy",
"InventoryWebComment": "Web copy",
"InventoryInvoiceComment": "Prints on the invoice"
}
]
}Supplier resolution
The supplier is resolved in this order:
VendorId— used only if it resolves to a real supplier account (IsValidSupplierID). An invalid id is ignored silently.- If no supplier is set yet,
VendorNameis looked up by exact business name on accounts of typeP. No fuzzy matching, no partial match.
On update, the supplier may be locked. A [System Five Web API Service] INI setting, Update Inventory Supplier, controls whether an update is allowed to change a part's main supplier:
| Setting | Behaviour on update |
|---|---|
Y (default) | VendorId / VendorName in the payload replaces the part's supplier |
N | The supplier is left unchanged, silently. Your payload's supplier is ignored. |
Inserts always set the supplier. There is no response field telling you which mode the server is in — if supplier changes appear not to stick, this setting is the reason.
Brand
BrandName is resolved through the same supplier-name lookup. An empty or unrecognised brand name leaves the existing brand alone rather than clearing it — there is no way to clear a brand through this endpoint.
Costs
Wholesale, Extra, Freight, Duty and Landed are applied to cost tier 1.
Note the asymmetry:
- On insert, costs are applied twice — once by the general field population and again by
ProcessCostsafter the virtual-warehouse lookup. This is deliberate: the virtual warehouse can overwrite cost, andProcessCostsputs your values back. - On update,
ProcessCostsdoes not run, and the virtual-warehouse lookup does not run either. Costs are applied once.
Virtual warehouse (insert only)
On insert, after the identifying fields are populated, UpdateFromVirtualWarehouse looks the part up in the virtual-warehouse/transfer table by its identifiers and copies matching data onto the new part. This means a newly inserted part can come back with values you did not send. It does not happen on update.
Prices
Prices is an array; each entry targets one price level in one department:
| Field | Type | Notes |
|---|---|---|
Department | integer | Required — an entry without it is skipped entirely |
Level | integer | Required, and must satisfy 0 <= Level < System5.PriceCount |
RegPriceCode | string | Price calculation method, first character only |
Regular | number | The regular price, when the code is a money code |
RegPricePercent | number | The regular percent, when the code is a margin/markup/discount code |
SalePriceCode | string | As above, for the sale price |
Sale | number | |
SalePricePercent | number |
The rules that matter:
- The price code decides which field is read. Money codes read
Regular/Sale; margin, markup and discount codes readRegPricePercent/SalePricePercent. - If you omit the price code, the part's existing code is used, so the same payload can behave differently on two parts.
- If the expected field is missing, the code falls back to the other one and derives the value. For
$ markup from landedand$ markup from cash price schedule, the percent value is added to landed cost / cash price to produce the effective price before comparison. - A value is written only when it differs from the current value. Sending the same price twice is a genuine no-op — the part's price-change date does not move.
- Negative values are rejected by the level helper (
>= 0required). - Where the value is a percent and the dataset has "exclude inc-tax on retail" behaviour (Australian datasets with
SQ_ExcludeIncTaxOnInvRetail), the tax-exclusive schedule is written instead of the normal one. The same payload therefore behaves differently on an Australian dataset. - After the array is processed the part's cached price-schedule list is cleared, forcing System Five to recompute.
Call GET /Inventory/GetPriceCodes to discover the valid code characters and their descriptions for the dataset before writing prices.
Comments — three different stores
| Field | Stored as |
|---|---|
InventorySellingComment | Comment record on the inventory file |
InventoryWebComment | Comment record on file number inventory + 1000 |
InventoryInvoiceComment | ComData record on the inventory file |
They are separate stores with separate lifecycles; writing one does not affect the others.
Child collections
STA, AltSupply and Barcodes are child arrays, processed through PutChildRecords.
None of them produce result entries. They can fail silently. Read the part back with
DetailedResponse=Yif they matter.
Behaviour specific to each:
Barcodesdefault to insert. The matcher deliberately assumes a new barcode unlessAltSupplyIdis supplied, because a part may legitimately have many barcodes per supplier. It does scan for an identical barcode value to avoid exact duplicates. Re-posting a part with its barcode list will otherwise accumulate rows.AltSupplymatches onAltSupplyId, else on part + supplier + record type.SupplierNamemay be sent instead ofSupplierId; the lookup is exact match only.STAmatches onSTAId, else on part + active +Startdate(+STANumberwhen supplied).
Free-form fields
InventoryFreeFormGroup1 and InventoryFreeFormGroup2 are arrays of { "FreeFormID": n, "FreeFormData": "..." }. Ids are dataset-specific; use ?FreeFormNameMap=Y on a GET to discover which id maps to which label.
Order of operations
Update: populate fields -> prices -> free-forms -> STA -> alt supply -> barcodes -> save.
Insert: create (via GetDefaultInventryCat to pick the creation mode) -> lock -> populate fields -> virtual warehouse -> costs -> prices -> free-forms -> STA -> alt supply -> barcodes -> save.
If part creation itself fails, the message is whatever System Five's CreateErrorMessage produces, or:
Failed to Add Part: {message}ActionResult values
| Value | Meaning |
|---|---|
Inserted Record | Created. Note: no "subject to System 5 verification" suffix here, unlike invoices and customers |
Updated Record | Updated |
An error occurred during the insert of the record | Btrieve write failed |
An error occurred during the update of the record | Btrieve write failed |
No Part or Item or Supplier Part Number provided. ... | Skipped — no identifier |
Category *and* one of the part numbers required to insert new record; ... | Skipped — insufficient data to insert |
Failed to Add Part: ... | Category/creation-mode resolution failed |
GET /Inventory/Inventory/{InventoryId}
{InventoryId} of 0 returns all parts. Paginate.
Query parameters
| Parameter | Notes |
|---|---|
Fields | Comma-separated names from the response model |
MarkedDeleted=Y | Include parts marked for deletion. Default excludes them |
eCommerceExport=Y | Only parts flagged for e-commerce export |
PriceLevel={n} | A specific price level, or -1 for all levels |
Department={n} | Stock, prices and sale dates for that department, or -1 for all. Requires Departmental Inventory to be enabled in the dataset |
CurrencyCode={n} | Prices and costs in that currency. Requires Multi-Currency to be enabled |
FreeFormNameMap=Y | Adds the FreeFormHeaders id-to-label map |
PageSize / PageNumber | Both or neither; PageNumber is 1-based |
PriceLevel, Department and CurrencyCode are parsed with StrToInt — a non-numeric value produces a conversion error in the response body, not a sensible default.
The Department and CurrencyCode parameters do not fail loudly when the corresponding System Five feature is not configured; you simply get the default data. If departmental figures look wrong, confirm the feature is enabled in the dataset before debugging the call.
Response
Array key Inventory. Records include identity, description, costs, Prices (one entry per level/department), InStock, StartSaleDate / EndSaleDate, AltSupply, Barcodes, STA, the free-form groups and the three comment fields.
Pagination uses the same forward-scan approach described in Customers — deep pages are progressively more expensive and hold the global lock.
GET /Inventory/InventoryChanges
Parts changed on or after EffectiveDateTime, via System Five record-state tracking.
Same prerequisites and same envelope-switching behaviour as GET /Customer/CustomerChanges — see Customers. Specifically:
- Record state tracking must be enabled, or you get the legacy envelope with
Record State tracking has not been enabled for this database; .... - An unparseable date returns the legacy envelope.
- No changes returns the legacy envelope with a
Successresponse. - Only a non-empty result uses the
APIResponse+Inventoryshape.
Accepts PriceLevel, eCommerceExport, Department, CurrencyCode, PageSize and PageNumber in addition to EffectiveDateTime.
This is the correct endpoint for ongoing catalogue synchronisation. It pages the change list rather than the parts file, so it does not suffer the forward-scan cost.
GET /Inventory/GetInventoryRecordCount
{ "SystemFive API Record Count": "Inventory", "Record Count": "48213" }The count includes parts marked for deletion, regardless of the MarkedDeleted parameter. If you page Inventory/0 without MarkedDeleted=Y, this count will be higher than the number of records you receive — do not use it to compute page counts for a filtered fetch.
Like the customer count, it is computed by walking the file.
GET /Inventory/GetPriceCodes
Returns the price calculation methods configured for the dataset:
{
"SystemFive API Price Codes": "Inventory",
"F": "Fixed",
"M": "Margin",
"U": "Markup",
"...": "..."
}An ad hoc shape: the code characters are top-level keys of the same object as the marker pair, not an array. Iterate the object's keys and skip SystemFive API Price Codes.
Call this before writing Prices, since the valid codes and their meanings are dataset-configurable and RegPriceCode / SalePriceCode take only the first character.
Field reference
Identity
| Field | Type | Notes |
|---|---|---|
InventoryId | integer | Match key 1 |
PartNumber | string | Match key; required with SubCategory to insert |
ItemNumber | string | Match key |
SupplierPartNumber | string | Match key |
SubCategory | string | System Five ledger-style category, e.g. 01.02. Required to insert |
VendorId | integer | Supplier account unique; ignored if invalid |
VendorName | string | Exact-match supplier lookup, used only when VendorId did not resolve |
BrandId / BrandName | integer / string | Brand; unknown names are ignored, never cleared |
Description and physical
Description, Description2, UnitOfMeasure, Size1, Size2, Size3, Measurement1, Measurement2, Weight, PackSize
Costs
Wholesale, Extra, Freight, Duty, Landed (all numbers, cost tier 1)
Flags
| Field | Type |
|---|---|
MarkedDeleted | boolean |
eCommerce | boolean |
Prices (Prices[])
Department, Level, RegPriceCode, Regular, RegPricePercent, RegPriceDesc, SalePriceCode, Sale, SalePricePercent, SalePriceDesc, PriceLevelName
Comments
InventorySellingComment, InventoryWebComment, InventoryInvoiceComment
Child collections
STA[], AltSupply[], Barcodes[], InventoryFreeFormGroup1[], InventoryFreeFormGroup2[]
Read-only in responses
InStock, StartSaleDate, EndSaleDate, LastPriceChg, KitType, InventoryFreeFormHeaders, PriceCodes


