Skip to main content

Creating & serialising items

An item is one physical, individually tracked unit of a SKU — the thing that carries the RFID tag or barcode. Serialising stock means turning "we have 40 of this SKU" into 40 addressable items, each with its own tag, location, state, and history. This guide walks the full working life of an item in Loca.fi: create against a SKU, query, update, move between places, write off and reinstate, swap tags, attach photos, and finally soft-delete.

Read Item lifecycle first for the state machine — states gate almost every operation below — and Identifiers & natural keys for how tag numbers and GUIDs address items.

Overview

The endpoints group into a simple arc:

  1. CreatePOST api/items/createitem (single) or createitems (batch): SKU + place + optional tag + extended properties. The item is born in state Present.
  2. QueryGET api/items/getfiltereditems (OData), getitem/{id}, tag lookup, CSV export.
  3. Maintainupdateitem (descriptive fields), updatetag (tag swaps), media endpoints.
  4. Moveupdateplaceexternal / moveitems, with movement history and CSV export.
  5. Retireconsumeitems / restoreitems for stock write-off and correction, deleteitem for soft delete, getdeleteditems for client cache sync.

All item endpoints require the Item module permission (read for queries, create/update/delete to match the verb).

Prerequisites

  • A token — see Getting started.
  • An operational SKU to serialise against — see SKU management. A SKU whose lifecycle state is Draft or Discontinued rejects item creation.
  • A place to put the item in — the initial PlaceId is required.
  • If the SKU's Item template defines extended properties, know their ids — Templates & extended properties.

Step-by-step

1. Create an item against a SKU

curl -sk -X POST "http://<host>/api/items/createitem" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"SkuId":"<skuId>","PlaceId":"<placeId>",
"Name":"Pallet 0042","Quantity":1,
"ItemTagList":[{"TagType":"PassiveRfid","TagNumber":"3034F8C4D19D8C4000000001"}],
"ItemExtendedPropertyList":[
{"ExtendedPropertyId":"<epId>","Value":"2026-08-01T00:00:00Z"}]}'

Returns 200 with the full ItemDetailDto. Rules that trip people up:

  • At most one tag. ItemTagList accepts a list but more than one entry is rejected (400, field TagList). A tag number already attached to another live item fails with TagNotAvailable — but deleting an item detaches its tag, so tag numbers of deleted items are reusable.
  • Extended properties must match the template exactly — send every property on the SKU's Item template, including non-required ones (empty Value if unset).
  • Quantity/multiplier follow the SKU type: a Standard SKU requires Quantity: 1 (and multiplier 1 if sent); Kanban/variable types have their own bounds. Wrong combinations fail on field Multiplier.

For bulk serialisation use POST api/items/createitems with {"Items":[<AddItemDto>, ...]}. It is a partial-success endpoint: 200 with AllCreated and an ItemsNotCreated error list — and it does not echo the created ids, so re-query afterwards.

Retail vs Asset

UnitCost on an item is an asset-management concept — per-asset cost for depreciation on individually valued equipment. Retail tenants leave it unset: retail pricing (supplier cost, RRP, sale price) lives on the SKU, and ItemSummaryDto.IsSkuLevelCost tells you which regime an item is under.

2. Query and filter

GET api/items/getfiltereditems is the OData workhorse ($filter, $orderby, $top, $skip, $count — remember %24 for $ in curl):

curl -sk "http://<host>/api/items/getfiltereditems?%24filter=SkuNumber%20eq%20%27MILK-1L%27%20and%20State%20eq%20%27Present%27&%24top=50&%24count=true" \
-H "Authorization: Token $TOKEN"

Returns a paged result of ItemSummaryDto — flattened names (SkuName, PlaceName, TagNumber, State) make most filters one-liners. Thumbnail is a signed URL valid for about one hour (see step 7). Results honour the caller's place-visibility restriction.

Single-item reads:

The same filter set exports as CSV via GET api/items/exportfiltereditems.

3. Update an item's details

POST api/items/updateitem is a full replace, not a patch — omitted fields are cleared:

curl -sk -X POST "http://<host>/api/items/updateitem" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"Id":"<itemId>","SkuId":"<skuId>","Name":"Pallet 0042 (repacked)",
"Description":"Repacked after inspection","AssetNumber":"A-0042",
"ItemExtendedPropertyList":[
{"ExtendedPropertyId":"<epId>","Value":"2026-09-01T00:00:00Z"}]}'

SkuId is required; changing it re-types the item and re-syncs its extended properties to the new SKU's template. updateitem does not change the tag (step 6), place (step 4), quantity, or multiplier — those have dedicated endpoints. Each successful update bumps EntityVersionNo.

4. Move stock between places

Single move — POST api/items/updateplaceexternal:

curl -sk -X POST "http://<host>/api/items/updateplaceexternal" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"Id":"<itemId>","NewPlaceId":"<placeId>","ReasonId":"<reasonId>",
"Comments":"Transfer to Store 1","ReferenceId":"WMS-MOVE-88123",
"MovedOn":"2026-07-06T22:15:00Z"}'

Three fields do the heavy lifting for integrations:

  • MovedOn supports back-dating — the movement is inserted into history at its chronological position; the item's current place only changes when the record is the newest one.
  • ReferenceId de-duplicates — a movement already recorded with the same reference is not re-applied, so replaying an integration feed is safe. Its presence also allows a move to the place the item is already in (otherwise rejected with ItemAlreadyInLocation).
  • ReasonId must reference a reason whose scope is Item.

Moves are gated by the item state machine — an item locked to an order (Allocated, Reserved, Dispatched, OnLoan) only accepts its order-workflow transitions; anything else is rejected with "Item not allowed to move".

Batch move — POST api/items/moveitems takes {"Items":[<same shape as above>, ...]}, processes rows oldest-MovedOn-first, and returns partial success: 200 with AllMoved plus ItemsNotMoved rows carrying ItemId and ErrorMessages.

Movement history is queryable via GET api/items/getitemplacehistory (OData) and exportable as CSV via exportitemplacehistory. Unless your $filter mentions DateMoved, both apply an implicit 365-day window — include an explicit DateMoved filter to see older history.

5. Consume and restore (write-off & correction)

POST api/items/consumeitems writes items off, addressed by id and/or tag number:

curl -sk -X POST "http://<host>/api/items/consumeitems" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"ItemIds":["<itemId>"],"TagNumbers":["3034F8C4D19D8C4000000002"],
"Comments":"Damaged in transit"}'

This is a partial-success endpoint: always 200, with failures listed in ItemsNotConsumed — each row has the ItemId/TagNumber, the item's current ItemState, and an ItemConsumeError of ItemDoesntExist, ItemAlreadyConsumed, or ItemStateChangeNotAllowed (order-bound states can't be consumed). An empty ItemsNotConsumed means everything succeeded — there is no AllConsumed convenience flag.

Consumed items are not deleted: they stay queryable and can be reinstated with POST api/items/restoreitems (same request shape). Only items currently in Consumed can be restored — anything else comes back in ItemsNotRestored with ItemRestoreError: "ItemNotConsumed". Restored items land in state Restored and behave like Present again.

6. Replace or remove a tag

POST api/items/updatetag both replaces and removes: one entry in ItemTagList swaps the tag, an empty list removes it.

curl -sk -X POST "http://<host>/api/items/updatetag" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"Id":"<itemId>",
"ItemTagList":[{"TagType":"PassiveRfid","TagNumber":"3034F8C4D19D8C4000000099"}]}'

The same TagNotAvailable reuse rule as create applies: the new number must not be attached to another live item (tags freed by item deletion or removal are fair game). Each swap writes a tag-replacement history row — but note the read side, getitemtaghistory, is currently non-functional (every call errors), so audit tag changes via the item's detail/state history for now.

7. Item media

Attach photos with POST api/items/uploadmedia — a multipart form upload with two parts per file, sharing the same field name: a form field whose value is a JSON metadata object (OwnerId = the item id, MediaType of Image/Document/Hyperlink/Archive/Other, optional IsPrimaryImage and UserDescription), plus the file part itself. The shared field name is what links metadata to file:

curl -sk -X POST "http://<host>/api/items/uploadmedia" \
-H "Authorization: Token $TOKEN" \
-F 'photo1={"OwnerId":"<itemId>","MediaType":"Image","IsPrimaryImage":true,"UserDescription":"Front of pallet"}' \
-F "photo1=@pallet-0042.jpg"

Returns true; repeat the field-name pairing to upload several files in one request (field names must be unique per file).

Then pick the thumbnail with POST api/items/setprimaryimage/{id}/{mediaId} — the primary image drives the Thumbnail on every item summary. Media ids are on the item's detail DTO (ItemMediaList). Thumbnail/media URLs returned in DTOs are signed and expire after about one hour; they resolve through GET api/items/getmedia/{path}, which is deliberately anonymous so <img> tags can load them without an Authorization header. Remove media (record and stored file) with DELETE api/items/deletemedia/{id}/{mediaId}.

8. Soft delete and client cache sync

DELETE api/items/deleteitem/{id} returns true, sets IsDeleted, and detaches the tag for reuse. Deletion is state-gated: allowed from the free-pool states plus Consumed, blocked while the item is order-bound — and, unusually, blocked from Restored (move or re-state the item first). Batch variant: POST api/items/deleteitems.

Clients that mirror the item list locally reconcile deletions with GET api/items/getdeleteditems/{deletedOnOrAfter}, where the route segment is a .NET ticks value (100 ns units since 0001-01-01) — the same watermark convention as the SKU and place deletion-sync endpoints. It returns the ids soft-deleted on or after that instant; store the ticks of each poll and pass it back next time.

Error handling

All failures follow the platform-wide error and response conventions400 with a JSON array of { ErrorCode, Message, Field }.

SituationResponse
Unknown SkuId on create400, field Sku
SKU is Draft/Discontinued400 "SKU is in non-operational state (…); item creation not permitted."
More than one entry in ItemTagList400 InvalidOperation, field TagList
Tag number already on another live item (create or updatetag)400 TagNotAvailable, field Tag
Invalid PlaceId on create400, field PlaceId
Quantity/multiplier breaks the SKU type's rules400, field Multiplier
updateitem / getitem with an unknown id400 NotFound
Move: destination place unknown400 "New Place doesn't exist"
Move: already at destination, no ReferenceId/coordinates400 ItemAlreadyInLocation
Move: reason scope is not Item400 InvalidReference "Invalid Reason Scope"
Move/delete blocked by item state400 ("Item not allowed to move" / state-gate message)
Consume/restore/batch move/batch create failures200 with per-row error entries — check the result lists
Tag lookup with no match200 with null body
No token / wrong scheme (Bearer)401
Token lacks the Item module permission for the verb403