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:
- Create —
POST api/items/createitem(single) orcreateitems(batch): SKU + place + optional tag + extended properties. The item is born in statePresent. - Query —
GET api/items/getfiltereditems(OData),getitem/{id}, tag lookup, CSV export. - Maintain —
updateitem(descriptive fields),updatetag(tag swaps), media endpoints. - Move —
updateplaceexternal/moveitems, with movement history and CSV export. - Retire —
consumeitems/restoreitemsfor stock write-off and correction,deleteitemfor soft delete,getdeleteditemsfor 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
DraftorDiscontinuedrejects item creation. - A place to put the item in — the initial
PlaceIdis 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.
ItemTagListaccepts a list but more than one entry is rejected (400, fieldTagList). A tag number already attached to another live item fails withTagNotAvailable— 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
Valueif unset). - Quantity/multiplier follow the SKU type: a
StandardSKU requiresQuantity: 1(and multiplier 1 if sent); Kanban/variable types have their own bounds. Wrong combinations fail on fieldMultiplier.
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.
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:
- By id —
GET api/items/getitem/{id}; unknown ids return400NotFound. - By tag —
GET api/items/getitem?tagNumber=<number>. Matching is on the tag number alone (tagTypeis accepted but ignored), and a no-match returns200with anullbody, not an error.
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:
MovedOnsupports 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.ReferenceIdde-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 withItemAlreadyInLocation).ReasonIdmust 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 conventions —
400 with a JSON array of { ErrorCode, Message, Field }.
| Situation | Response |
|---|---|
Unknown SkuId on create | 400, field Sku |
SKU is Draft/Discontinued | 400 "SKU is in non-operational state (…); item creation not permitted." |
More than one entry in ItemTagList | 400 InvalidOperation, field TagList |
Tag number already on another live item (create or updatetag) | 400 TagNotAvailable, field Tag |
Invalid PlaceId on create | 400, field PlaceId |
| Quantity/multiplier breaks the SKU type's rules | 400, field Multiplier |
updateitem / getitem with an unknown id | 400 NotFound |
| Move: destination place unknown | 400 "New Place doesn't exist" |
Move: already at destination, no ReferenceId/coordinates | 400 ItemAlreadyInLocation |
| Move: reason scope is not Item | 400 InvalidReference "Invalid Reason Scope" |
| Move/delete blocked by item state | 400 ("Item not allowed to move" / state-gate message) |
| Consume/restore/batch move/batch create failures | 200 with per-row error entries — check the result lists |
| Tag lookup with no match | 200 with null body |
No token / wrong scheme (Bearer) | 401 |
Token lacks the Item module permission for the verb | 403 |
Related
- Item lifecycle — the state machine gating moves, consumes, and deletes.
- Identifiers & natural keys — tag numbers,
ExternalRef, and id precedence. - SKU management — previous in the setup path; items need an operational SKU.
- Templates & extended properties — the custom fields your items are created with.
- Reference pages: Create Item, Create Items, List Items, Get Item, Get Item by Tag, Update Item, Move Item, Move Items, Movement History, Consume Items, Restore Items, Update Item Tag, Delete Item, Deleted Items Sync.