# Edara API v3: Stock Items

Read the [guide](../guide.md) first: authentication, paging, errors, rate limits and the shared `PagedResult`, `BatchResult` and `*Reference` shapes.

- `GET /v3/stock-items`: Lists stock items.
- `POST /v3/stock-items/list`: Lists stock items by filters.
- `GET /v3/stock-items/{id}`: Gets a stock item by id.
- `GET /v3/stock-items/search`: Searches stock items.
- `POST /v3/stock-items`: Creates a stock item.
- `POST /v3/stock-items/batch`: Creates stock items in batch.
- `PUT /v3/stock-items/{id}`: Updates a stock item by id.
- `PUT /v3/stock-items/code/{code}`: Updates a stock item by code.
- `PUT /v3/stock-items/external-id`: Updates a stock item external id.
- `PUT /v3/stock-items/batch/by-id`: Batch updates stock items by id.
- `PUT /v3/stock-items/batch/by-code`: Batch updates stock items by code.
- `PUT /v3/stock-items/batch/by-sku`: Batch updates stock items by SKU.
- `DELETE /v3/stock-items/{id}`: Deletes a stock item by id.
- `PUT /v3/stock-items/{id}/deactivate`: Deactivates a stock item by id.
- `PUT /v3/stock-items/code/{code}/deactivate`: Deactivates a stock item by code.
- `POST /v3/stock-items/link-to-parent`: Links a stock item to a parent.
- `GET /v3/stock-items/{stockItemId}/validate/serial/{serialNo}/returns/{customerId}`: Validates an item serial for return.
- `GET /v3/stock-items/{stockItemId}/validate/serial/{serialNo}/returned/{rrCode}`: Validates an item serial returned.
- `GET /v3/stock-items/balances`: Lists stock item balances.
- `GET /v3/stock-items/warehouse-summary`: Gets stock item sales and stock position by warehouse.
- `GET /v3/stock-items/{stockItemId}/balance`: Gets a stock item balance.
- `GET /v3/stock-items/{stockItemId}/balance/warehouses/{warehouseId}`: Gets a stock item balance by warehouse.
- `GET /v3/stock-items/{stockItemId}/cost`: Gets a stock item cost.
- `GET /v3/stock-items/cost/by-skus`: Gets stock item costs by SKUs.
- `GET /v3/stock-items/cost/by-part-numbers`: Gets stock item costs by part numbers.
- `GET /v3/stock-items/cost/by-ids`: Gets stock item costs by ids.
- `GET /v3/stock-items/{stockItemId}/global-balance`: Gets a stock item global balance by id.
- `GET /v3/stock-items/global-balances`: Gets a stock item global balance.

## Endpoints

### `GET /v3/stock-items`: Lists stock items.

Operation `GetStockItems` · permission `read:stock-item`

**Query string**: `GetStockItemsQuery`

| field | type | default | validation | description |
|---|---|---|---|---|
| `offset` | `int?` | 0 | >= 0 | Number of items to skip before returning results. Defaults to 0. |
| `limit` | `int?` | 100 | 1..1000 (out of range → 400) | Maximum number of items to return. Defaults to 100, maximum 1000. |
| `ids` | `List<int>` | new List<int>() | each: GreaterThan(0) | Optional stock-item identifier filter. |
| `codes` | `List<string>` | new List<string>() | each: Must(code => !string.IsNullOrWhiteSpace(code)) | Optional codes filter. |
| `partNumbers` | `List<string>` | new List<string>() | each: Must(partNumber => !string.IsNullOrWhiteSpace(partNumber)) | Optional part-numbers filter. |
| `externalIds` | `List<string>` | new List<string>() | each: Must(externalId => !string.IsNullOrWhiteSpace(externalId)) | Optional external-ids filter. |
| `description` | `string` |  | MaximumLength(500) .Must(description => !string.IsNullOrWhiteSpace(description)) .When(x => x.Description != null) | Optional description contains filter. |
| `code` | `string` |  | MaximumLength(200) .Must(code => !string.IsNullOrWhiteSpace(code)) .When(x => x.Code != null) | Optional exact-code filter. |
| `sku` | `string` |  | MaximumLength(50) .Must(sku => !string.IsNullOrWhiteSpace(sku)) .When(x => x.Sku != null) | Optional SKU filter. |
| `partNumber` | `string` |  | MaximumLength(500) .Must(partNumber => !string.IsNullOrWhiteSpace(partNumber)) .When(x => x.PartNumber != null) | Optional part-number filter. |
| `externalId` | `string` |  | MaximumLength(128) .Must(externalId => !string.IsNullOrWhiteSpace(externalId)) .When(x => x.ExternalId != null) | Optional exact external-id filter. |
| `classificationId` | `int?` |  | GreaterThan(0) .When(x => x.ClassificationId.HasValue) | Optional classification identifier filter. |
| `updateDate` | `DateTime?` |  |  | Updated-after filter. |

**Responses**: OK `PagedResult<StockItemResponse>`: Paged list of stock items.

**Notes**

- Only one filter is applied per request. The first one present in this order wins, and any later one is ignored without an error: `code`, `sku`, `partNumber`, `externalId`, `classificationId`, `codes`, `partNumbers`, `ids`, `externalIds`.
- `code`, `partNumber` and `externalId` are exact matches, not partial ones. `sku` is an exact match that ignores case and returns at most one item.
- When one of these filters finds nothing, you get `200` with an empty `items` and `totalCount` 0, not `404`.
- A `code` lookup always applies your user's item data permissions, even when the organization has not turned them on for stock items. An item your user is not permitted to see comes back as an empty result.
- `codes`, `partNumbers`, `ids` and `externalIds` are exact matches. Values are trimmed, duplicates are removed ignoring case, and unknown values are skipped without an error.
- These list filters also return inactive items and grouping items.
- `classificationId` returns only items directly in that classification, not items in its child classifications. Inactive items and grouping items are included.
- With any of these filters, `offset` and `limit` are ignored and every match comes back in one response. `totalCount` is the number of matches, and `page` and `pageSize` only repeat your request.
- With none of these filters, or with only `description` or `updateDate`, you get the default list. It is paged, ordered by id, and `totalCount` is the full number of matches.
- The default list never includes grouping items, but it does include inactive items.
- `description` is a partial match (contains). `updateDate` returns items created or updated strictly after that time.
- `sku` and `partNumber` never work as partial-match filters on this endpoint.
- The default list applies item data permissions only when the organization has turned them on. With them on, a user with no permitted items gets `404`.
- The default list returns `404` when nothing matches, unlike the filters above, which return `200` with an empty `items`. Handle both.
- Only the first 100 characters of `description` are used for matching, although longer values are accepted.
- `childItems` is filled only for grouping items, and the units of measure list only for items that have a unit of measure chain.
- `code`, `sku`, `partNumber` and `externalId` can be null in the response.
- Results from the filters above have no guaranteed order.

---

### `POST /v3/stock-items/list`: Lists stock items by filters.

Operation `GetStockItemsByFilters` · permission `read:stock-item`

**Body**: `StockItemFiltersRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `offset` | `int?` | 0 | >= 0 | Number of items to skip before returning results. Defaults to 0. |
| `limit` | `int?` | 100 | 1..1000 (out of range → 400) | Maximum number of items to return. Defaults to 100, maximum 1000. |
| `description` | `string` |  | MaximumLength(500) .Must(d => !string.IsNullOrWhiteSpace(d)) .When(x => x.Description != null) | Description text to match. |
| `sku` | `string` |  | MaximumLength(50) .Must(s => !string.IsNullOrWhiteSpace(s)) .When(x => x.Sku != null) | SKU to match. |
| `partNumber` | `string` |  | MaximumLength(500) .Must(p => !string.IsNullOrWhiteSpace(p)) .When(x => x.PartNumber != null) | Part number to match. |
| `ids` | `List<int>` | new List<int>() | each: GreaterThan(0) | Stock-item identifiers to resolve. |

**Responses**: OK `PagedResult<StockItemResponse>`: Paged list of stock items.

**Notes**

- Results are ordered by id and paged with `offset` and `limit`. `totalCount` is the full number of matches.
- An `offset` past the last item returns `totalCount` 0, not the real total.
- `description`, `sku` and `partNumber` are partial matches (contains), and `ids` is an exact list. All the filters you send must match together, and an empty `ids` list means no id filter.
- Inactive items are returned. Grouping items are not, and neither are items with no value for the grouping flag.
- Only the first 100 characters of `description` and `partNumber` are used for matching, although longer values are accepted.
- A filter that contains only spaces returns `400`, and so does an id of 0 or less.
- Item data permissions are applied unless the organization has explicitly turned them off for stock items.
- No match returns `200` with an empty `items`, never `404`.

---

### `GET /v3/stock-items/{id}`: Gets a stock item by id.

Operation `GetStockItemById` · permission `read:stock-item`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The stock item id. |

**Responses**: OK `StockItemResponse`: The stock item.

**Notes**

- Inactive items and grouping items are returned.
- The item picture is never included in the response.
- The global opening balance is the sum of the item's opening balances across all warehouses.
- `childItems` is filled only for grouping items, and the units of measure list only for items that have a unit of measure chain.

---

### `GET /v3/stock-items/search`: Searches stock items.

Operation `SearchStockItems` · permission `read:stock-item`

> - Separator selects the match mode: comma (`a,b,c`) is OR (default), plus (`a+b`) is AND (every token must match), `*` is wildcard (returns the full active list).
> - Tokens are matched against the search fields enabled by tenant settings: `AllowSearchStockItemsByPartNumber`, `AllowSearchStockItemsByItemCode`, `AllowSearchStockItemsByClassificationCode`, `AllowSearchStockItemsByDescription`, `AllowSearchStockItemsBySKU`, `AllowSearchStockItemsByOENumber`.
> - Returns the full matched set (no paging). Inactive items are excluded.
> - Example: `keywords=screwdriver,hammer` returns items matching either token.

**Query string**: `SearchStockItemsQuery`

| field | type | default | validation | description |
|---|---|---|---|---|
| `keywords` | `string` |  | NotEmpty() | Tokenized expression: `,` = OR, `+` = AND, `*` = all. Example: `screwdriver,hammer`. |

**Responses**: OK `IReadOnlyCollection<StockItemResponse>`: Matched stock items.

**Notes**

- At most 1000 items are returned. There is no paging and no `totalCount`, so you cannot tell a cut-off result from a complete one.
- Results are sorted by classification code, then description. When more than 1000 items match, which 1000 you get is not defined.
- The whole `keywords` value, trimmed and not split, is first tried as an exact part number. If any item matches, only those items are returned and no keyword search runs.
- Next, by default, the keywords are matched against unit of measure part numbers only, and the other fields are searched only if that finds nothing. Two organization settings control this step, so it can differ between organizations.
- If `keywords` contains a plus sign, it is split on plus signs and every part must match. Otherwise it is split on commas and any part can match.
- When `keywords` has both, only the plus signs split it. For example, a+b,c gives the parts a and b,c.
- A `keywords` value of exactly one asterisk skips matching and returns up to 1000 items.
- Each part is matched against the fields the organization has enabled for search, out of classification code, part number, SKU, OE number, code and description. A part matches when any enabled field matches.
- Each field is a partial match by default, and an organization setting can make it an exact match. These settings change what is searched, not which fields are returned, so code defensively.
- Inactive items and grouping items are never returned.
- Blank `keywords` is rejected with a validation error. No match returns `200` with an empty array, never `404`.
- If the organization has turned off search on classification code, part number, SKU, code and description, the search returns nothing, even when OE number is enabled.

---

### `POST /v3/stock-items`: Creates a stock item.

Operation `CreateStockItem` · permission `create:stock-item`

> - **IsCurrent** on each `ChildItems` entry is **ignored on create**: it only applies to Update calls that transform a simple item into a grouping item.

**Body**: `CreateStockItemRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `code` | `string` |  | MaximumLength(200) .Must(c => !string.IsNullOrWhiteSpace(c)).When(x => x.Code != null) | The stock-item code (unique per tenant). |
| `description` | `string` |  | NotEmpty().MaximumLength(500) | The stock-item description. Required. |
| `otherLanguageDescription` | `string` |  | MaximumLength(500) .Must(d => !string.IsNullOrWhiteSpace(d)).When(x => x.OtherLanguageDescription != null) | The description in the secondary language (e.g. Arabic). |
| `sku` | `string` |  | MaximumLength(50) .Must(s => !string.IsNullOrWhiteSpace(s)).When(x => x.Sku != null) | The stock-keeping unit identifier (unique per tenant when supplied). |
| `partNumber` | `string` |  | MaximumLength(500) .Must(p => !string.IsNullOrWhiteSpace(p)).When(x => x.PartNumber != null) | The manufacturer or vendor part number. |
| `oENumber` | `string` |  | MaximumLength(200) | The OEM (original equipment manufacturer) number. |
| `externalId` | `string` |  | MaximumLength(128) .Must(e => !string.IsNullOrWhiteSpace(e)).When(x => x.ExternalId != null) | An external identifier supplied by the caller for reconciliation. |
| `classificationId` | `int?` |  | GreaterThan(0).When(x => x.ClassificationId.HasValue) | The id of the classification node this item belongs to. |
| `brandId` | `int?` |  | GreaterThan(0).When(x => x.BrandId.HasValue) | The id of the brand. |
| `parentItemId` | `int?` |  | GreaterThan(0).When(x => x.ParentItemId.HasValue) | The id of the parent item, when this item is a variant. |
| `isGroupingItem` | `bool?` |  |  | Whether this item is a grouping (parent) item with variants underneath. |
| `tags` | `string[]` |  | Must(tags => tags == null \|\| tags.All(tag => !string.IsNullOrWhiteSpace(tag)))  .Must(tags => tags == null \|\| string.Join(",", tags).Length <= 4000) | The tags to assign to the item. |
| `singleUomId` | `int?` |  | GreaterThan(0).When(x => x.SingleUomId.HasValue) | The id of the single UOM when the item is not part of a UOM chain. |
| `unitOfMeasureChainId` | `int?` |  | GreaterThan(0).When(x => x.UnitOfMeasureChainId.HasValue) | The id of the UOM chain the item uses. |
| `unitOfMeasure` | `int?` |  | GreaterThan(0).When(x => x.UnitOfMeasure.HasValue) | The id of the resolved unit of measure for this item. |
| `stockItemUnitOfMeasures` | `List<StockItemUnitOfMeasureRequest>` |  | each: SetValidator(new StockItemUnitOfMeasureRequestValidator()) | Per-UOM pricing and identifiers when the item uses a UOM chain. |
| `weight` | `decimal?` |  |  | The item weight. |
| `height` | `decimal?` |  |  | The item height. |
| `width` | `decimal?` |  |  | The item width. |
| `length` | `decimal?` |  |  | The item length. |
| `warranty` | `decimal?` |  |  | The warranty period (interpretation per tenant settings). |
| `validityPeriodInDays` | `decimal?` |  |  | The validity period in days after which the item is treated as expired. |
| `itemLocation` | `string` |  | MaximumLength(200) | The default storage location of the item within a warehouse. |
| `imageUrl` | `string` |  | MaximumLength(200) | The URL to the item image. |
| `note` | `string` |  | MaximumLength(500) | Free-form notes captured against the item. |
| `datasheet` | `string` |  |  | The URL or text of the item datasheet. |
| `attachedDocument` | `string` |  |  | The URL of an attached document. |
| `salesPrice` | `decimal?` |  |  | The default sales price. |
| `purchasePrice` | `decimal?` |  |  | The default purchase price. |
| `dealerPrice` | `decimal?` |  |  | The dealer price. |
| `superDealerPrice` | `decimal?` |  |  | The super-dealer price. |
| `minimumPrice` | `decimal?` |  |  | The minimum allowed sales price. |
| `referencePrice` | `decimal?` |  |  | The reference price used in margin calculations. |
| `unitPrice` | `decimal?` |  |  | The unit price; used by some legacy callers. |
| `openingAverageCost` | `decimal?` |  |  | The opening average cost; this is copied into the current average cost. |
| `openingFifo` | `decimal?` |  |  | The opening FIFO cost; this is copied into the current FIFO valuation. |
| `openingLifo` | `decimal?` |  |  | The opening LIFO cost; this is copied into the current LIFO valuation. |
| `openingLastCost` | `decimal?` |  |  | The opening last cost; this is copied into the current last cost. |
| `averageCost` | `decimal?` |  |  | The current average cost. |
| `fifo` | `decimal?` |  |  | The current FIFO valuation. |
| `lifo` | `decimal?` |  |  | The current LIFO valuation. |
| `lastCost` | `decimal?` |  |  | The current last cost. |
| `globalReorderingPoint` | `decimal?` |  |  | The global reordering point across all warehouses. |
| `globalMinimumPoint` | `decimal?` |  |  | The global minimum stock point across all warehouses. |
| `globalMaximumPoint` | `decimal?` |  |  | The global maximum stock point across all warehouses. |
| `taxId` | `int?` |  | GreaterThan(0).When(x => x.TaxId.HasValue) | The id of the default sales tax for this item. |
| `withholdingTaxId` | `int?` |  | GreaterThan(0).When(x => x.WithholdingTaxId.HasValue) | The id of the withholding tax for this item, when applicable. |
| `eInvoiceCode` | `string` |  | MaximumLength(50) | The e-invoice item code (used by Egypt/KSA e-invoicing). |
| `eInvoiceCodeType` | `string` |  | MaximumLength(50) | The e-invoice code type (e.g. GS1, EGS). |
| `enforceSerialEntry` | `bool?` |  |  | Whether issuing this item must capture serial numbers. |
| `enforceBatchNumberDateEntry` | `bool?` |  |  | Whether issuing this item must capture a batch number and production date. |
| `enforceExpirationDateEntry` | `bool?` |  |  | Whether issuing this item must capture an expiration date. |
| `autofillBatchNumber` | `bool?` |  |  | Whether batch numbers should be auto-generated when not supplied. |
| `allowSerialDuplication` | `bool?` |  |  | Whether the same serial number may appear on more than one document. |
| `salesPriceDiscount` | `decimal?` |  |  | The discount value applied to the sales price (interpreted per `SalesPriceDiscountType`). |
| `salesPriceDiscountType` | `int?` |  |  | The discount type for the sales price (0 = amount, 1 = percentage). |
| `salesPriceDiscountIsLimited` | `bool?` |  |  | Whether the sales-price discount is bounded by the date range below. |
| `salesPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the sales-price discount window. |
| `salesPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the sales-price discount window. |
| `dealerPriceDiscount` | `decimal?` |  |  | The discount value applied to the dealer price. |
| `dealerPriceDiscountType` | `int?` |  |  | The discount type for the dealer price (0 = amount, 1 = percentage). |
| `dealerPriceDiscountIsLimited` | `bool?` |  |  | Whether the dealer-price discount is bounded by the date range below. |
| `dealerPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the dealer-price discount window. |
| `dealerPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the dealer-price discount window. |
| `superDealerPriceDiscount` | `decimal?` |  |  | The discount value applied to the super-dealer price. |
| `superDealerPriceDiscountType` | `int?` |  |  | The discount type for the super-dealer price (0 = amount, 1 = percentage). |
| `superDealerPriceDiscountIsLimited` | `bool?` |  |  | Whether the super-dealer-price discount is bounded by the date range below. |
| `superDealerPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the super-dealer-price discount window. |
| `superDealerPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the super-dealer-price discount window. |
| `dynamicPropertiesInfo` | `List<DynamicPropertyInfo>` |  |  | The dynamic-property values to assign to the item. |
| `childItems` | `List<StockItemChildRequest>` |  |  | The child variants of this item when it is a grouping item. |

**Responses**: Created `StockItemResponse`: Stock item created.

**Business errors** (HTTP 409, match on `errorCode`):

- `CannotAssignInactiveTax`: An inactive tax cannot be assigned.
- `CodeContainsInvalidSpecialCharacters`: The stock-item code contains invalid special characters.
- `DescriptionContainsInvalidSpecialCharacters`: The stock-item description contains invalid special characters.
- `DupplicatedSKU`: Another stock item already uses the same SKU.
- `InvalidTaxScopeForItem`: The tax scope is not valid for this stock item.
- `InvalidTaxTypeForItem`: The tax type is not valid for this stock item.
- `MustSelectValidTaxType`: A valid tax type must be selected.
- `OtherLangDescriptionContainsInvalidSpecialCharacters`: The other-language description contains invalid special characters.
- `StockItemBrandRequired`: A brand must be assigned to the stock item.
- `StockItemCodeExists`: Another stock item already uses the same code.
- `StockItemCodeRequired`: The stock-item code is required.
- `StockItemDescriptionExists`: Another stock item already uses the same description.
- `StockItemPartNumberExists`: Another stock item already uses the same part number.
- `StockItemPartNumberRequired`: The stock-item part number is required.
- `StockItemSalesPriceMustBeGreaterThanZero`: The sales price must be greater than zero.
- `StockItemSKURequired`: The stock-item SKU is required.
- `StockitemsClassificationCannotBeNull`: A classification must be assigned to the stock item.
- `TaxNotFound`: The specified tax was not found.

**Notes**

- Only `description` is always required. Depending on organization settings, `code`, `sku`, `partNumber`, `brandId` and `salesPrice` can be required too, so a request that works in one organization can fail in another.
- A missing value that the organization requires gives `409` with `StockItemCodeRequired`, `StockItemSKURequired`, `StockItemPartNumberRequired`, `StockItemBrandRequired` or `StockItemSalesPriceMustBeGreaterThanZero`. `code` is only required when the organization does not generate codes automatically.
- When the organization requires a sales price, a `salesPrice` that fails the check can also be rejected with a plain `400` before the `409` check runs. Handle both.
- Uniqueness also depends on organization settings. Where the organization enforces it, a duplicate `code`, `description`, `partNumber` or `sku` gives `409` with `StockItemCodeExists`, `StockItemDescriptionExists`, `StockItemPartNumberExists` or `DupplicatedSKU`.
- The part number check also covers every `uomPartNumber` in `stockItemUnitOfMeasures`. `externalId` and barcodes are not checked for duplicates.
- If you omit `classificationId`, the organization's default classification is used when one is configured. Otherwise you get `409` `StockitemsClassificationCannotBeNull`.
- `brandId`, `classificationId` and `parentItemId` are not checked for existence. An id that does not exist gives `500` instead of a business error, so check these ids before you send them.
- Send `singleUomId` or `unitOfMeasureChainId`, not both. Both gives `400`. With neither, the organization's default single unit is used and `stockItemUnitOfMeasures` is ignored.
- `taxId` and `withholdingTaxId` must point to an existing, active tax that is not scoped to service items and has a sales or purchase account. Otherwise you get `409` with `TaxNotFound`, `CannotAssignInactiveTax`, `InvalidTaxScopeForItem` or `InvalidTaxTypeForItem`.
- When the organization is connected to e-invoicing or e-receipts, `taxId` is required. A missing one gives `409` `MustSelectValidTaxType`.
- The tax rate and withholding tax rate are copied onto the item when it is created.
- A negative opening cost gives `409` `InvalidOpeningCostValue`.
- On create, `averageCost`, `fifo`, `lifo` and `lastCost` in the request are ignored. They are set from the four opening cost fields.
- Unless the organization allows special characters in item descriptions, a `description` that contains a double quote, single quote, pipe, backslash, comma, less-than sign or greater-than sign gives `400`.
- The `409` codes `CodeContainsInvalidSpecialCharacters`, `DescriptionContainsInvalidSpecialCharacters` and `OtherLangDescriptionContainsInvalidSpecialCharacters` are listed for this endpoint but are not returned. The special character check gives `400`.
- For a grouping item (`isGroupingItem` set to true), `dynamicPropertiesInfo` is only accepted when the organization has dynamic properties enabled for stock items. Otherwise you get `400`.
- Every `dynamicValueId` must exist and belong to the `dynamicPropertyId` you state. Names you send are replaced with the stored ones.
- A grouping item needs at least one entry in `childItems`. Each child needs one value for every parent property, and no two children can have the same combination.
- One stock item is created per `childItems` entry, using that entry's code, part number, SKU and price. Each child must pass the same required and uniqueness rules. A child's `code` is cleared when the organization generates codes automatically, and children are created without an `externalId`.
- Tags in `tags` that do not exist yet are added to the organization's tag list.
- The create is all or nothing. If any part fails, including a unit of measure, a dynamic property, a child item or a tag, nothing is saved.

---

### `POST /v3/stock-items/batch`: Creates stock items in batch.

Operation `BatchInsertStockItems` · permission `create:stock-item`

**Body**: `List<CreateStockItemRequest>` (JSON array)

| field | type | default | validation | description |
|---|---|---|---|---|
| `code` | `string` |  | MaximumLength(200) .Must(c => !string.IsNullOrWhiteSpace(c)).When(x => x.Code != null) | The stock-item code (unique per tenant). |
| `description` | `string` |  | NotEmpty().MaximumLength(500) | The stock-item description. Required. |
| `otherLanguageDescription` | `string` |  | MaximumLength(500) .Must(d => !string.IsNullOrWhiteSpace(d)).When(x => x.OtherLanguageDescription != null) | The description in the secondary language (e.g. Arabic). |
| `sku` | `string` |  | MaximumLength(50) .Must(s => !string.IsNullOrWhiteSpace(s)).When(x => x.Sku != null) | The stock-keeping unit identifier (unique per tenant when supplied). |
| `partNumber` | `string` |  | MaximumLength(500) .Must(p => !string.IsNullOrWhiteSpace(p)).When(x => x.PartNumber != null) | The manufacturer or vendor part number. |
| `oENumber` | `string` |  | MaximumLength(200) | The OEM (original equipment manufacturer) number. |
| `externalId` | `string` |  | MaximumLength(128) .Must(e => !string.IsNullOrWhiteSpace(e)).When(x => x.ExternalId != null) | An external identifier supplied by the caller for reconciliation. |
| `classificationId` | `int?` |  | GreaterThan(0).When(x => x.ClassificationId.HasValue) | The id of the classification node this item belongs to. |
| `brandId` | `int?` |  | GreaterThan(0).When(x => x.BrandId.HasValue) | The id of the brand. |
| `parentItemId` | `int?` |  | GreaterThan(0).When(x => x.ParentItemId.HasValue) | The id of the parent item, when this item is a variant. |
| `isGroupingItem` | `bool?` |  |  | Whether this item is a grouping (parent) item with variants underneath. |
| `tags` | `string[]` |  | Must(tags => tags == null \|\| tags.All(tag => !string.IsNullOrWhiteSpace(tag)))  .Must(tags => tags == null \|\| string.Join(",", tags).Length <= 4000) | The tags to assign to the item. |
| `singleUomId` | `int?` |  | GreaterThan(0).When(x => x.SingleUomId.HasValue) | The id of the single UOM when the item is not part of a UOM chain. |
| `unitOfMeasureChainId` | `int?` |  | GreaterThan(0).When(x => x.UnitOfMeasureChainId.HasValue) | The id of the UOM chain the item uses. |
| `unitOfMeasure` | `int?` |  | GreaterThan(0).When(x => x.UnitOfMeasure.HasValue) | The id of the resolved unit of measure for this item. |
| `stockItemUnitOfMeasures` | `List<StockItemUnitOfMeasureRequest>` |  | each: SetValidator(new StockItemUnitOfMeasureRequestValidator()) | Per-UOM pricing and identifiers when the item uses a UOM chain. |
| `weight` | `decimal?` |  |  | The item weight. |
| `height` | `decimal?` |  |  | The item height. |
| `width` | `decimal?` |  |  | The item width. |
| `length` | `decimal?` |  |  | The item length. |
| `warranty` | `decimal?` |  |  | The warranty period (interpretation per tenant settings). |
| `validityPeriodInDays` | `decimal?` |  |  | The validity period in days after which the item is treated as expired. |
| `itemLocation` | `string` |  | MaximumLength(200) | The default storage location of the item within a warehouse. |
| `imageUrl` | `string` |  | MaximumLength(200) | The URL to the item image. |
| `note` | `string` |  | MaximumLength(500) | Free-form notes captured against the item. |
| `datasheet` | `string` |  |  | The URL or text of the item datasheet. |
| `attachedDocument` | `string` |  |  | The URL of an attached document. |
| `salesPrice` | `decimal?` |  |  | The default sales price. |
| `purchasePrice` | `decimal?` |  |  | The default purchase price. |
| `dealerPrice` | `decimal?` |  |  | The dealer price. |
| `superDealerPrice` | `decimal?` |  |  | The super-dealer price. |
| `minimumPrice` | `decimal?` |  |  | The minimum allowed sales price. |
| `referencePrice` | `decimal?` |  |  | The reference price used in margin calculations. |
| `unitPrice` | `decimal?` |  |  | The unit price; used by some legacy callers. |
| `openingAverageCost` | `decimal?` |  |  | The opening average cost; this is copied into the current average cost. |
| `openingFifo` | `decimal?` |  |  | The opening FIFO cost; this is copied into the current FIFO valuation. |
| `openingLifo` | `decimal?` |  |  | The opening LIFO cost; this is copied into the current LIFO valuation. |
| `openingLastCost` | `decimal?` |  |  | The opening last cost; this is copied into the current last cost. |
| `averageCost` | `decimal?` |  |  | The current average cost. |
| `fifo` | `decimal?` |  |  | The current FIFO valuation. |
| `lifo` | `decimal?` |  |  | The current LIFO valuation. |
| `lastCost` | `decimal?` |  |  | The current last cost. |
| `globalReorderingPoint` | `decimal?` |  |  | The global reordering point across all warehouses. |
| `globalMinimumPoint` | `decimal?` |  |  | The global minimum stock point across all warehouses. |
| `globalMaximumPoint` | `decimal?` |  |  | The global maximum stock point across all warehouses. |
| `taxId` | `int?` |  | GreaterThan(0).When(x => x.TaxId.HasValue) | The id of the default sales tax for this item. |
| `withholdingTaxId` | `int?` |  | GreaterThan(0).When(x => x.WithholdingTaxId.HasValue) | The id of the withholding tax for this item, when applicable. |
| `eInvoiceCode` | `string` |  | MaximumLength(50) | The e-invoice item code (used by Egypt/KSA e-invoicing). |
| `eInvoiceCodeType` | `string` |  | MaximumLength(50) | The e-invoice code type (e.g. GS1, EGS). |
| `enforceSerialEntry` | `bool?` |  |  | Whether issuing this item must capture serial numbers. |
| `enforceBatchNumberDateEntry` | `bool?` |  |  | Whether issuing this item must capture a batch number and production date. |
| `enforceExpirationDateEntry` | `bool?` |  |  | Whether issuing this item must capture an expiration date. |
| `autofillBatchNumber` | `bool?` |  |  | Whether batch numbers should be auto-generated when not supplied. |
| `allowSerialDuplication` | `bool?` |  |  | Whether the same serial number may appear on more than one document. |
| `salesPriceDiscount` | `decimal?` |  |  | The discount value applied to the sales price (interpreted per `SalesPriceDiscountType`). |
| `salesPriceDiscountType` | `int?` |  |  | The discount type for the sales price (0 = amount, 1 = percentage). |
| `salesPriceDiscountIsLimited` | `bool?` |  |  | Whether the sales-price discount is bounded by the date range below. |
| `salesPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the sales-price discount window. |
| `salesPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the sales-price discount window. |
| `dealerPriceDiscount` | `decimal?` |  |  | The discount value applied to the dealer price. |
| `dealerPriceDiscountType` | `int?` |  |  | The discount type for the dealer price (0 = amount, 1 = percentage). |
| `dealerPriceDiscountIsLimited` | `bool?` |  |  | Whether the dealer-price discount is bounded by the date range below. |
| `dealerPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the dealer-price discount window. |
| `dealerPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the dealer-price discount window. |
| `superDealerPriceDiscount` | `decimal?` |  |  | The discount value applied to the super-dealer price. |
| `superDealerPriceDiscountType` | `int?` |  |  | The discount type for the super-dealer price (0 = amount, 1 = percentage). |
| `superDealerPriceDiscountIsLimited` | `bool?` |  |  | Whether the super-dealer-price discount is bounded by the date range below. |
| `superDealerPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the super-dealer-price discount window. |
| `superDealerPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the super-dealer-price discount window. |
| `dynamicPropertiesInfo` | `List<DynamicPropertyInfo>` |  |  | The dynamic-property values to assign to the item. |
| `childItems` | `List<StockItemChildRequest>` |  |  | The child variants of this item when it is a grouping item. |

**Responses**: OK `StockItemBatchResult`: Batch insert result.

**Business errors** (HTTP 409, match on `errorCode`):

- `CannotAssignInactiveTax`: An inactive tax cannot be assigned.
- `CodeContainsInvalidSpecialCharacters`: The stock-item code contains invalid special characters.
- `DescriptionContainsInvalidSpecialCharacters`: The stock-item description contains invalid special characters.
- `DupplicatedSKU`: Another stock item already uses the same SKU.
- `InvalidTaxScopeForItem`: The tax scope is not valid for this stock item.
- `InvalidTaxTypeForItem`: The tax type is not valid for this stock item.
- `MustSelectValidTaxType`: A valid tax type must be selected.
- `OtherLangDescriptionContainsInvalidSpecialCharacters`: The other-language description contains invalid special characters.
- `StockItemBrandRequired`: A brand must be assigned to the stock item.
- `StockItemCodeExists`: Another stock item already uses the same code.
- `StockItemCodeRequired`: The stock-item code is required.
- `StockItemDescriptionExists`: Another stock item already uses the same description.
- `StockItemPartNumberExists`: Another stock item already uses the same part number.
- `StockItemPartNumberRequired`: The stock-item part number is required.
- `StockItemSalesPriceMustBeGreaterThanZero`: The sales price must be greater than zero.
- `StockItemSKURequired`: The stock-item SKU is required.
- `StockitemsClassificationCannotBeNull`: A classification must be assigned to the stock item.
- `TaxNotFound`: The specified tax was not found.

**Notes**

- The response is always `200`, with a `succeeded` list and a `failed` map of key to reason. Each item is saved on its own, so items that succeeded stay saved when a later item fails.
- Each `failed` reason is plain text, not a structured error. A business rule failure reads "Bad Request." followed by the `errorCode` and the message. Unexpected failures start with "Internal Server Error."
- The whole request is rejected with `400` only when the list is empty or has more than 1000 items.
- `failed` is keyed by `description`, or by an empty string when it is missing. Two failed items with the same description overwrite each other.
- After an item fails, later items in the batch with the same `description` are skipped and appear in neither list. Make descriptions unique within a batch.
- Entries in `succeeded` are not read back after saving, so values that the server fills in can be missing. Read the item again if you need them.

---

### `PUT /v3/stock-items/{id}`: Updates a stock item by id.

Operation `UpdateStockItem` · permission `update:stock-item`

> - When transforming a simple item into a grouping item (`existing.IsGroupingItem = false`, `request.IsGroupingItem = true`), exactly one entry in `ChildItems` must set **IsCurrent = true** to mark the carry-over of the original simple item. On Updates of already-grouping items, `IsCurrent` is ignored.

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The stock item id. |

**Body (sparse PUT: omitted fields keep their value)**: `UpdateStockItemRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `imageUrlProvided` | `bool` |  |  | True when the request body explicitly included `ImageUrl`. Set by `FromBodySparseAttribute`; never bound from client JSON. Internal-connector callers that omit ImageUrl keep the stock item's existing image instead of having it nulled out (sparse-PUT semantics). |
| `id` | `int?` |  |  | Stock-item identifier. Optional on single-update; required on batch by-id update. |
| `code` | `string` |  | MaximumLength(200) .Must(c => !string.IsNullOrWhiteSpace(c)).When(x => x.Code != null) | The stock-item code (unique per tenant). |
| `description` | `string` |  | NotEmpty().MaximumLength(500) | The stock-item description. |
| `otherLanguageDescription` | `string` |  | MaximumLength(500) .Must(d => !string.IsNullOrWhiteSpace(d)).When(x => x.OtherLanguageDescription != null) | The description in the secondary language (e.g. Arabic). |
| `sku` | `string` |  | MaximumLength(50) .Must(s => !string.IsNullOrWhiteSpace(s)).When(x => x.Sku != null) | The stock-keeping unit identifier (unique per tenant when supplied). |
| `partNumber` | `string` |  | MaximumLength(500) .Must(p => !string.IsNullOrWhiteSpace(p)).When(x => x.PartNumber != null) | The manufacturer or vendor part number. |
| `oENumber` | `string` |  | MaximumLength(200) | The OEM (original equipment manufacturer) number. |
| `externalId` | `string` |  | MaximumLength(128) .Must(e => !string.IsNullOrWhiteSpace(e)).When(x => x.ExternalId != null) | An external identifier supplied by the caller for reconciliation. |
| `classificationId` | `int?` |  | GreaterThan(0).When(x => x.ClassificationId.HasValue) | The id of the classification node this item belongs to. |
| `brandId` | `int?` |  | GreaterThan(0).When(x => x.BrandId.HasValue) | The id of the brand. |
| `parentItemId` | `int?` |  | GreaterThan(0).When(x => x.ParentItemId.HasValue) | The id of the parent item, when this item is a variant. |
| `isGroupingItem` | `bool?` |  |  | Whether this item is a grouping (parent) item with variants underneath. |
| `tags` | `string[]` |  | Must(tags => tags == null \|\| tags.All(tag => !string.IsNullOrWhiteSpace(tag)))  .Must(tags => tags == null \|\| string.Join(",", tags).Length <= 4000) | The tags to assign to the item (replaces any existing tags). |
| `singleUomId` | `int?` |  | GreaterThan(0).When(x => x.SingleUomId.HasValue) | The id of the single UOM when the item is not part of a UOM chain. |
| `unitOfMeasureChainId` | `int?` |  | GreaterThan(0).When(x => x.UnitOfMeasureChainId.HasValue) | The id of the UOM chain the item uses. |
| `unitOfMeasure` | `int?` |  | GreaterThan(0).When(x => x.UnitOfMeasure.HasValue) | The id of the resolved unit of measure for this item. |
| `stockItemUnitOfMeasures` | `List<StockItemUnitOfMeasureRequest>` |  | each: SetValidator(new StockItemUnitOfMeasureRequestValidator()) | Per-UOM pricing and identifiers when the item uses a UOM chain. |
| `weight` | `decimal?` |  |  | The item weight. |
| `height` | `decimal?` |  |  | The item height. |
| `width` | `decimal?` |  |  | The item width. |
| `length` | `decimal?` |  |  | The item length. |
| `warranty` | `decimal?` |  |  | The warranty period (interpretation per tenant settings). |
| `validityPeriodInDays` | `decimal?` |  |  | The validity period in days after which the item is treated as expired. |
| `itemLocation` | `string` |  | MaximumLength(200) | The default storage location of the item within a warehouse. |
| `imageUrl` | `string` |  | MaximumLength(200) | The URL to the item image. |
| `note` | `string` |  | MaximumLength(500) | Free-form notes captured against the item. |
| `datasheet` | `string` |  |  | The URL or text of the item datasheet. |
| `attachedDocument` | `string` |  |  | The URL of an attached document. |
| `salesPrice` | `decimal?` |  |  | The default sales price. |
| `purchasePrice` | `decimal?` |  |  | The default purchase price. |
| `dealerPrice` | `decimal?` |  |  | The dealer price. |
| `superDealerPrice` | `decimal?` |  |  | The super-dealer price. |
| `minimumPrice` | `decimal?` |  |  | The minimum allowed sales price. |
| `referencePrice` | `decimal?` |  |  | The reference price used in margin calculations. |
| `unitPrice` | `decimal?` |  |  | The unit price; used by some legacy callers. |
| `openingAverageCost` | `decimal?` |  |  | The opening average cost. |
| `openingFifo` | `decimal?` |  |  | The opening FIFO cost. |
| `openingLifo` | `decimal?` |  |  | The opening LIFO cost. |
| `openingLastCost` | `decimal?` |  |  | The opening last cost. |
| `averageCost` | `decimal?` |  |  | The current average cost. |
| `fifo` | `decimal?` |  |  | The current FIFO valuation. |
| `lifo` | `decimal?` |  |  | The current LIFO valuation. |
| `lastCost` | `decimal?` |  |  | The current last cost. |
| `globalReorderingPoint` | `decimal?` |  |  | The global reordering point across all warehouses. |
| `globalMinimumPoint` | `decimal?` |  |  | The global minimum stock point across all warehouses. |
| `globalMaximumPoint` | `decimal?` |  |  | The global maximum stock point across all warehouses. |
| `taxId` | `int?` |  | GreaterThan(0).When(x => x.TaxId.HasValue) | The id of the default sales tax for this item. |
| `withholdingTaxId` | `int?` |  | GreaterThan(0).When(x => x.WithholdingTaxId.HasValue) | The id of the withholding tax for this item, when applicable. |
| `eInvoiceCode` | `string` |  | MaximumLength(50) | The e-invoice item code (used by Egypt/KSA e-invoicing). |
| `eInvoiceCodeType` | `string` |  | MaximumLength(50) | The e-invoice code type (e.g. GS1, EGS). |
| `enforceSerialEntry` | `bool?` |  |  | Whether issuing this item must capture serial numbers. |
| `enforceBatchNumberDateEntry` | `bool?` |  |  | Whether issuing this item must capture a batch number and production date. |
| `enforceExpirationDateEntry` | `bool?` |  |  | Whether issuing this item must capture an expiration date. |
| `autofillBatchNumber` | `bool?` |  |  | Whether batch numbers should be auto-generated when not supplied. |
| `allowSerialDuplication` | `bool?` |  |  | Whether the same serial number may appear on more than one document. |
| `allowForSMS` | `bool?` |  |  | Whether the item is permitted on outbound SMS notifications. Update-only. |
| `salesPriceDiscount` | `decimal?` |  |  | The discount value applied to the sales price (interpreted per `SalesPriceDiscountType`). |
| `salesPriceDiscountType` | `int?` |  |  | The discount type for the sales price (0 = amount, 1 = percentage). |
| `salesPriceDiscountIsLimited` | `bool?` |  |  | Whether the sales-price discount is bounded by the date range below. |
| `salesPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the sales-price discount window. |
| `salesPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the sales-price discount window. |
| `dealerPriceDiscount` | `decimal?` |  |  | The discount value applied to the dealer price. |
| `dealerPriceDiscountType` | `int?` |  |  | The discount type for the dealer price (0 = amount, 1 = percentage). |
| `dealerPriceDiscountIsLimited` | `bool?` |  |  | Whether the dealer-price discount is bounded by the date range below. |
| `dealerPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the dealer-price discount window. |
| `dealerPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the dealer-price discount window. |
| `superDealerPriceDiscount` | `decimal?` |  |  | The discount value applied to the super-dealer price. |
| `superDealerPriceDiscountType` | `int?` |  |  | The discount type for the super-dealer price (0 = amount, 1 = percentage). |
| `superDealerPriceDiscountIsLimited` | `bool?` |  |  | Whether the super-dealer-price discount is bounded by the date range below. |
| `superDealerPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the super-dealer-price discount window. |
| `superDealerPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the super-dealer-price discount window. |
| `dynamicPropertiesInfo` | `List<DynamicPropertyInfo>` |  |  | The dynamic-property values to assign to the item. |
| `childItems` | `List<StockItemChildRequest>` |  |  | The child variants of this item when it is a grouping item. |

**Responses**: OK `StockItemResponse`: The updated stock item.

**Business errors** (HTTP 409, match on `errorCode`):

- `BatchNumberCannotChangeInUse`: The batch-number setting cannot be changed because the item is already used in transactions.
- `CannotAssignInactiveTax`: An inactive tax cannot be assigned.
- `ChangingStockItemUOMnotAllowedAlreadyIntransactions`: The unit of measure cannot change because the item is already used in transactions.
- `CodeContainsInvalidSpecialCharacters`: The stock-item code contains invalid special characters.
- `DescriptionContainsInvalidSpecialCharacters`: The stock-item description contains invalid special characters.
- `DupplicatedSKU`: Another stock item already uses the same SKU.
- `EnforceExpirationDateEntryCannotChangeInUse`: The expiration-date-entry flag cannot be changed because the item is already used in transactions.
- `InvalidTaxScopeForItem`: The tax scope is not valid for this stock item.
- `InvalidTaxTypeForItem`: The tax type is not valid for this stock item.
- `MustSelectValidTaxType`: A valid tax type must be selected.
- `OtherLangDescriptionContainsInvalidSpecialCharacters`: The other-language description contains invalid special characters.
- `PhantomRead`: The stock item was modified by another user; reload and retry.
- `StockItemBrandRequired`: A brand must be assigned to the stock item.
- `StockItemCodeExists`: Another stock item already uses the same code.
- `StockItemCodeRequired`: The stock-item code is required.
- `StockItemDescriptionExists`: Another stock item already uses the same description.
- `StockItemPartNumberExists`: Another stock item already uses the same part number.
- `StockItemPartNumberRequired`: The stock-item part number is required.
- `StockItemSalesPriceMustBeGreaterThanZero`: The sales price must be greater than zero.
- `StockItemSKURequired`: The stock-item SKU is required.
- `TaxNotFound`: The specified tax was not found.

**Notes**

- This is a full replace, not a partial update. Every field you omit is overwritten: text, ids, `tags`, `externalId` and `imageUrl` are cleared, true or false flags become false, and prices become 0.
- Always send the whole item: read it with GET, change what you need, then PUT it back.
- Only these are kept when you omit them: the eight cost fields (the four opening costs, `averageCost`, `fifo`, `lifo` and `lastCost`), `code`, and the unit setup when you omit both `singleUomId` and `unitOfMeasureChainId`.
- If you omit `imageUrl`, the item's image is removed. Send the current `imageUrl` to keep it.
- An empty, null or malformed body gives `400` with the message "The request body is required."
- The unit chain and `stockItemUnitOfMeasures` are replaced on every update. Sending `unitOfMeasureChainId` without `stockItemUnitOfMeasures` leaves the item with no unit rows.
- Changing the unit chain of an item that is already used in transactions gives `409` `ChangingStockItemUOMnotAllowedAlreadyIntransactions`. Sending both `singleUomId` and `unitOfMeasureChainId` gives `400`.
- Because omitted flags become false, leaving out `enforceBatchNumberDateEntry` or `enforceExpirationDateEntry` on an item that has them on and is in use gives `409` `BatchNumberCannotChangeInUse` or `EnforceExpirationDateEntryCannotChangeInUse`. This applies in organizations that use batch numbers.
- Because an omitted `salesPrice` becomes 0, the update fails with `400` or `409` `StockItemSalesPriceMustBeGreaterThanZero` when the organization requires a sales price.
- The required, uniqueness and tax rules of POST /v3/stock-items apply here too. The item is not compared with itself in the uniqueness checks.
- An omitted `classificationId` is not replaced by the organization default on update. The classification is cleared.
- You can get these `409` codes although they are not in the documented list: `PreventModifyOpeningCostAfterClosingJE` when an opening cost changes after a closing journal entry exists, `InvalidOpeningCostValue` for a negative opening cost, and `DynamicPropertyValueCannotDeleteHasDependentData` when you remove a dynamic property value whose child items are in use.
- `PhantomRead` is only returned when the item is deleted while your request is running.
- To turn a simple item into a grouping item, send `isGroupingItem` as true with `dynamicPropertiesInfo` and at least one entry in `childItems`, exactly one of them with `isCurrent` set to true.
- In that change, the item you update becomes the current child, not the parent. It takes that child's code, part number, SKU and price. A new parent item is created with the old description, part number and SKU, and the other children are created under it.
- The response is the item with the `id` you sent, which is now a child with `parentItemId` set. Use `parentItemId` to find the new parent.
- On an item that is already a grouping item, removing a value from `dynamicPropertiesInfo` deletes the child items that use it. Adding a value requires at least one `childItems` entry that uses it, and the new children are created.
- `dynamicPropertiesInfo` is only accepted when the organization has dynamic properties enabled for stock items. Otherwise you get `400`.
- Tags in `tags` that do not exist yet are added to the organization's tag list, and the tax rates are copied onto the item again.

---

### `PUT /v3/stock-items/code/{code}`: Updates a stock item by code.

Operation `UpdateStockItemByCode` · permission `update:stock-item`

> - When transforming a simple item into a grouping item (`existing.IsGroupingItem = false`, `request.IsGroupingItem = true`), exactly one entry in `ChildItems` must set **IsCurrent = true** to mark the carry-over of the original simple item. On Updates of already-grouping items, `IsCurrent` is ignored.

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `code` | route | `string` |  | The stock item code. |

**Body (sparse PUT: omitted fields keep their value)**: `UpdateStockItemRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `imageUrlProvided` | `bool` |  |  | True when the request body explicitly included `ImageUrl`. Set by `FromBodySparseAttribute`; never bound from client JSON. Internal-connector callers that omit ImageUrl keep the stock item's existing image instead of having it nulled out (sparse-PUT semantics). |
| `id` | `int?` |  |  | Stock-item identifier. Optional on single-update; required on batch by-id update. |
| `code` | `string` |  | MaximumLength(200) .Must(c => !string.IsNullOrWhiteSpace(c)).When(x => x.Code != null) | The stock-item code (unique per tenant). |
| `description` | `string` |  | NotEmpty().MaximumLength(500) | The stock-item description. |
| `otherLanguageDescription` | `string` |  | MaximumLength(500) .Must(d => !string.IsNullOrWhiteSpace(d)).When(x => x.OtherLanguageDescription != null) | The description in the secondary language (e.g. Arabic). |
| `sku` | `string` |  | MaximumLength(50) .Must(s => !string.IsNullOrWhiteSpace(s)).When(x => x.Sku != null) | The stock-keeping unit identifier (unique per tenant when supplied). |
| `partNumber` | `string` |  | MaximumLength(500) .Must(p => !string.IsNullOrWhiteSpace(p)).When(x => x.PartNumber != null) | The manufacturer or vendor part number. |
| `oENumber` | `string` |  | MaximumLength(200) | The OEM (original equipment manufacturer) number. |
| `externalId` | `string` |  | MaximumLength(128) .Must(e => !string.IsNullOrWhiteSpace(e)).When(x => x.ExternalId != null) | An external identifier supplied by the caller for reconciliation. |
| `classificationId` | `int?` |  | GreaterThan(0).When(x => x.ClassificationId.HasValue) | The id of the classification node this item belongs to. |
| `brandId` | `int?` |  | GreaterThan(0).When(x => x.BrandId.HasValue) | The id of the brand. |
| `parentItemId` | `int?` |  | GreaterThan(0).When(x => x.ParentItemId.HasValue) | The id of the parent item, when this item is a variant. |
| `isGroupingItem` | `bool?` |  |  | Whether this item is a grouping (parent) item with variants underneath. |
| `tags` | `string[]` |  | Must(tags => tags == null \|\| tags.All(tag => !string.IsNullOrWhiteSpace(tag)))  .Must(tags => tags == null \|\| string.Join(",", tags).Length <= 4000) | The tags to assign to the item (replaces any existing tags). |
| `singleUomId` | `int?` |  | GreaterThan(0).When(x => x.SingleUomId.HasValue) | The id of the single UOM when the item is not part of a UOM chain. |
| `unitOfMeasureChainId` | `int?` |  | GreaterThan(0).When(x => x.UnitOfMeasureChainId.HasValue) | The id of the UOM chain the item uses. |
| `unitOfMeasure` | `int?` |  | GreaterThan(0).When(x => x.UnitOfMeasure.HasValue) | The id of the resolved unit of measure for this item. |
| `stockItemUnitOfMeasures` | `List<StockItemUnitOfMeasureRequest>` |  | each: SetValidator(new StockItemUnitOfMeasureRequestValidator()) | Per-UOM pricing and identifiers when the item uses a UOM chain. |
| `weight` | `decimal?` |  |  | The item weight. |
| `height` | `decimal?` |  |  | The item height. |
| `width` | `decimal?` |  |  | The item width. |
| `length` | `decimal?` |  |  | The item length. |
| `warranty` | `decimal?` |  |  | The warranty period (interpretation per tenant settings). |
| `validityPeriodInDays` | `decimal?` |  |  | The validity period in days after which the item is treated as expired. |
| `itemLocation` | `string` |  | MaximumLength(200) | The default storage location of the item within a warehouse. |
| `imageUrl` | `string` |  | MaximumLength(200) | The URL to the item image. |
| `note` | `string` |  | MaximumLength(500) | Free-form notes captured against the item. |
| `datasheet` | `string` |  |  | The URL or text of the item datasheet. |
| `attachedDocument` | `string` |  |  | The URL of an attached document. |
| `salesPrice` | `decimal?` |  |  | The default sales price. |
| `purchasePrice` | `decimal?` |  |  | The default purchase price. |
| `dealerPrice` | `decimal?` |  |  | The dealer price. |
| `superDealerPrice` | `decimal?` |  |  | The super-dealer price. |
| `minimumPrice` | `decimal?` |  |  | The minimum allowed sales price. |
| `referencePrice` | `decimal?` |  |  | The reference price used in margin calculations. |
| `unitPrice` | `decimal?` |  |  | The unit price; used by some legacy callers. |
| `openingAverageCost` | `decimal?` |  |  | The opening average cost. |
| `openingFifo` | `decimal?` |  |  | The opening FIFO cost. |
| `openingLifo` | `decimal?` |  |  | The opening LIFO cost. |
| `openingLastCost` | `decimal?` |  |  | The opening last cost. |
| `averageCost` | `decimal?` |  |  | The current average cost. |
| `fifo` | `decimal?` |  |  | The current FIFO valuation. |
| `lifo` | `decimal?` |  |  | The current LIFO valuation. |
| `lastCost` | `decimal?` |  |  | The current last cost. |
| `globalReorderingPoint` | `decimal?` |  |  | The global reordering point across all warehouses. |
| `globalMinimumPoint` | `decimal?` |  |  | The global minimum stock point across all warehouses. |
| `globalMaximumPoint` | `decimal?` |  |  | The global maximum stock point across all warehouses. |
| `taxId` | `int?` |  | GreaterThan(0).When(x => x.TaxId.HasValue) | The id of the default sales tax for this item. |
| `withholdingTaxId` | `int?` |  | GreaterThan(0).When(x => x.WithholdingTaxId.HasValue) | The id of the withholding tax for this item, when applicable. |
| `eInvoiceCode` | `string` |  | MaximumLength(50) | The e-invoice item code (used by Egypt/KSA e-invoicing). |
| `eInvoiceCodeType` | `string` |  | MaximumLength(50) | The e-invoice code type (e.g. GS1, EGS). |
| `enforceSerialEntry` | `bool?` |  |  | Whether issuing this item must capture serial numbers. |
| `enforceBatchNumberDateEntry` | `bool?` |  |  | Whether issuing this item must capture a batch number and production date. |
| `enforceExpirationDateEntry` | `bool?` |  |  | Whether issuing this item must capture an expiration date. |
| `autofillBatchNumber` | `bool?` |  |  | Whether batch numbers should be auto-generated when not supplied. |
| `allowSerialDuplication` | `bool?` |  |  | Whether the same serial number may appear on more than one document. |
| `allowForSMS` | `bool?` |  |  | Whether the item is permitted on outbound SMS notifications. Update-only. |
| `salesPriceDiscount` | `decimal?` |  |  | The discount value applied to the sales price (interpreted per `SalesPriceDiscountType`). |
| `salesPriceDiscountType` | `int?` |  |  | The discount type for the sales price (0 = amount, 1 = percentage). |
| `salesPriceDiscountIsLimited` | `bool?` |  |  | Whether the sales-price discount is bounded by the date range below. |
| `salesPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the sales-price discount window. |
| `salesPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the sales-price discount window. |
| `dealerPriceDiscount` | `decimal?` |  |  | The discount value applied to the dealer price. |
| `dealerPriceDiscountType` | `int?` |  |  | The discount type for the dealer price (0 = amount, 1 = percentage). |
| `dealerPriceDiscountIsLimited` | `bool?` |  |  | Whether the dealer-price discount is bounded by the date range below. |
| `dealerPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the dealer-price discount window. |
| `dealerPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the dealer-price discount window. |
| `superDealerPriceDiscount` | `decimal?` |  |  | The discount value applied to the super-dealer price. |
| `superDealerPriceDiscountType` | `int?` |  |  | The discount type for the super-dealer price (0 = amount, 1 = percentage). |
| `superDealerPriceDiscountIsLimited` | `bool?` |  |  | Whether the super-dealer-price discount is bounded by the date range below. |
| `superDealerPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the super-dealer-price discount window. |
| `superDealerPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the super-dealer-price discount window. |
| `dynamicPropertiesInfo` | `List<DynamicPropertyInfo>` |  |  | The dynamic-property values to assign to the item. |
| `childItems` | `List<StockItemChildRequest>` |  |  | The child variants of this item when it is a grouping item. |

**Responses**: OK `StockItemResponse`: The updated stock item.

**Business errors** (HTTP 409, match on `errorCode`):

- `BatchNumberCannotChangeInUse`: The batch-number setting cannot be changed because the item is already used in transactions.
- `CannotAssignInactiveTax`: An inactive tax cannot be assigned.
- `ChangingStockItemUOMnotAllowedAlreadyIntransactions`: The unit of measure cannot change because the item is already used in transactions.
- `CodeContainsInvalidSpecialCharacters`: The stock-item code contains invalid special characters.
- `DescriptionContainsInvalidSpecialCharacters`: The stock-item description contains invalid special characters.
- `DupplicatedSKU`: Another stock item already uses the same SKU.
- `EnforceExpirationDateEntryCannotChangeInUse`: The expiration-date-entry flag cannot be changed because the item is already used in transactions.
- `InvalidTaxScopeForItem`: The tax scope is not valid for this stock item.
- `InvalidTaxTypeForItem`: The tax type is not valid for this stock item.
- `MustSelectValidTaxType`: A valid tax type must be selected.
- `OtherLangDescriptionContainsInvalidSpecialCharacters`: The other-language description contains invalid special characters.
- `PhantomRead`: The stock item was modified by another user; reload and retry.
- `StockItemBrandRequired`: A brand must be assigned to the stock item.
- `StockItemCodeExists`: Another stock item already uses the same code.
- `StockItemCodeRequired`: The stock-item code is required.
- `StockItemDescriptionExists`: Another stock item already uses the same description.
- `StockItemPartNumberExists`: Another stock item already uses the same part number.
- `StockItemPartNumberRequired`: The stock-item part number is required.
- `StockItemSalesPriceMustBeGreaterThanZero`: The sales price must be greater than zero.
- `StockItemSKURequired`: The stock-item SKU is required.
- `TaxNotFound`: The specified tax was not found.

**Notes**

- Works like PUT /v3/stock-items/{id}: full replace, the same kept fields, the same grouping, unit and `imageUrl` rules, and the same errors. Only the way the item is found differs.
- The `code` in the path is trimmed and must match exactly. Partial matches are not supported, and a blank code gives `400`.
- Only items your user has data permission for are found. If the item exists but your user has no data permission for it you get `403`. If it does not exist you get `404`.
- If you omit `code` in the body, the item keeps the code from the path. A different `code` in the body renames the item.
- After a rename the response is `200` with a null body, even though the update was saved. Read the item again by its new code.

---

### `PUT /v3/stock-items/external-id`: Updates a stock item external id.

Operation `UpdateStockItemExternalId` · permission `update:stock-item`

**Body**: `UpdateStockItemExternalIdRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `id` | `int?` |  | NotNull().GreaterThan(0) | Stock-item identifier. Required. |
| `externalId` | `string` |  | MaximumLength(128) | New external identifier; null/empty to clear. |

**Responses**: OK `StockItemResponse`: The updated stock item.

**Business errors** (HTTP 409, match on `errorCode`):

- `PhantomRead`: The stock item was modified by another user; reload and retry.

**Notes**

- Changes only `externalId`. Nothing else on the item is touched, and the full replace rules of PUT /v3/stock-items/{id} do not apply.
- A blank or whitespace-only `externalId` clears the external id.
- `externalId` is not checked for uniqueness. Several items can end up with the same value, and you do not get a `409` for it.
- An `id` that does not exist gives `404`. `PhantomRead` (`409`) is only returned when the item is deleted while your request is running.

---

### `PUT /v3/stock-items/batch/by-id`: Batch updates stock items by id.

Operation `BatchUpdateStockItemsById` · permission `update:stock-item`

**Body (sparse PUT: omitted fields keep their value)**: `List<UpdateStockItemRequest>` (JSON array)

| field | type | default | validation | description |
|---|---|---|---|---|
| `imageUrlProvided` | `bool` |  |  | True when the request body explicitly included `ImageUrl`. Set by `FromBodySparseAttribute`; never bound from client JSON. Internal-connector callers that omit ImageUrl keep the stock item's existing image instead of having it nulled out (sparse-PUT semantics). |
| `id` | `int?` |  |  | Stock-item identifier. Optional on single-update; required on batch by-id update. |
| `code` | `string` |  | MaximumLength(200) .Must(c => !string.IsNullOrWhiteSpace(c)).When(x => x.Code != null) | The stock-item code (unique per tenant). |
| `description` | `string` |  | NotEmpty().MaximumLength(500) | The stock-item description. |
| `otherLanguageDescription` | `string` |  | MaximumLength(500) .Must(d => !string.IsNullOrWhiteSpace(d)).When(x => x.OtherLanguageDescription != null) | The description in the secondary language (e.g. Arabic). |
| `sku` | `string` |  | MaximumLength(50) .Must(s => !string.IsNullOrWhiteSpace(s)).When(x => x.Sku != null) | The stock-keeping unit identifier (unique per tenant when supplied). |
| `partNumber` | `string` |  | MaximumLength(500) .Must(p => !string.IsNullOrWhiteSpace(p)).When(x => x.PartNumber != null) | The manufacturer or vendor part number. |
| `oENumber` | `string` |  | MaximumLength(200) | The OEM (original equipment manufacturer) number. |
| `externalId` | `string` |  | MaximumLength(128) .Must(e => !string.IsNullOrWhiteSpace(e)).When(x => x.ExternalId != null) | An external identifier supplied by the caller for reconciliation. |
| `classificationId` | `int?` |  | GreaterThan(0).When(x => x.ClassificationId.HasValue) | The id of the classification node this item belongs to. |
| `brandId` | `int?` |  | GreaterThan(0).When(x => x.BrandId.HasValue) | The id of the brand. |
| `parentItemId` | `int?` |  | GreaterThan(0).When(x => x.ParentItemId.HasValue) | The id of the parent item, when this item is a variant. |
| `isGroupingItem` | `bool?` |  |  | Whether this item is a grouping (parent) item with variants underneath. |
| `tags` | `string[]` |  | Must(tags => tags == null \|\| tags.All(tag => !string.IsNullOrWhiteSpace(tag)))  .Must(tags => tags == null \|\| string.Join(",", tags).Length <= 4000) | The tags to assign to the item (replaces any existing tags). |
| `singleUomId` | `int?` |  | GreaterThan(0).When(x => x.SingleUomId.HasValue) | The id of the single UOM when the item is not part of a UOM chain. |
| `unitOfMeasureChainId` | `int?` |  | GreaterThan(0).When(x => x.UnitOfMeasureChainId.HasValue) | The id of the UOM chain the item uses. |
| `unitOfMeasure` | `int?` |  | GreaterThan(0).When(x => x.UnitOfMeasure.HasValue) | The id of the resolved unit of measure for this item. |
| `stockItemUnitOfMeasures` | `List<StockItemUnitOfMeasureRequest>` |  | each: SetValidator(new StockItemUnitOfMeasureRequestValidator()) | Per-UOM pricing and identifiers when the item uses a UOM chain. |
| `weight` | `decimal?` |  |  | The item weight. |
| `height` | `decimal?` |  |  | The item height. |
| `width` | `decimal?` |  |  | The item width. |
| `length` | `decimal?` |  |  | The item length. |
| `warranty` | `decimal?` |  |  | The warranty period (interpretation per tenant settings). |
| `validityPeriodInDays` | `decimal?` |  |  | The validity period in days after which the item is treated as expired. |
| `itemLocation` | `string` |  | MaximumLength(200) | The default storage location of the item within a warehouse. |
| `imageUrl` | `string` |  | MaximumLength(200) | The URL to the item image. |
| `note` | `string` |  | MaximumLength(500) | Free-form notes captured against the item. |
| `datasheet` | `string` |  |  | The URL or text of the item datasheet. |
| `attachedDocument` | `string` |  |  | The URL of an attached document. |
| `salesPrice` | `decimal?` |  |  | The default sales price. |
| `purchasePrice` | `decimal?` |  |  | The default purchase price. |
| `dealerPrice` | `decimal?` |  |  | The dealer price. |
| `superDealerPrice` | `decimal?` |  |  | The super-dealer price. |
| `minimumPrice` | `decimal?` |  |  | The minimum allowed sales price. |
| `referencePrice` | `decimal?` |  |  | The reference price used in margin calculations. |
| `unitPrice` | `decimal?` |  |  | The unit price; used by some legacy callers. |
| `openingAverageCost` | `decimal?` |  |  | The opening average cost. |
| `openingFifo` | `decimal?` |  |  | The opening FIFO cost. |
| `openingLifo` | `decimal?` |  |  | The opening LIFO cost. |
| `openingLastCost` | `decimal?` |  |  | The opening last cost. |
| `averageCost` | `decimal?` |  |  | The current average cost. |
| `fifo` | `decimal?` |  |  | The current FIFO valuation. |
| `lifo` | `decimal?` |  |  | The current LIFO valuation. |
| `lastCost` | `decimal?` |  |  | The current last cost. |
| `globalReorderingPoint` | `decimal?` |  |  | The global reordering point across all warehouses. |
| `globalMinimumPoint` | `decimal?` |  |  | The global minimum stock point across all warehouses. |
| `globalMaximumPoint` | `decimal?` |  |  | The global maximum stock point across all warehouses. |
| `taxId` | `int?` |  | GreaterThan(0).When(x => x.TaxId.HasValue) | The id of the default sales tax for this item. |
| `withholdingTaxId` | `int?` |  | GreaterThan(0).When(x => x.WithholdingTaxId.HasValue) | The id of the withholding tax for this item, when applicable. |
| `eInvoiceCode` | `string` |  | MaximumLength(50) | The e-invoice item code (used by Egypt/KSA e-invoicing). |
| `eInvoiceCodeType` | `string` |  | MaximumLength(50) | The e-invoice code type (e.g. GS1, EGS). |
| `enforceSerialEntry` | `bool?` |  |  | Whether issuing this item must capture serial numbers. |
| `enforceBatchNumberDateEntry` | `bool?` |  |  | Whether issuing this item must capture a batch number and production date. |
| `enforceExpirationDateEntry` | `bool?` |  |  | Whether issuing this item must capture an expiration date. |
| `autofillBatchNumber` | `bool?` |  |  | Whether batch numbers should be auto-generated when not supplied. |
| `allowSerialDuplication` | `bool?` |  |  | Whether the same serial number may appear on more than one document. |
| `allowForSMS` | `bool?` |  |  | Whether the item is permitted on outbound SMS notifications. Update-only. |
| `salesPriceDiscount` | `decimal?` |  |  | The discount value applied to the sales price (interpreted per `SalesPriceDiscountType`). |
| `salesPriceDiscountType` | `int?` |  |  | The discount type for the sales price (0 = amount, 1 = percentage). |
| `salesPriceDiscountIsLimited` | `bool?` |  |  | Whether the sales-price discount is bounded by the date range below. |
| `salesPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the sales-price discount window. |
| `salesPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the sales-price discount window. |
| `dealerPriceDiscount` | `decimal?` |  |  | The discount value applied to the dealer price. |
| `dealerPriceDiscountType` | `int?` |  |  | The discount type for the dealer price (0 = amount, 1 = percentage). |
| `dealerPriceDiscountIsLimited` | `bool?` |  |  | Whether the dealer-price discount is bounded by the date range below. |
| `dealerPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the dealer-price discount window. |
| `dealerPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the dealer-price discount window. |
| `superDealerPriceDiscount` | `decimal?` |  |  | The discount value applied to the super-dealer price. |
| `superDealerPriceDiscountType` | `int?` |  |  | The discount type for the super-dealer price (0 = amount, 1 = percentage). |
| `superDealerPriceDiscountIsLimited` | `bool?` |  |  | Whether the super-dealer-price discount is bounded by the date range below. |
| `superDealerPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the super-dealer-price discount window. |
| `superDealerPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the super-dealer-price discount window. |
| `dynamicPropertiesInfo` | `List<DynamicPropertyInfo>` |  |  | The dynamic-property values to assign to the item. |
| `childItems` | `List<StockItemChildRequest>` |  |  | The child variants of this item when it is a grouping item. |

**Responses**: OK `StockItemBatchResult`: Batch update result.

**Business errors** (HTTP 409, match on `errorCode`):

- `BatchNumberCannotChangeInUse`: The batch-number setting cannot be changed because the item is already used in transactions.
- `CannotAssignInactiveTax`: An inactive tax cannot be assigned.
- `ChangingStockItemUOMnotAllowedAlreadyIntransactions`: The unit of measure cannot change because the item is already used in transactions.
- `CodeContainsInvalidSpecialCharacters`: The stock-item code contains invalid special characters.
- `DescriptionContainsInvalidSpecialCharacters`: The stock-item description contains invalid special characters.
- `DupplicatedSKU`: Another stock item already uses the same SKU.
- `EnforceExpirationDateEntryCannotChangeInUse`: The expiration-date-entry flag cannot be changed because the item is already used in transactions.
- `InvalidTaxScopeForItem`: The tax scope is not valid for this stock item.
- `InvalidTaxTypeForItem`: The tax type is not valid for this stock item.
- `MustSelectValidTaxType`: A valid tax type must be selected.
- `OtherLangDescriptionContainsInvalidSpecialCharacters`: The other-language description contains invalid special characters.
- `PhantomRead`: The stock item was modified by another user; reload and retry.
- `StockItemBrandRequired`: A brand must be assigned to the stock item.
- `StockItemCodeExists`: Another stock item already uses the same code.
- `StockItemCodeRequired`: The stock-item code is required.
- `StockItemDescriptionExists`: Another stock item already uses the same description.
- `StockItemPartNumberExists`: Another stock item already uses the same part number.
- `StockItemPartNumberRequired`: The stock-item part number is required.
- `StockItemSalesPriceMustBeGreaterThanZero`: The sales price must be greater than zero.
- `StockItemSKURequired`: The stock-item SKU is required.
- `TaxNotFound`: The specified tax was not found.

**Notes**

- Each item is a full replace, as in PUT /v3/stock-items/{id}. Omitted fields are cleared, set to false or set to 0, except the cost fields and the unit setup. The grouping rules also apply.
- An omitted `imageUrl` removes the item's image.
- The response is always `200` with `succeeded` and `failed`. Each item is saved on its own, and nothing is rolled back when another item fails.
- The whole request is rejected with `400` only when the list is empty or has more than 1000 items. Failure reasons are plain text, as in POST /v3/stock-items/batch.
- `failed` is keyed by the item's `description`, or by "Item ID" followed by the id when the description is missing. Failed items with the same description overwrite each other.
- An item with a missing or unknown `id` fails with the reason "A StockItem with the specified ID was not found."
- An omitted `code` is not kept here, unlike the single update. The item's code is blanked, or the item fails with `StockItemCodeRequired` when the organization requires codes. Always send `code`.
- Entries in `succeeded` are not read back after saving. Read the item again if you need the stored values.

---

### `PUT /v3/stock-items/batch/by-code`: Batch updates stock items by code.

Operation `BatchUpdateStockItemsByCode` · permission `update:stock-item`

**Body (sparse PUT: omitted fields keep their value)**: `List<UpdateStockItemRequest>` (JSON array)

| field | type | default | validation | description |
|---|---|---|---|---|
| `imageUrlProvided` | `bool` |  |  | True when the request body explicitly included `ImageUrl`. Set by `FromBodySparseAttribute`; never bound from client JSON. Internal-connector callers that omit ImageUrl keep the stock item's existing image instead of having it nulled out (sparse-PUT semantics). |
| `id` | `int?` |  |  | Stock-item identifier. Optional on single-update; required on batch by-id update. |
| `code` | `string` |  | MaximumLength(200) .Must(c => !string.IsNullOrWhiteSpace(c)).When(x => x.Code != null) | The stock-item code (unique per tenant). |
| `description` | `string` |  | NotEmpty().MaximumLength(500) | The stock-item description. |
| `otherLanguageDescription` | `string` |  | MaximumLength(500) .Must(d => !string.IsNullOrWhiteSpace(d)).When(x => x.OtherLanguageDescription != null) | The description in the secondary language (e.g. Arabic). |
| `sku` | `string` |  | MaximumLength(50) .Must(s => !string.IsNullOrWhiteSpace(s)).When(x => x.Sku != null) | The stock-keeping unit identifier (unique per tenant when supplied). |
| `partNumber` | `string` |  | MaximumLength(500) .Must(p => !string.IsNullOrWhiteSpace(p)).When(x => x.PartNumber != null) | The manufacturer or vendor part number. |
| `oENumber` | `string` |  | MaximumLength(200) | The OEM (original equipment manufacturer) number. |
| `externalId` | `string` |  | MaximumLength(128) .Must(e => !string.IsNullOrWhiteSpace(e)).When(x => x.ExternalId != null) | An external identifier supplied by the caller for reconciliation. |
| `classificationId` | `int?` |  | GreaterThan(0).When(x => x.ClassificationId.HasValue) | The id of the classification node this item belongs to. |
| `brandId` | `int?` |  | GreaterThan(0).When(x => x.BrandId.HasValue) | The id of the brand. |
| `parentItemId` | `int?` |  | GreaterThan(0).When(x => x.ParentItemId.HasValue) | The id of the parent item, when this item is a variant. |
| `isGroupingItem` | `bool?` |  |  | Whether this item is a grouping (parent) item with variants underneath. |
| `tags` | `string[]` |  | Must(tags => tags == null \|\| tags.All(tag => !string.IsNullOrWhiteSpace(tag)))  .Must(tags => tags == null \|\| string.Join(",", tags).Length <= 4000) | The tags to assign to the item (replaces any existing tags). |
| `singleUomId` | `int?` |  | GreaterThan(0).When(x => x.SingleUomId.HasValue) | The id of the single UOM when the item is not part of a UOM chain. |
| `unitOfMeasureChainId` | `int?` |  | GreaterThan(0).When(x => x.UnitOfMeasureChainId.HasValue) | The id of the UOM chain the item uses. |
| `unitOfMeasure` | `int?` |  | GreaterThan(0).When(x => x.UnitOfMeasure.HasValue) | The id of the resolved unit of measure for this item. |
| `stockItemUnitOfMeasures` | `List<StockItemUnitOfMeasureRequest>` |  | each: SetValidator(new StockItemUnitOfMeasureRequestValidator()) | Per-UOM pricing and identifiers when the item uses a UOM chain. |
| `weight` | `decimal?` |  |  | The item weight. |
| `height` | `decimal?` |  |  | The item height. |
| `width` | `decimal?` |  |  | The item width. |
| `length` | `decimal?` |  |  | The item length. |
| `warranty` | `decimal?` |  |  | The warranty period (interpretation per tenant settings). |
| `validityPeriodInDays` | `decimal?` |  |  | The validity period in days after which the item is treated as expired. |
| `itemLocation` | `string` |  | MaximumLength(200) | The default storage location of the item within a warehouse. |
| `imageUrl` | `string` |  | MaximumLength(200) | The URL to the item image. |
| `note` | `string` |  | MaximumLength(500) | Free-form notes captured against the item. |
| `datasheet` | `string` |  |  | The URL or text of the item datasheet. |
| `attachedDocument` | `string` |  |  | The URL of an attached document. |
| `salesPrice` | `decimal?` |  |  | The default sales price. |
| `purchasePrice` | `decimal?` |  |  | The default purchase price. |
| `dealerPrice` | `decimal?` |  |  | The dealer price. |
| `superDealerPrice` | `decimal?` |  |  | The super-dealer price. |
| `minimumPrice` | `decimal?` |  |  | The minimum allowed sales price. |
| `referencePrice` | `decimal?` |  |  | The reference price used in margin calculations. |
| `unitPrice` | `decimal?` |  |  | The unit price; used by some legacy callers. |
| `openingAverageCost` | `decimal?` |  |  | The opening average cost. |
| `openingFifo` | `decimal?` |  |  | The opening FIFO cost. |
| `openingLifo` | `decimal?` |  |  | The opening LIFO cost. |
| `openingLastCost` | `decimal?` |  |  | The opening last cost. |
| `averageCost` | `decimal?` |  |  | The current average cost. |
| `fifo` | `decimal?` |  |  | The current FIFO valuation. |
| `lifo` | `decimal?` |  |  | The current LIFO valuation. |
| `lastCost` | `decimal?` |  |  | The current last cost. |
| `globalReorderingPoint` | `decimal?` |  |  | The global reordering point across all warehouses. |
| `globalMinimumPoint` | `decimal?` |  |  | The global minimum stock point across all warehouses. |
| `globalMaximumPoint` | `decimal?` |  |  | The global maximum stock point across all warehouses. |
| `taxId` | `int?` |  | GreaterThan(0).When(x => x.TaxId.HasValue) | The id of the default sales tax for this item. |
| `withholdingTaxId` | `int?` |  | GreaterThan(0).When(x => x.WithholdingTaxId.HasValue) | The id of the withholding tax for this item, when applicable. |
| `eInvoiceCode` | `string` |  | MaximumLength(50) | The e-invoice item code (used by Egypt/KSA e-invoicing). |
| `eInvoiceCodeType` | `string` |  | MaximumLength(50) | The e-invoice code type (e.g. GS1, EGS). |
| `enforceSerialEntry` | `bool?` |  |  | Whether issuing this item must capture serial numbers. |
| `enforceBatchNumberDateEntry` | `bool?` |  |  | Whether issuing this item must capture a batch number and production date. |
| `enforceExpirationDateEntry` | `bool?` |  |  | Whether issuing this item must capture an expiration date. |
| `autofillBatchNumber` | `bool?` |  |  | Whether batch numbers should be auto-generated when not supplied. |
| `allowSerialDuplication` | `bool?` |  |  | Whether the same serial number may appear on more than one document. |
| `allowForSMS` | `bool?` |  |  | Whether the item is permitted on outbound SMS notifications. Update-only. |
| `salesPriceDiscount` | `decimal?` |  |  | The discount value applied to the sales price (interpreted per `SalesPriceDiscountType`). |
| `salesPriceDiscountType` | `int?` |  |  | The discount type for the sales price (0 = amount, 1 = percentage). |
| `salesPriceDiscountIsLimited` | `bool?` |  |  | Whether the sales-price discount is bounded by the date range below. |
| `salesPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the sales-price discount window. |
| `salesPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the sales-price discount window. |
| `dealerPriceDiscount` | `decimal?` |  |  | The discount value applied to the dealer price. |
| `dealerPriceDiscountType` | `int?` |  |  | The discount type for the dealer price (0 = amount, 1 = percentage). |
| `dealerPriceDiscountIsLimited` | `bool?` |  |  | Whether the dealer-price discount is bounded by the date range below. |
| `dealerPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the dealer-price discount window. |
| `dealerPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the dealer-price discount window. |
| `superDealerPriceDiscount` | `decimal?` |  |  | The discount value applied to the super-dealer price. |
| `superDealerPriceDiscountType` | `int?` |  |  | The discount type for the super-dealer price (0 = amount, 1 = percentage). |
| `superDealerPriceDiscountIsLimited` | `bool?` |  |  | Whether the super-dealer-price discount is bounded by the date range below. |
| `superDealerPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the super-dealer-price discount window. |
| `superDealerPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the super-dealer-price discount window. |
| `dynamicPropertiesInfo` | `List<DynamicPropertyInfo>` |  |  | The dynamic-property values to assign to the item. |
| `childItems` | `List<StockItemChildRequest>` |  |  | The child variants of this item when it is a grouping item. |

**Responses**: OK `StockItemBatchResult`: Batch update result.

**Business errors** (HTTP 409, match on `errorCode`):

- `BatchNumberCannotChangeInUse`: The batch-number setting cannot be changed because the item is already used in transactions.
- `CannotAssignInactiveTax`: An inactive tax cannot be assigned.
- `ChangingStockItemUOMnotAllowedAlreadyIntransactions`: The unit of measure cannot change because the item is already used in transactions.
- `CodeContainsInvalidSpecialCharacters`: The stock-item code contains invalid special characters.
- `DescriptionContainsInvalidSpecialCharacters`: The stock-item description contains invalid special characters.
- `DupplicatedSKU`: Another stock item already uses the same SKU.
- `EnforceExpirationDateEntryCannotChangeInUse`: The expiration-date-entry flag cannot be changed because the item is already used in transactions.
- `InvalidTaxScopeForItem`: The tax scope is not valid for this stock item.
- `InvalidTaxTypeForItem`: The tax type is not valid for this stock item.
- `MustSelectValidTaxType`: A valid tax type must be selected.
- `OtherLangDescriptionContainsInvalidSpecialCharacters`: The other-language description contains invalid special characters.
- `PhantomRead`: The stock item was modified by another user; reload and retry.
- `StockItemBrandRequired`: A brand must be assigned to the stock item.
- `StockItemCodeExists`: Another stock item already uses the same code.
- `StockItemCodeRequired`: The stock-item code is required.
- `StockItemDescriptionExists`: Another stock item already uses the same description.
- `StockItemPartNumberExists`: Another stock item already uses the same part number.
- `StockItemPartNumberRequired`: The stock-item part number is required.
- `StockItemSalesPriceMustBeGreaterThanZero`: The sales price must be greater than zero.
- `StockItemSKURequired`: The stock-item SKU is required.
- `TaxNotFound`: The specified tax was not found.

**Notes**

- The response works like PUT /v3/stock-items/batch/by-id: always `200` with `succeeded` and `failed`, each item saved on its own, and a whole-request `400` only for an empty list or more than 1000 items.
- Each item is found by its trimmed `code`, with an exact match, among the items your user has data permission for. An item outside your data permission is reported as not found.
- An item with a blank or missing `code` fails with the reason "A StockItem with the specified Code was not found."
- `failed` is keyed by the item's `description`, or by an empty string when it is missing, so all failures without a description collapse into one entry.
- Grouping is not handled here. Child items and parent dynamic properties in the request are ignored, and the item is updated as a simple item.
- An `id` in the body is ignored. The item updated is always the one found by `code`.
- Each item is otherwise a full replace, as in PUT /v3/stock-items/{id}: omitted fields are cleared, set to false or set to 0, with the same rule for keeping the unit setup. Because `code` is the lookup key, it is never blanked here.

---

### `PUT /v3/stock-items/batch/by-sku`: Batch updates stock items by SKU.

Operation `BatchUpdateStockItemsBySKU` · permission `update:stock-item`

**Body (sparse PUT: omitted fields keep their value)**: `List<UpdateStockItemRequest>` (JSON array)

| field | type | default | validation | description |
|---|---|---|---|---|
| `imageUrlProvided` | `bool` |  |  | True when the request body explicitly included `ImageUrl`. Set by `FromBodySparseAttribute`; never bound from client JSON. Internal-connector callers that omit ImageUrl keep the stock item's existing image instead of having it nulled out (sparse-PUT semantics). |
| `id` | `int?` |  |  | Stock-item identifier. Optional on single-update; required on batch by-id update. |
| `code` | `string` |  | MaximumLength(200) .Must(c => !string.IsNullOrWhiteSpace(c)).When(x => x.Code != null) | The stock-item code (unique per tenant). |
| `description` | `string` |  | NotEmpty().MaximumLength(500) | The stock-item description. |
| `otherLanguageDescription` | `string` |  | MaximumLength(500) .Must(d => !string.IsNullOrWhiteSpace(d)).When(x => x.OtherLanguageDescription != null) | The description in the secondary language (e.g. Arabic). |
| `sku` | `string` |  | MaximumLength(50) .Must(s => !string.IsNullOrWhiteSpace(s)).When(x => x.Sku != null) | The stock-keeping unit identifier (unique per tenant when supplied). |
| `partNumber` | `string` |  | MaximumLength(500) .Must(p => !string.IsNullOrWhiteSpace(p)).When(x => x.PartNumber != null) | The manufacturer or vendor part number. |
| `oENumber` | `string` |  | MaximumLength(200) | The OEM (original equipment manufacturer) number. |
| `externalId` | `string` |  | MaximumLength(128) .Must(e => !string.IsNullOrWhiteSpace(e)).When(x => x.ExternalId != null) | An external identifier supplied by the caller for reconciliation. |
| `classificationId` | `int?` |  | GreaterThan(0).When(x => x.ClassificationId.HasValue) | The id of the classification node this item belongs to. |
| `brandId` | `int?` |  | GreaterThan(0).When(x => x.BrandId.HasValue) | The id of the brand. |
| `parentItemId` | `int?` |  | GreaterThan(0).When(x => x.ParentItemId.HasValue) | The id of the parent item, when this item is a variant. |
| `isGroupingItem` | `bool?` |  |  | Whether this item is a grouping (parent) item with variants underneath. |
| `tags` | `string[]` |  | Must(tags => tags == null \|\| tags.All(tag => !string.IsNullOrWhiteSpace(tag)))  .Must(tags => tags == null \|\| string.Join(",", tags).Length <= 4000) | The tags to assign to the item (replaces any existing tags). |
| `singleUomId` | `int?` |  | GreaterThan(0).When(x => x.SingleUomId.HasValue) | The id of the single UOM when the item is not part of a UOM chain. |
| `unitOfMeasureChainId` | `int?` |  | GreaterThan(0).When(x => x.UnitOfMeasureChainId.HasValue) | The id of the UOM chain the item uses. |
| `unitOfMeasure` | `int?` |  | GreaterThan(0).When(x => x.UnitOfMeasure.HasValue) | The id of the resolved unit of measure for this item. |
| `stockItemUnitOfMeasures` | `List<StockItemUnitOfMeasureRequest>` |  | each: SetValidator(new StockItemUnitOfMeasureRequestValidator()) | Per-UOM pricing and identifiers when the item uses a UOM chain. |
| `weight` | `decimal?` |  |  | The item weight. |
| `height` | `decimal?` |  |  | The item height. |
| `width` | `decimal?` |  |  | The item width. |
| `length` | `decimal?` |  |  | The item length. |
| `warranty` | `decimal?` |  |  | The warranty period (interpretation per tenant settings). |
| `validityPeriodInDays` | `decimal?` |  |  | The validity period in days after which the item is treated as expired. |
| `itemLocation` | `string` |  | MaximumLength(200) | The default storage location of the item within a warehouse. |
| `imageUrl` | `string` |  | MaximumLength(200) | The URL to the item image. |
| `note` | `string` |  | MaximumLength(500) | Free-form notes captured against the item. |
| `datasheet` | `string` |  |  | The URL or text of the item datasheet. |
| `attachedDocument` | `string` |  |  | The URL of an attached document. |
| `salesPrice` | `decimal?` |  |  | The default sales price. |
| `purchasePrice` | `decimal?` |  |  | The default purchase price. |
| `dealerPrice` | `decimal?` |  |  | The dealer price. |
| `superDealerPrice` | `decimal?` |  |  | The super-dealer price. |
| `minimumPrice` | `decimal?` |  |  | The minimum allowed sales price. |
| `referencePrice` | `decimal?` |  |  | The reference price used in margin calculations. |
| `unitPrice` | `decimal?` |  |  | The unit price; used by some legacy callers. |
| `openingAverageCost` | `decimal?` |  |  | The opening average cost. |
| `openingFifo` | `decimal?` |  |  | The opening FIFO cost. |
| `openingLifo` | `decimal?` |  |  | The opening LIFO cost. |
| `openingLastCost` | `decimal?` |  |  | The opening last cost. |
| `averageCost` | `decimal?` |  |  | The current average cost. |
| `fifo` | `decimal?` |  |  | The current FIFO valuation. |
| `lifo` | `decimal?` |  |  | The current LIFO valuation. |
| `lastCost` | `decimal?` |  |  | The current last cost. |
| `globalReorderingPoint` | `decimal?` |  |  | The global reordering point across all warehouses. |
| `globalMinimumPoint` | `decimal?` |  |  | The global minimum stock point across all warehouses. |
| `globalMaximumPoint` | `decimal?` |  |  | The global maximum stock point across all warehouses. |
| `taxId` | `int?` |  | GreaterThan(0).When(x => x.TaxId.HasValue) | The id of the default sales tax for this item. |
| `withholdingTaxId` | `int?` |  | GreaterThan(0).When(x => x.WithholdingTaxId.HasValue) | The id of the withholding tax for this item, when applicable. |
| `eInvoiceCode` | `string` |  | MaximumLength(50) | The e-invoice item code (used by Egypt/KSA e-invoicing). |
| `eInvoiceCodeType` | `string` |  | MaximumLength(50) | The e-invoice code type (e.g. GS1, EGS). |
| `enforceSerialEntry` | `bool?` |  |  | Whether issuing this item must capture serial numbers. |
| `enforceBatchNumberDateEntry` | `bool?` |  |  | Whether issuing this item must capture a batch number and production date. |
| `enforceExpirationDateEntry` | `bool?` |  |  | Whether issuing this item must capture an expiration date. |
| `autofillBatchNumber` | `bool?` |  |  | Whether batch numbers should be auto-generated when not supplied. |
| `allowSerialDuplication` | `bool?` |  |  | Whether the same serial number may appear on more than one document. |
| `allowForSMS` | `bool?` |  |  | Whether the item is permitted on outbound SMS notifications. Update-only. |
| `salesPriceDiscount` | `decimal?` |  |  | The discount value applied to the sales price (interpreted per `SalesPriceDiscountType`). |
| `salesPriceDiscountType` | `int?` |  |  | The discount type for the sales price (0 = amount, 1 = percentage). |
| `salesPriceDiscountIsLimited` | `bool?` |  |  | Whether the sales-price discount is bounded by the date range below. |
| `salesPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the sales-price discount window. |
| `salesPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the sales-price discount window. |
| `dealerPriceDiscount` | `decimal?` |  |  | The discount value applied to the dealer price. |
| `dealerPriceDiscountType` | `int?` |  |  | The discount type for the dealer price (0 = amount, 1 = percentage). |
| `dealerPriceDiscountIsLimited` | `bool?` |  |  | Whether the dealer-price discount is bounded by the date range below. |
| `dealerPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the dealer-price discount window. |
| `dealerPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the dealer-price discount window. |
| `superDealerPriceDiscount` | `decimal?` |  |  | The discount value applied to the super-dealer price. |
| `superDealerPriceDiscountType` | `int?` |  |  | The discount type for the super-dealer price (0 = amount, 1 = percentage). |
| `superDealerPriceDiscountIsLimited` | `bool?` |  |  | Whether the super-dealer-price discount is bounded by the date range below. |
| `superDealerPriceDiscountDateFrom` | `DateTime?` |  |  | The start date of the super-dealer-price discount window. |
| `superDealerPriceDiscountDateTo` | `DateTime?` |  |  | The end date of the super-dealer-price discount window. |
| `dynamicPropertiesInfo` | `List<DynamicPropertyInfo>` |  |  | The dynamic-property values to assign to the item. |
| `childItems` | `List<StockItemChildRequest>` |  |  | The child variants of this item when it is a grouping item. |

**Responses**: OK `StockItemBatchResult`: Batch update result.

**Business errors** (HTTP 409, match on `errorCode`):

- `BatchNumberCannotChangeInUse`: The batch-number setting cannot be changed because the item is already used in transactions.
- `CannotAssignInactiveTax`: An inactive tax cannot be assigned.
- `ChangingStockItemUOMnotAllowedAlreadyIntransactions`: The unit of measure cannot change because the item is already used in transactions.
- `CodeContainsInvalidSpecialCharacters`: The stock-item code contains invalid special characters.
- `DescriptionContainsInvalidSpecialCharacters`: The stock-item description contains invalid special characters.
- `DupplicatedSKU`: Another stock item already uses the same SKU.
- `EnforceExpirationDateEntryCannotChangeInUse`: The expiration-date-entry flag cannot be changed because the item is already used in transactions.
- `InvalidTaxScopeForItem`: The tax scope is not valid for this stock item.
- `InvalidTaxTypeForItem`: The tax type is not valid for this stock item.
- `MustSelectValidTaxType`: A valid tax type must be selected.
- `OtherLangDescriptionContainsInvalidSpecialCharacters`: The other-language description contains invalid special characters.
- `PhantomRead`: The stock item was modified by another user; reload and retry.
- `StockItemBrandRequired`: A brand must be assigned to the stock item.
- `StockItemCodeExists`: Another stock item already uses the same code.
- `StockItemCodeRequired`: The stock-item code is required.
- `StockItemDescriptionExists`: Another stock item already uses the same description.
- `StockItemPartNumberExists`: Another stock item already uses the same part number.
- `StockItemPartNumberRequired`: The stock-item part number is required.
- `StockItemSalesPriceMustBeGreaterThanZero`: The sales price must be greater than zero.
- `StockItemSKURequired`: The stock-item SKU is required.
- `TaxNotFound`: The specified tax was not found.

**Notes**

- Works like PUT /v3/stock-items/batch/by-code (same response, `failed` keyed by `description` or an empty string, no grouping handling), except each item is found by its trimmed `sku`.
- The `sku` match is not case sensitive. If more than one item has that SKU, only one of them is updated.
- An item with a blank `sku` fails with the reason "A StockItem with the specified SKU was not found."
- An omitted `code` blanks the item's code, or the item fails with `StockItemCodeRequired` when the organization requires codes. Always send `code`.

---

### `DELETE /v3/stock-items/{id}`: Deletes a stock item by id.

Operation `DeleteStockItemById` · permission `delete:stock-item`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The stock item id. |

**Responses**: NoContent: (not declared; read from the method body)

**Business errors** (HTTP 409, match on `errorCode`):

- `ItemCannotDeleteItInUse`: The stock item cannot be deleted because it is in use.
- `ItemCannotDeleteItInUseInPhysicalCount`: The stock item cannot be deleted because it is used in a physical count.
- `ItemCannotDeleterelatedToSupplier`: The stock item cannot be deleted because it is referenced by a supplier.
- `PhantomRead`: The stock item was modified by another user; reload and retry.

**Notes**

- The delete is permanent. The item is removed, not marked as deleted.
- An unknown `id` gives `404`.
- Deleting a grouping (parent) item also deletes its child items.
- An item that is referenced anywhere else, for example in a bundle, cannot be deleted and fails with `ItemCannotDeleteItInUse`. An item used in a physical count fails with `ItemCannotDeleteItInUseInPhysicalCount`. Nothing is deleted in either case.
- `ItemCannotDeleterelatedToSupplier` is listed for this endpoint but is not returned. An item linked to a supplier fails with `ItemCannotDeleteItInUse`.
- In rare cases the API returns `204` although the item was not deleted. If the delete matters, check that the item is gone.

---

### `PUT /v3/stock-items/{id}/deactivate`: Deactivates a stock item by id.

Operation `DeactivateStockItemById` · permission `update:stock-item`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The stock item id. |

**Responses**: NoContent: Stock item deactivated.

**Notes**

- Only this item is deactivated. There is no check for stock balance or open documents, and the child items of a grouping item stay active.
- An unknown `id` gives `404`.
- Deactivating an item that is already inactive returns `204`.
- A `204` does not guarantee the change. Errors while deactivating are not reported, so read the item again if you need to be sure.
- There is no v3 endpoint to reactivate a stock item, and the update endpoints cannot change the active state. Reactivate the item in the Edara application.

---

### `PUT /v3/stock-items/code/{code}/deactivate`: Deactivates a stock item by code.

Operation `DeactivateStockItemByCode` · permission `update:stock-item`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `code` | route | `string` |  | The stock item code. |

**Responses**: NoContent: Stock item deactivated.

**Notes**

- Works like PUT /v3/stock-items/{id}/deactivate: only the item is deactivated, a `204` does not guarantee the change, and there is no reactivation endpoint.
- The item is found by the trimmed `code` in the path. A blank code gives `400`, an item your user has no data permission for gives `403`, and a code that does not exist gives `404`.

---

### `POST /v3/stock-items/link-to-parent`: Links a stock item to a parent.

Operation `LinkStockItemToParent` · permission `update:stock-item`

**Body**: `LinkStockItemToParentRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `childItemId` | `int?` |  | NotNull() .GreaterThan(0) | Child stock-item identifier. |
| `parentItemId` | `int?` |  | NotNull() .GreaterThan(0) | Parent (grouping) stock-item identifier. |
| `dynamicPropertiesValuesIds` | `string` |  | NotEmpty(); Must(value => { var tokens = SplitTokens(value); return tokens.All(t => int.TryParse(t, out _)); }) .When(x => !string.IsNullOrWhiteSpace(x.DynamicPropertiesValuesIds)); Must(value => { var tokens = SplitTokens(value); return tokens.Length == tokens.Distinct().Count(); }) .When(x => !string.IsNullOrWhiteSpace(x.DynamicPropertiesValuesIds) && SplitTokens(x.DynamicPropertiesValuesIds).All(t => int.TryParse(t, out _))); Must(value => SplitTokens(value).Length <= 3) .When(x => !string.IsNullOrWhiteSpace(x.DynamicPropertiesValuesIds)) | Comma-separated dynamic-property value identifiers (1-3 distinct positive integers) identifying the variant. |

**Responses**: NoContent: Stock item linked.

**Business errors** (HTTP 409, match on `errorCode`):

- `ChildItemEqualsParentItem`: A stock item cannot be linked to itself.
- `ChildItemNotEligibleForLink`: The child stock item is not eligible to be linked (missing, inactive, already a grouping item, already linked to a parent, or already has dynamic-property values).
- `ParentItemMustBeGroupingItem`: The parent stock item must exist and be marked as a grouping item.
- `ParentDynamicPropertiesInconsistent`: The parent item has dynamic-property definitions but no existing children; this state is inconsistent.
- `DynamicPropertyValuesInvalid`: One or more dynamic-property value identifiers are invalid or do not belong to a property group.
- `MultipleValuesForSameDynamicPropertyGroup`: More than one value was supplied for the same dynamic-property group.
- `DuplicateDynamicPropertyCombination`: Another child of the same parent already uses this exact dynamic-property value combination.
- `DynamicPropertyGroupsDoNotMatchExistingChildren`: The dynamic-property groups supplied do not match the groups used by existing children of this parent.

**Notes**

- `dynamicPropertiesValuesIds` is a comma-separated string of numeric value ids, with no duplicates and at most 3 values.
- If another link request for the same `childItemId` is still running, you get `400` with the message "This link request is already being processed by another user."
- The child must exist, be active, not be a grouping item, and not already have a parent or dynamic property values. It cannot be its own parent.
- The parent must exist and be a grouping item. It does not have to be active.
- Every value id must be valid, with one value per property. The combination must not already be used by another child of the parent, and it must cover the same properties as the existing children.
- A parent that has dynamic properties but no children is rejected as inconsistent.
- Linking sets the child's parent and dynamic property values, and adds any values the parent does not have yet to the parent. The child's description, code and prices do not change.

---

### `GET /v3/stock-items/{stockItemId}/validate/serial/{serialNo}/returns/{customerId}`: Validates an item serial for return.

Operation `ValidateItemSerialForReturn` · permission `read:stock-item`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `stockItemId` | route | `int` |  | The stock item id. |
| `serialNo` | route | `string` |  | The serial number to validate. |
| `customerId` | route | `int?` |  | The customer id. |

**Responses**: OK `bool`: True when the serial is valid for return.

**Notes**

- The result is true only when the most recent movement of that serial for the item is an issue (`IO`). The serial number must match exactly.
- With `customerId`, only that customer's movements are looked at before the most recent one is picked. A serial that was later issued to or returned by another customer can still come back true.
- A `customerId` of 0 or less means any customer.
- An invalid serial gives `200` with false, never `404`. That covers an unknown stock item, an unknown serial, a serial that was never issued, and a serial whose latest movement is not an issue. The response body is a bare JSON boolean.
- Only current documents are checked, not archived ones, and movements in all warehouses count.
- When two movements have the same date, which one counts as the most recent is not defined.

---

### `GET /v3/stock-items/{stockItemId}/validate/serial/{serialNo}/returned/{rrCode}`: Validates an item serial returned.

Operation `ValidateItemSerialReturned` · permission `read:stock-item`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `stockItemId` | route | `int` |  | The stock item id. |
| `serialNo` | route | `string` |  | The serial number to validate. |
| `rrCode` | route | `string` |  | The return-receipt code. |

**Responses**: OK `bool`: True when the serial has been returned.

**Notes**

- The result is true only when the most recent matching movement of that serial for the item is a return receipt (`RR`). The serial number must match exactly.
- Without `rrCode`, the check answers whether the serial is currently in a returned state. Any later movement, for example a new issue (`IO`), makes it false.
- With `rrCode`, only movements on the document with exactly that code are considered. The check then answers whether the serial was on that return document, whatever happened afterwards.
- A blank `rrCode` means no document filter. `rrCode` and `serialNo` are trimmed.
- No match gives `200` with false, never `404`. That covers an unknown stock item, an unknown serial and a wrong `rrCode`. The response body is a bare JSON boolean.
- Only current documents are checked, not archived ones. There is no customer or warehouse filter.
- When two movements have the same date, which one counts as the most recent is not defined.

---

### `GET /v3/stock-items/balances`: Lists stock item balances.

Operation `GetStockItemBalances` · permission `read:stock-item:balance`

**Query string**: `GetStockItemBalancesQuery`

| field | type | default | validation | description |
|---|---|---|---|---|
| `offset` | `int?` | 0 | >= 0 | Number of items to skip before returning results. Defaults to 0. |
| `limit` | `int?` | 100 | 1..1000 (out of range → 400) | Maximum number of items to return. Defaults to 100, maximum 1000. |
| `stockItemId` | `int?` |  | GreaterThan(0) .When(x => x.StockItemId.HasValue) | Optional stock-item identifier filter. |
| `warehouseId` | `int?` |  | GreaterThan(0) .When(x => x.WarehouseId.HasValue) | Optional warehouse identifier filter. |

**Responses**: OK `PagedResult<StockItemBalanceResponse>`: Paged list of stock item balances.

**Notes**

- Each row is one stock item in one warehouse for one batch. `stockItemId` and `warehouseId` are optional exact-match filters.
- With neither filter, you page through every stock row of the organization, including rows with a zero balance.
- Rows are ordered by stock item and then warehouse. When an item has several batches in one warehouse, their rows can move between pages. Use a large `limit` or filter by `stockItemId`.
- An empty result gives `404` with the message "No Data Found.", not an empty page. This happens for unknown ids, for no stock, and for an `offset` past the end, so treat `404` as the end of the list when you page.
- The response has the current balance only. There is no available quantity field.
- A missing reserved quantity comes back as 0 and a missing batch number as null. The balance by stock item endpoint returns an empty string for a missing batch, so handle both.

---

### `GET /v3/stock-items/warehouse-summary`: Gets stock item sales and stock position by warehouse.

Operation `GetStockItemWarehouseSummary` · permission `read:stock-item:balance`

**Query string**: `GetStockItemWarehouseSummaryQuery`

| field | type | default | validation | description |
|---|---|---|---|---|
| `dateFrom` | `DateTime` |  | NotEmpty() | Start of the sales/sales-return summary period. |
| `dateTo` | `DateTime` |  | NotEmpty(); GreaterThanOrEqualTo(x => x.DateFrom) .When(x => x.DateFrom != default(DateTime) && x.DateTo != default(DateTime)) | End of the sales/sales-return summary period. |
| `warehouseId` | `List<int>` | new List<int>() | each: GreaterThan(0) | Optional warehouse identifiers filter. |
| `stockitemId` | `int?` |  | GreaterThan(0) .When(x => x.StockitemId.HasValue) | Optional stock-item identifier filter. |
| `excludeOtherParty` | `bool` |  |  | When true, excludes Issue/Receive Other Party transactions from the sales summary. |
| `offset` | `int?` | 0 | GreaterThanOrEqualTo(0) .When(x => x.Offset.HasValue) | Number of stock items to skip before returning results. Defaults to 0. |
| `limit` | `int?` | 1000 | InclusiveBetween(1, GetStockItemWarehouseSummaryQuery.MaxLimit) .When(x => x.Limit.HasValue) | Maximum number of stock items to return. Defaults to 1000, maximum 1000. |

**Responses**: OK `PagedResult<StockItemWarehouseSummaryResponse>`: Paged list of stock item warehouse summaries.

**Notes**

- Paging is over stock items, not over warehouse rows. Items are ordered by id, and grouping (parent) items are never included.
- `limit` defaults to 1000 and accepts up to 5000, even where the parameter description says 1000. A higher value gives `400`.
- Only stock items your user has data permission for are returned, unless the organization has turned off data permissions for stock items. Results can differ between users and organizations, so code defensively.
- `balance` and `reserved` are current totals across all batches. They are not as of `dateTo`.
- `sales` counts issue work orders (`IO`) only and `salesReturn` counts return receipts (`RR`) only, both converted for unit of measure.
- `dateFrom` and `dateTo` are both inclusive and apply to the work order date. A `dateTo` without a time means the start of that day, so send the end of the day (for example 23:59:59.997) to include it.
- With `excludeOtherParty` set to true, work orders that have an other party are left out of `sales` and `salesReturn`.
- `expectedReceive` comes from open purchase orders that are not cancelled: ordered quantity minus issued quantity. It is not converted for unit of measure.
- `expectedTransfer` is the quantity of pending transfers into the warehouse, converted for unit of measure.
- The date range does not apply to `expectedReceive` or `expectedTransfer`.
- For each item, `warehouses` lists only the warehouses that hold a stock record for it. `warehouseId` narrows that list but does not remove items: an item with no matching stock record comes back with an empty `warehouses` list.
- An unknown `stockitemId` gives `200` with empty `items` and a `totalCount` of 0, not `404`.

---

### `GET /v3/stock-items/{stockItemId}/balance`: Gets a stock item balance.

Operation `GetStockItemBalanceByStockItemId` · permission `read:stock-item:balance`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `stockItemId` | route | `int` |  | The stock item id. |

**Responses**: OK `List<StockItemBalanceResponse>`: Stock item balance. · NotFound `ProblemDetails`: No balance found for the specified stock item.

**Notes**

- The response is a plain JSON array with no paging envelope. You get one row per warehouse and batch, and rows with a zero balance are included.
- The balance is always the current one. There is no date parameter.
- `balance` is the on-hand quantity, and `reservedBalance` is 0 when nothing is reserved. `batchNumber` is an empty string, never null, when the row has no batch.
- There is no available quantity field. Calculate it yourself as `balance` minus `reservedBalance`.
- `stockItem` is always filled in, even when the item is inactive.
- An unknown `stockItemId`, or an item with no stock rows, returns `404`, not an empty array.

---

### `GET /v3/stock-items/{stockItemId}/balance/warehouses/{warehouseId}`: Gets a stock item balance by warehouse.

Operation `GetStockItemBalanceByWarehouse` · permission `read:stock-item:balance`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `stockItemId` | route | `int` |  | The stock item id. |
| `warehouseId` | route | `int` |  | The warehouse id. |

**Responses**: OK `List<StockItemBalanceResponse>`: Stock item balance for the warehouse. · NotFound `ProblemDetails`: No balance found for the specified stock item and warehouse.

**Notes**

- The response is a JSON array with one row per batch in that warehouse, not a single object.
- The balance is always the current one, and there is no available quantity field.
- An unknown item, an unknown warehouse, and a pair with no stock row all return the same `404`.

---

### `GET /v3/stock-items/{stockItemId}/cost`: Gets a stock item cost.

Operation `GetStockItemCost` · permission `read:stock-item:balance`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `stockItemId` | route | `int` |  | The stock item id. |

**Query string**: `GetStockItemCostQuery`

| field | type | default | validation | description |
|---|---|---|---|---|
| `date` | `DateTime?` |  | NotNull() | Cost date. |

**Responses**: OK `decimal`: Stock item cost. · NotFound `ProblemDetails`: No stock item with the specified identifier was found.

**Notes**

- The cost is always the average cost. The organization's cost evaluation setting is not applied.
- The value is the average cost on the item's latest work order movement at or before `date`, not counting opening-balance movements. If there is none, you get the opening-balance cost.
- A `date` without a time means the start of that day, so that day's movements are left out. Send the end of the day to include them.
- The cost is global across all warehouses and is in the organization's base currency. The response is a plain decimal number, not an object.

---

### `GET /v3/stock-items/cost/by-skus`: Gets stock item costs by SKUs.

Operation `GetStockItemCostsBySkus` · permission `read:stock-item:balance`

> - The SKU list cannot exceed 200 entries.

**Query string**: `GetStockItemCostsBySkusQuery`

| field | type | default | validation | description |
|---|---|---|---|---|
| `skus` | `string` |  |  | Comma-separated stock-item SKUs (max 200). |
| `date` | `DateTime?` |  | NotNull() | Cost evaluation date (on or before). |
| _(object rule)_ | | | RuleFor(x => x) .Custom((query, context) => { if (string.IsNullOrWhiteSpace(query?.Skus)) { return; } if (query.Skus.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Length > 200) { context.AddFailure("Skus", "Skus count cannot exceed 200."); } }) | |

**Responses**: OK `IReadOnlyCollection<StockItemCostResponse>`: Stock item costs.

**Notes**

- Costs follow the same rules as the single stock item cost: always the average cost, as of the latest movement at or before `date`. A `date` without a time means the start of that day.
- An empty `skus` returns `404`, not `400`. More than 200 SKUs returns `400`.
- Unknown SKUs are left out of the response without an error, and you get `404` only when none is found. Check the returned items against the list you sent.

---

### `GET /v3/stock-items/cost/by-part-numbers`: Gets stock item costs by part numbers.

Operation `GetStockItemCostsByPartNumbers` · permission `read:stock-item:balance`

> - The part-number list cannot exceed 200 entries.

**Query string**: `GetStockItemCostsByPartNumbersQuery`

| field | type | default | validation | description |
|---|---|---|---|---|
| `partNumbers` | `string` |  |  | Comma-separated stock-item part numbers (max 200). |
| `date` | `DateTime?` |  | NotNull() | Cost evaluation date (on or before). |
| _(object rule)_ | | | RuleFor(x => x) .Custom((query, context) => { if (string.IsNullOrWhiteSpace(query?.PartNumbers)) { return; } if (query.PartNumbers.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Length > 200) { context.AddFailure("PartNumbers", "Part numbers count cannot exceed 200."); } }) | |

**Responses**: OK `IReadOnlyCollection<StockItemCostResponse>`: Stock item costs.

**Notes**

- Costs follow the same rules as the single stock item cost: always the average cost, as of the latest movement at or before `date`.
- An empty `partNumbers` returns `404`. Unknown part numbers are left out of the response without an error.

---

### `GET /v3/stock-items/cost/by-ids`: Gets stock item costs by ids.

Operation `GetStockItemCostsByIds` · permission `read:stock-item:balance`

> - The identifier list cannot exceed 100 entries.

**Query string**: `GetStockItemCostsByIdsQuery`

| field | type | default | validation | description |
|---|---|---|---|---|
| `ids` | `string` |  |  | Comma-separated stock-item identifiers (max 100). |
| `date` | `DateTime?` |  | NotNull() | Cost evaluation date (on or before). |
| _(object rule)_ | | | RuleFor(x => x) .Custom((query, context) => { if (string.IsNullOrWhiteSpace(query?.Ids)) { return; } if (query.Ids.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Length > 100) { context.AddFailure("Ids", "Ids count cannot exceed 100."); } }) | |

**Responses**: OK `IReadOnlyCollection<StockItemCostResponse>`: Stock item costs.

**Notes**

- Costs follow the same rules as the single stock item cost: always the average cost, as of the latest movement at or before `date`.
- An empty `ids` returns `404`. Unknown ids are left out of the response without an error.

---

### `GET /v3/stock-items/{stockItemId}/global-balance`: Gets a stock item global balance by id.

Operation `GetStockItemGlobalBalanceById` · permission `read:stock-item:balance`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `stockItemId` | route | `int` |  | The stock item id. |

**Responses**: OK `StockItemGlobalBalanceResponse`: Stock item global balance. · NotFound `ProblemDetails`: No stock item with the specified identifier was found.

**Notes**

- The response is a single object with `stockItem`, `globalBalance` and `globalReservedBalance`. It has no per-warehouse or per-batch rows and no available quantity field.
- `globalBalance` is the on-hand total across all warehouses plus the quantities of internal transfers that are `Pending` or `Rejected`. While such a transfer is open, it does not equal the sum of the per-warehouse `balance` values.
- `globalReservedBalance` is the reserved total across all warehouses, with no transfer adjustment. It is 0 when nothing is reserved.
- Values are always current. There is no date parameter.
- An unknown id returns `404`. A known item with no stock returns `200` with both values 0.

---

### `GET /v3/stock-items/global-balances`: Gets a stock item global balance.

Operation `GetStockItemGlobalBalance` · permission `read:stock-item:balance`

> - Supply either `code` or `sku`, but not both.

**Query string**: `GetStockItemGlobalBalanceQuery`

| field | type | default | validation | description |
|---|---|---|---|---|
| `code` | `string` |  |  | Stock-item code. Mutually exclusive with `Sku`. |
| `sku` | `string` |  |  | Stock-item SKU. Mutually exclusive with `Code`. |

**Responses**: OK `decimal`: Stock item global balance. · NotFound `ProblemDetails`: No stock item was found for the supplied code or SKU.

**Notes**

- The response is a plain decimal number, for example 42.0000, not an object. It is the global on-hand figure only, with no reserved figure and no stock item reference.
- Send exactly one of `code` or `sku`. Sending both, or neither, returns `400`.
- `code` is trimmed and matched exactly. An item outside your stock item data permissions returns `404`, the same as an unknown code.
- `sku` is trimmed and matched exactly, ignoring case. An unknown SKU returns `404`.
- If more than one item has the same `sku`, one of them is picked without an error.
- The value is the current on-hand total across all warehouses plus the quantities of internal transfers that are `Pending` or `Rejected`, the same as `globalBalance`.

## Types

### `BrandReference`

Lightweight brand reference (id + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The brand id. |
| `name` | `string` | The brand name. |

### `ClassificationReference`

Lightweight stock-item classification reference (id + code + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The classification id. |
| `code` | `string` | The classification code. |
| `name` | `string` | The classification name. |

### `DynamicPropertyInfo`

Dynamic-property value assignment for a grouping or child stock item.

| field | type | description |
|---|---|---|
| `dynamicPropertyId` | `int` | Dynamic-property identifier (e.g. "Color"). |
| `dynamicProperty` | `string` | Dynamic-property name; corrected from DB on write. |
| `dynamicValueId` | `int` | Dynamic-property value identifier (e.g. "Red"). |
| `dynamicValue` | `string` | Dynamic-property value text; corrected from DB on write. |

### `StockItemBalanceResponse`

Response describing a stock-item balance row in a specific warehouse and (optional) batch.

| field | type | description |
|---|---|---|
| `warehouse` | `WarehouseReference` | The warehouse the balance applies to. |
| `stockItem` | `StockItemReference` | The stock item the balance applies to. |
| `balance` | `decimal` | The available on-hand quantity in the primary UOM. |
| `reservedBalance` | `decimal` | The quantity reserved against pending issues. |
| `batchNumber` | `string` | The batch number, when the stock item is batch-tracked. |
| `productionDate` | `DateTime?` | The batch production date, when applicable. |
| `expirationDate` | `DateTime?` | The batch expiration date, when applicable. |

### `StockItemBatchResult`

Response describing the result of a stock-item batch operation.

| field | type | description |
|---|---|---|
| `succeeded` | `List<StockItemResponse>` | Successfully processed stock items. |
| `failed` | `Dictionary<string,string>` | Failed items keyed by description with the failure reason. |

### `StockItemChildRequest`

Request to create a child (variant) of a grouping stock item.

| field | type | description |
|---|---|---|
| `code` | `string` | Child code. |
| `sku` | `string` | Child SKU. |
| `price` | `decimal?` | Child price. |
| `partNumber` | `string` | Child part number. |
| `dynamicPropertiesInfo` | `List<DynamicPropertyInfo>` | Dynamic-property value combination defining this variant. |
| `isCurrent` | `bool?` | Marks this child as the carry-over of the existing item when an Update transforms a simple item into a grouping item. Exactly one child must set this in the transform case; ignored otherwise. |

### `StockItemChildResponse`

Response describing a child stock item of a grouping item.

| field | type | description |
|---|---|---|
| `id` | `int` | Child identifier. |
| `description` | `string` | Description. |
| `code` | `string` | Code. |
| `sku` | `string` | SKU. |
| `price` | `decimal` | Displayed price. |
| `partNumber` | `string` | Part number. |
| `dynamicPropertiesInfo` | `List<DynamicPropertyInfo>` | Dynamic-property assignments defining this variant. |
| `salesPriceDiscount` | `decimal?` | The discount value applied to the sales price (interpreted per `SalesPriceDiscountType`). |
| `salesPriceDiscountType` | `int?` | The discount type for the sales price (0 = amount, 1 = percentage). |
| `salesPriceDiscountIsLimited` | `bool?` | Whether the sales-price discount is bounded by the date range below. |
| `salesPriceDiscountDateFrom` | `DateTime?` | The start date of the sales-price discount window. |
| `salesPriceDiscountDateTo` | `DateTime?` | The end date of the sales-price discount window. |
| `dealerPriceDiscount` | `decimal?` | The discount value applied to the dealer price. |
| `dealerPriceDiscountType` | `int?` | The discount type for the dealer price (0 = amount, 1 = percentage). |
| `dealerPriceDiscountIsLimited` | `bool?` | Whether the dealer-price discount is bounded by the date range below. |
| `dealerPriceDiscountDateFrom` | `DateTime?` | The start date of the dealer-price discount window. |
| `dealerPriceDiscountDateTo` | `DateTime?` | The end date of the dealer-price discount window. |
| `superDealerPriceDiscount` | `decimal?` | The discount value applied to the super-dealer price. |
| `superDealerPriceDiscountType` | `int?` | The discount type for the super-dealer price (0 = amount, 1 = percentage). |
| `superDealerPriceDiscountIsLimited` | `bool?` | Whether the super-dealer-price discount is bounded by the date range below. |
| `superDealerPriceDiscountDateFrom` | `DateTime?` | The start date of the super-dealer-price discount window. |
| `superDealerPriceDiscountDateTo` | `DateTime?` | The end date of the super-dealer-price discount window. |

### `StockItemCostResponse`

Response describing the current cost of a stock item.

| field | type | description |
|---|---|---|
| `stockItemId` | `int` | The stock-item id. |
| `stockItemCode` | `string` | The stock-item code. |
| `stockItemSku` | `string` | The stock-item SKU. |
| `stockItemPartNumber` | `string` | The stock-item part number. |
| `cost` | `decimal` | The unit cost in the system currency. |

### `StockItemGlobalBalanceResponse`

Response describing a stock item's global balance summary across all warehouses.

| field | type | description |
|---|---|---|
| `stockItem` | `StockItemReference` | The stock item the balance applies to. |
| `globalBalance` | `decimal` | The total on-hand quantity across all warehouses, in the primary UOM. |
| `globalReservedBalance` | `decimal` | The total reserved quantity across all warehouses, in the primary UOM. |

### `StockItemReference`

Lightweight stock-item reference (id + code + description).

| field | type | description |
|---|---|---|
| `id` | `int` | The stock-item id. |
| `code` | `string` | The stock-item code. |
| `description` | `string` | The stock-item description. |

### `StockItemResponse`

Response describing a stock item.

| field | type | description |
|---|---|---|
| `id` | `int` | Identifier. |
| `code` | `string` | Code. |
| `description` | `string` | Description. |
| `otherLanguageDescription` | `string` | Alternate-language description. |
| `sku` | `string` | SKU. |
| `partNumber` | `string` | Part number. |
| `externalId` | `string` | External identifier. |
| `classification` | `ClassificationReference` | Classification. |
| `brand` | `BrandReference` | Brand. |
| `unitOfMeasureChainId` | `int?` | Unit-of-measure chain identifier. |
| `unitOfMeasure` | `int?` | Unit-of-measure identifier. |
| `singleUomId` | `int?` | Single UOM identifier. |
| `primaryUomId` | `int?` | Primary unit-of-measure identifier resolved from the chain. |
| `isGroupingItem` | `bool` | Whether the item is a grouping item. |
| `tags` | `string[]` | Item tags. |
| `dynamicPropertiesInfo` | `List<DynamicPropertyInfo>` | Dynamic-property assignments. |
| `childItems` | `List<StockItemChildResponse>` | Child items. |
| `stockItemUnitOfMeasures` | `List<StockItemUnitOfMeasureResponse>` | UOM chain. |
| `salesPrice` | `decimal` | Sales price. |
| `purchasePrice` | `decimal` | Purchase price. |
| `dealerPrice` | `decimal` | Dealer price. |
| `superDealerPrice` | `decimal` | Super-dealer price. |
| `minimumPrice` | `decimal` | Minimum price. |
| `referencePrice` | `decimal` | Reference price. |
| `salesPriceDiscount` | `decimal?` | The discount value applied to the sales price (interpreted per `SalesPriceDiscountType`). |
| `salesPriceDiscountType` | `int?` | The discount type for the sales price (0 = amount, 1 = percentage). |
| `salesPriceDiscountIsLimited` | `bool?` | Whether the sales-price discount is bounded by the date range below. |
| `salesPriceDiscountDateFrom` | `DateTime?` | The start date of the sales-price discount window. |
| `salesPriceDiscountDateTo` | `DateTime?` | The end date of the sales-price discount window. |
| `dealerPriceDiscount` | `decimal?` | The discount value applied to the dealer price. |
| `dealerPriceDiscountType` | `int?` | The discount type for the dealer price (0 = amount, 1 = percentage). |
| `dealerPriceDiscountIsLimited` | `bool?` | Whether the dealer-price discount is bounded by the date range below. |
| `dealerPriceDiscountDateFrom` | `DateTime?` | The start date of the dealer-price discount window. |
| `dealerPriceDiscountDateTo` | `DateTime?` | The end date of the dealer-price discount window. |
| `superDealerPriceDiscount` | `decimal?` | The discount value applied to the super-dealer price. |
| `superDealerPriceDiscountType` | `int?` | The discount type for the super-dealer price (0 = amount, 1 = percentage). |
| `superDealerPriceDiscountIsLimited` | `bool?` | Whether the super-dealer-price discount is bounded by the date range below. |
| `superDealerPriceDiscountDateFrom` | `DateTime?` | The start date of the super-dealer-price discount window. |
| `superDealerPriceDiscountDateTo` | `DateTime?` | The end date of the super-dealer-price discount window. |
| `averageCost` | `double?` | Average cost. |
| `lastCost` | `double?` | Last cost. |
| `fifo` | `double?` | FIFO value. |
| `lifo` | `double?` | LIFO value. |
| `openingAverageCost` | `double?` | Opening average cost. |
| `openingFifo` | `double?` | Opening FIFO valuation. |
| `openingLifo` | `double?` | Opening LIFO valuation. |
| `openingLastCost` | `double?` | Opening last cost. |
| `tax` | `TaxReference` | Tax. |
| `taxRate` | `decimal` | Tax rate. |
| `withholdingTax` | `TaxReference` | Withholding tax. |
| `withholdingTaxRate` | `decimal` | Withholding tax rate. |
| `globalOpeningBalance` | `decimal` | Global opening balance. |
| `globalReorderingPoint` | `decimal?` | Global reordering point. |
| `globalMinimumPoint` | `decimal?` | Global minimum point. |
| `globalMaximumPoint` | `decimal?` | Global maximum point. |
| `unitPrice` | `decimal` | Unit price. |
| `length` | `decimal` | Length. |
| `width` | `decimal` | Width. |
| `height` | `decimal` | Height. |
| `weight` | `decimal` | Weight. |
| `warranty` | `decimal` | Warranty. |
| `note` | `string` | Note. |
| `itemLocation` | `string` | Item location. |
| `datasheet` | `string` | Datasheet. |
| `imageUrl` | `string` | Image URL. |
| `parentItemId` | `int?` | Parent item identifier. |
| `enforceSerialEntry` | `bool` | Whether serial entry is enforced. |
| `enforceBatchNumberDateEntry` | `bool` | Whether batch-number date entry is enforced. |
| `enforceExpirationDateEntry` | `bool` | Whether expiry-date entry is enforced. |
| `allowSerialDuplication` | `bool` | Whether serial duplication is allowed. |
| `autofillBatchNumber` | `bool` | Whether batch numbers are autofilled. |
| `allowForSMS` | `bool` | Whether SMS is allowed. |

### `StockItemUnitOfMeasureRequest`

Request to create or update an entry in the stock-item unit-of-measure chain.

| field | type | validation | description |
|---|---|---|---|
| `unitOfMeasureId` | `int` | GreaterThan(0) | The id of the unit of measure. |
| `uomName` | `string` | MaximumLength(200) | The display name of the unit of measure as it appears for this stock item. |
| `uomPartNumber` | `string` | MaximumLength(200) | The part number captured at this UOM level. |
| `uomPrice` | `decimal` |  | The sales price at this UOM level. |
| `uomDealerPrice` | `decimal` |  | The dealer price at this UOM level. |
| `uomSuperDealerPrice` | `decimal` |  | The super-dealer price at this UOM level. |
| `isPrimary` | `bool` |  | Whether this UOM is the primary (smallest) unit of the chain. Exactly one entry must be primary. |
| `ratioToPrimary` | `decimal` | GreaterThan(0m) .When(x => !x.IsPrimary) | The conversion ratio to the primary UOM. 1 for the primary; larger for parent units. |

### `StockItemUnitOfMeasureResponse`

Response describing a stock-item UOM entry.

| field | type | description |
|---|---|---|
| `id` | `int` | Identifier. |
| `stockItemId` | `int` | Stock-item identifier. |
| `unitOfMeasureId` | `int` | Unit-of-measure identifier. |
| `uomName` | `string` | UOM name. |
| `code` | `string` | Code. |
| `uomPartNumber` | `string` | UOM part number. |
| `uomPrice` | `decimal` | UOM price. |
| `uomDealerPrice` | `decimal` | Dealer price. |
| `uomSuperDealerPrice` | `decimal` | Super-dealer price. |
| `ratioToPrimary` | `decimal` | Ratio to the primary UOM. |
| `isPrimary` | `bool` | Whether this is the primary UOM. |
| `isDefault` | `bool` | Whether this is the default UOM. |
| `isUsedInTransaction` | `bool` | Whether the UOM is used in transactions. |
| `uomChainName` | `string` | Chain name. |
| `unitOfMeasureChainId` | `int` | Unit-of-measure chain identifier. |
| `stockItemPartNumber` | `string` | Stock-item part number for this UOM. |

### `StockItemWarehouseSummaryResponse`

Response describing a stock item's sales and stock position, broken down by warehouse.

| field | type | description |
|---|---|---|
| `stockitemId` | `int` | Stock item identifier. |
| `description` | `string` | Description. |
| `partNumber` | `string` | Part number. |
| `sku` | `string` | SKU. |
| `warehouses` | `List<StockItemWarehouseSummaryWarehouseResponse>` | Per-warehouse sales and stock position. |

### `StockItemWarehouseSummaryWarehouseResponse`

A single warehouse's sales and stock position for a stock item.

| field | type | description |
|---|---|---|
| `warehouseId` | `int` | Warehouse identifier. |
| `description` | `string` | Warehouse description. |
| `sales` | `decimal` | Sales quantity in Primary UOM for the requested period. |
| `salesReturn` | `decimal` | Sales-return quantity in Primary UOM for the requested period. |
| `balance` | `decimal` | Current on-hand stock balance. |
| `reserved` | `decimal` | Reserved stock quantity. |
| `expectedReceive` | `decimal` | Expected receiving quantity from open purchase orders. |
| `expectedTransfer` | `decimal` | Expected incoming transfer quantity. |

### `TaxReference`

Lightweight tax reference (id + name + rate).

| field | type | description |
|---|---|---|
| `id` | `int` | The tax id. |
| `name` | `string` | The tax name. |
| `rate` | `decimal` | The tax rate as a percentage. |

### `WarehouseReference`

Lightweight warehouse reference (id + code + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The warehouse id. |
| `code` | `string` | The warehouse code. |
| `name` | `string` | The warehouse name. |
