Line Item Attributes API
Attach per-line key/value pairs to any line item — serial numbers, MAC addresses, firmware versions, warranty dates, asset tags. Attribute keys are managed at the tenant level (the catalog); entries are per-line-item.
Auth: Api-Key header. Same auth model as every other line-item endpoint.
Key Catalog (Tenant-Level)
Keys are the labels available for selection when adding attributes to a line item. Manage the catalog once at the tenant level; all projects share the same key list.
GET /api/v1/line-item-attribute-keys
List all active attribute keys for the tenant. Add ?includeInactive=true to also return deactivated keys.
Response
{
"success": true,
"data": [
{
"id": 7,
"name": "Serial Number",
"order": 0,
"isActive": true,
"usageCount": 12,
"updatedAt": "2026-05-24T15:30:00Z"
}
],
"timestamp": "2026-05-24T17:35:00Z"
}
GET /api/v1/line-item-attribute-keys/{id}
Fetch a single key by its integer ID.
Response
{
"success": true,
"data": {
"id": 7,
"name": "Serial Number",
"order": 0,
"isActive": true,
"usageCount": 12,
"updatedAt": "2026-05-24T15:30:00Z"
},
"timestamp": "2026-05-24T17:35:00Z"
}
POST /api/v1/line-item-attribute-keys
Create a new key in the tenant's attribute key catalog.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Display label for the key. Must be unique within the tenant. |
Response (200 OK)
{
"success": true,
"data": {
"id": 7,
"name": "Serial Number",
"order": 0,
"isActive": true,
"usageCount": 0,
"updatedAt": "2026-05-24T17:35:00Z"
},
"message": "Attribute key created",
"timestamp": "2026-05-24T17:35:00Z"
}
PUT /api/v1/line-item-attribute-keys/{id}
Rename a key and/or change its active status. All fields are optional — supply only what you want to change.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | No | New display label. Must be unique if provided. |
isActive |
boolean | No | Set to false to deactivate (hides from dropdown but preserves existing entries). |
Deactivating a key hides it from the dropdown on new entries but leaves existing entries intact. Deletion is permanent — use it only when no line items reference the key (the DELETE endpoint returns a 409 with a usage count if the key is still in use).
DELETE /api/v1/line-item-attribute-keys/{id}
Permanently delete a key. Succeeds only if no line-item entries reference it.
409 KEY_IN_USE
If the key is in use, deletion is blocked and the response includes a usage count:
{
"success": false,
"error": {
"code": "KEY_IN_USE",
"message": "Attribute key 7 is used on 7 line item entries. Deactivate it (set isActive=false) or remove those entries first.",
"details": "usage_count=7"
},
"timestamp": "2026-05-24T17:35:00Z"
}
Entries (Per-Line-Item)
Each line item has its own set of attribute entries — key/value pairs drawn from the tenant's key catalog.
GET /api/v1/projects/{projectId}/rooms/{roomId}/items/{itemId}/attributes
List all attribute entries for a line item.
Response
{
"success": true,
"data": [
{
"id": 4471,
"equipmentId": 1234,
"keyId": 7,
"keyName": "Serial Number",
"keyIsActive": true,
"value": "SN-00142",
"order": 0
}
],
"timestamp": "2026-05-24T17:35:00Z"
}
POST /api/v1/projects/{projectId}/rooms/{roomId}/items/{itemId}/attributes
Add a new attribute entry to a line item. Supply either keyId (the key's integer ID) or keyName (the key's display label) — keyId wins if both are provided.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
keyId |
integer | One of keyId / keyName | Integer ID of the attribute key from the tenant catalog. Takes priority over keyName. |
keyName |
string | One of keyId / keyName | Display name of the attribute key (case-insensitive match). Only active keys are searched. |
value |
string | No | The attribute value. May be empty. |
Response (200 OK)
{
"success": true,
"data": {
"id": 4471,
"equipmentId": 1234,
"keyId": 7,
"keyName": "Serial Number",
"keyIsActive": true,
"value": "SN-00142",
"order": 0
},
"message": "Attribute entry created",
"timestamp": "2026-05-24T17:35:00Z"
}
PUT /api/v1/projects/{projectId}/rooms/{roomId}/items/{itemId}/attributes/{entryId}
Update the value and/or key of an existing entry. All fields are optional — omit any field to leave it unchanged.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
keyId |
integer | No | Change which key this entry uses (by integer ID). Takes priority over keyName if both supplied. |
keyName |
string | No | Change which key this entry uses (by display label, case-insensitive). Only active keys are searched. |
value |
string | No | New value for the entry. Preserved unchanged if omitted. |
DELETE /api/v1/projects/{projectId}/rooms/{roomId}/items/{itemId}/attributes/{entryId}
Delete a single attribute entry. The key in the tenant catalog is not affected.
Per-Unit Attributes (Device Instances)
A line item with quantity > 1 has one physical unit per copy, each with its own attributes (serial numbers, MAC addresses). Units are keyed by a project-stable identity, so their attributes survive version revisions as a single source of truth. Duplicate lines of the same make/model in a room share one unit pool sized to their summed quantity. Change-order lines that have no live line item are not addressable by itemId here.
GET /api/v1/projects/{projectId}/rooms/{roomId}/items/{itemId}/units
List the physical units of a line item and each unit's attributes. Units are created on first access. Returns an array of { instanceId, unitNumber, attributes: [{ id, keyId, keyName, value, order }] }. Empty for a quantity-1 line (use the line attribute endpoints).
POST /api/v1/projects/{projectId}/rooms/{roomId}/items/{itemId}/units/{instanceId}/attributes
Add one attribute entry to a single unit by its instanceId (from the units list). Body: { "keyId": <int> } or { "keyName": "<active key name>" } plus an optional "value". Returns the created entry with its id.
POST /api/v1/projects/{projectId}/rooms/{roomId}/items/{itemId}/units/by-number/{unitNumber}/attributes
One-call variant: add an attribute to a unit by its 1-based unit number (e.g. .../units/by-number/2/attributes for the second unit) without first looking up its instanceId. The unit is created if it doesn't exist yet. Same body and response as the by-instance variant.
PUT /api/v1/projects/{projectId}/rooms/{roomId}/items/{itemId}/units/{instanceId}/attributes/{entryId}
Update one unit attribute. Sparse — supply value to change it (omit to leave unchanged), and optionally keyId/keyName to repoint the key.
DELETE /api/v1/projects/{projectId}/rooms/{roomId}/items/{itemId}/units/{instanceId}/attributes/{entryId}
Delete one unit attribute entry. The tenant key catalog is not affected.
Error Codes
| Code | HTTP | When |
|---|---|---|
INVALID_INPUT |
400 | Blank name, missing required field, neither keyId nor keyName supplied |
NOT_FOUND |
404 | Attribute key not found (catalog endpoints) |
LINE_ITEM_NOT_FOUND |
404 | Line item not found or belongs to another tenant |
ATTRIBUTE_NOT_FOUND |
404 | Attribute entry not found or belongs to a different line item |
KEY_NOT_FOUND |
404 | The keyId or keyName on an entry create/update doesn't resolve to an active key |
KEY_INACTIVE |
409 | POST entries only — keyId points at a key that exists but is deactivated |
DUPLICATE_KEY_NAME |
409 | Case-insensitive name collision when creating or renaming a key |
KEY_IN_USE |
409 | Hard delete attempted on a key that still has entries; error.details contains usage_count=N |
Endpoint Summary
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/line-item-attribute-keys |
List tenant attribute keys |
| GET | /api/v1/line-item-attribute-keys/{id} |
Get one key |
| POST | /api/v1/line-item-attribute-keys |
Create a key |
| PUT | /api/v1/line-item-attribute-keys/{id} |
Rename / activate / deactivate a key |
| DELETE | /api/v1/line-item-attribute-keys/{id} |
Hard-delete a key (blocked if in use) |
| GET | /api/v1/projects/{p}/rooms/{r}/items/{i}/attributes |
List entries on a line item |
| POST | /api/v1/projects/{p}/rooms/{r}/items/{i}/attributes |
Add an entry |
| PUT | /api/v1/projects/{p}/rooms/{r}/items/{i}/attributes/{entryId} |
Update an entry |
| DELETE | /api/v1/projects/{p}/rooms/{r}/items/{i}/attributes/{entryId} |
Delete an entry |
