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. Each key carries cadAttributeTag, the AutoCAD block attribute it maps to, or null when it has none, and cadSyncDirection, how its value syncs with drawings: push, pull or both for a key with a CAD tag, none for one without. A tagged key whose direction was never chosen reads push.
Response
{
"success": true,
"data": [
{
"id": 7,
"name": "Serial Number",
"order": 0,
"isActive": true,
"cadAttributeTag": "SERIAL_NUMBER",
"cadSyncDirection": "push",
"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,
"cadAttributeTag": "SERIAL_NUMBER",
"cadSyncDirection": "push",
"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,
"cadAttributeTag": null,
"cadSyncDirection": "none",
"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, set or clear its CAD attribute tag, choose how it syncs with drawings, and/or change its active status. All fields are optional — supply only what you want to change. The response is the key, as GET returns it.
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). |
cadAttributeTag |
string | No | The AutoCAD block attribute this key maps to, for example SERIAL_NUMBER. Omit it or send null to leave it unchanged; send "" to clear it. |
cadSyncDirection |
string | No | How the key's value syncs with drawings, in any case: push (AVstackr's value is written to the drawing), pull (the drawing's value is copied into AVstackr) or both for a key with a CAD tag; none for one without. Omit it or send null to leave it unchanged. See the field ownership on the CAD Drawings API. |
A CAD attribute tag is upper-cased, spaces and hyphens become _, and other characters are dropped; the result must start with a letter and be at most 32 characters of A-Z, 0-9 and _. It must be unique across your keys, deactivated ones included, and it cannot be one of the names reserved for built-in fields: AVSTACKR_ID, DEVICETAG, MANUFACTURER, MODEL, PARTNUMBER, DESCRIPTION, CATEGORY, LOCATION, SYSTEM, UNIT, QTY, RACKUNITS, HEIGHT, WIDTH, DEPTH, WEIGHT, PROJECT. A refused tag returns 400 INVALID_INPUT with the reason. Renaming a key keeps its tag. POST does not take a tag; set it with PUT after creating the key.
The fields are applied in the order name, cadAttributeTag, cadSyncDirection, isActive, each saved on its own, so a refused tag leaves a rename in the same request saved. Giving a key without a direction a tag sets push, and clearing the tag sets none. A cadSyncDirection other than the four words is refused with 400 INVALID_INPUT before anything is saved; one that does not fit the key, none on a tagged key or a direction on a key left without a tag, returns 400 INVALID_SYNC_DIRECTION with the name and tag already saved.
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, and a new unit gets its device tag at once when the tag pattern can be filled in. A quantity-1 line has one unit. Returns an array of { instanceId, instanceKey, unitNumber, deviceTag, attributes: [{ id, keyId, keyName, value, order }] }, where instanceKey is the unit's permanent GUID and deviceTag is its tag, such as DISP-003, or null while it is untagged.
PATCH /api/v1/projects/{projectId}/rooms/{roomId}/items/{itemId}/units/{instanceId}
Sets one unit's device tag by hand, replacing the tag it has or giving an untagged unit one. The unit is found by its instanceId and must belong to the project in the path. A tag cannot be cleared, only replaced. A tag that fits the unit's own tag pattern, such as DISP-010 on a display, counts in the project's numbering, so later automatic tags continue after it.
Path Parameters
| Field | Type | Required | Description |
|---|---|---|---|
projectId | integer | Yes | The project's ID |
roomId | integer | Yes | The room of the line |
itemId | integer | Yes | The line item's ID |
instanceId | integer | Yes | The unit's instanceId, from the units list |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
deviceTag |
string | Yes | The new tag: 1 to 50 characters of letters, digits and . _ / -, starting with a letter or digit, and unique within the project. Surrounding spaces are trimmed. |
Example Request
{
"deviceTag": "DISP-010"
}
Response (200 - Success)
{
"success": true,
"data": {
"instanceId": 5012,
"instanceKey": "8f14e45f-ceea-467a-9575-1b2f6b0c3a21",
"unitNumber": 2,
"deviceTag": "DISP-010"
},
"message": "Device tag set",
"timestamp": "2026-10-11T15:30:00Z"
}
Error Responses
| Status | Code | Description |
|---|---|---|
| 400 | INVALID_INPUT | deviceTag is missing or blank |
| 400 | INVALID_DEVICE_TAG | The tag breaks the character rule |
| 400 | DEVICE_TAG_IN_USE | Another unit in the project has the tag. The message names that unit, its device and its room. |
| 400 | UNKNOWN_FIELDS | The body names a field other than deviceTag |
| 403 | READ_ONLY_ACCESS | The caller has read-only access |
| 404 | UNIT_NOT_FOUND | No unit with that instanceId in the project |
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, or a refused cadAttributeTag (invalid, reserved, or used by another key) |
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 |
INVALID_DEVICE_TAG |
400 | A unit's new device tag breaks the character rule |
DEVICE_TAG_IN_USE |
400 | Another unit in the project already has the device tag |
UNIT_NOT_FOUND |
404 | The unit's instanceId is not a unit of the project |
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 / set CAD tag / 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 |
