CAD Drawings API
Link AutoCAD drawings to a project, list the units each one places, and sync a drawing with the project: submit its blocks, get a plan, apply the answers.
This is the contract the AutoCAD plugin works from after it has placed, linked, moved, edited or erased blocks; the CAD Blocks API is the one it reads before it places them, and signs in the same way. For how drawings look in the app, see Device Tags.
Writing a plugin of your own? The AutoCAD Plugin guide shows when AVstackr's plugin links, submits, applies, patches and unplaces, step by step from the drafter's side.
How a Sync Works
- Link the DWG once with
POST /api/v1/projects/{projectId}/cad/drawings. The plugin keeps the returned id inside the drawing, as What the Plugin Stores in the DWG describes below. A drawing has one view; a unit has at most one placement per drawing.
- Submit every AVstackr block the drawing holds with
POST …/drawings/{id}/submit. AVstackr matches each block to a unit and answers with a plan: safe actions it may apply without asking, and proposals a person decides. With autoApplySafe: true the safe actions are applied in the same call.
- Send the person's answers with
POST …/drawings/{id}/apply. AVstackr plans again from what it has stored, applies each answer that still holds and skips the rest with a reason.
- Carry out the result's
restamp and refresh lists in the drawing. They are the plugin's to-do list; AVstackr writes nothing for them.
A plan is not stored: the plugin holds it while the person decides.
Matching
Each posted block is matched to a unit in this order, the first hit winning:
- By key:
instanceKey names a unit of the project.
- By handle: the drawing has a placement recorded at the block's
handle, for example after ATTSYNC stripped the key. The plan restamps the block.
- By device tag: the block's
DEVICETAG attribute names, ignoring letter case, exactly one unit that has no placement on this drawing and that no other block claims.
When two blocks claim one unit, the block at the unit's recorded handle keeps it, else the first posted; the other is a copy. A block that matches nothing is unmatched. A recorded placement whose unit no posted block keeps is missing: AVstackr learns of erased blocks from their absence, so every submit carries the whole set.
Rules for Blocks
- A submit carries at most 10,000 blocks, and an apply at most 10,000 decisions. A body over 10,000,000 bytes is refused before AVstackr's own checks run.
handle is the INSERT's AutoCAD entity handle: 1 to 16 hex digits, trimmed and upper-cased, unique within one submit.
blockName is 1 to 255 characters; layer and visibilityState at most 255 each.
x, y, z, rotation (radians), scaleX, scaleY and scaleZ are required finite numbers.
attributes holds every ATTRIB the block has, by tag, an empty one as "". A tag left out is one the block lacks, which AVstackr never refreshes or pulls. Tags are upper-cased and values trimmed.
requested is the palette action asked for on a block without a key, such as link or add. It is a hint only: the plan offers the same answers either way.
Field Ownership
Which side's value wins for each field a matched block carries. Values compare after trimming, and an empty value equals none.
| Field | Owner | On submit |
AVSTACKR_ID, MANUFACTURER, MODEL, PARTNUMBER, DESCRIPTION, CATEGORY, LOCATION, SYSTEM, UNIT, QTY, RACKUNITS, HEIGHT, WIDTH, DEPTH, WEIGHT, PROJECT | Project | A different drawing value is a refresh. Never pulled. |
DEVICETAG | Project | refresh. A tag typed into a block counts only for matching an unmatched block; a placed unit's tag is changed in AVstackr. |
Geometry: x, y, z, rotation, scale, layer, visibilityState | Drawing | Recorded on the placement (geometry); never pushed, and AVstackr never moves a block. |
Attribute key, push | Project | refresh when the drawing differs. |
Attribute key, pull | Drawing | pull when the drawing's value changed since the last sync. The units contract still carries the project's value, so a new block is filled in. |
Attribute key, both | Shared | Only the drawing changed: pull. Only the project changed: refresh. Both changed: conflict. Before any value has been synced, the drawing's value is pulled when it has one. |
| Attribute key without a CAD tag | Neither | Not in the units contract and not synced. |
An attribute key's direction is its cadSyncDirection on the Line Item Attributes API. A key with a CAD tag whose direction was never chosen syncs push. A pulled value is written to the unit's own attribute, up to 500 characters; a longer one is skipped with a reason. A refresh of a field the project has no value for writes an empty value.
What the Plugin Stores in the DWG
For plugin authors: a linked drawing holds exactly this for AVstackr, and nothing else.
- In the drawing's named object dictionary, an XRECORD named
AVSTACKR holding the drawing's id, the projectId, the tenant id and the AVstackr base URL. This is the drawing's identity. AutoCAD's FINGERPRINTGUID, sent as fingerprintGuid, is only a hint for re-linking a DWG whose dictionary entry was lost, since a Save As copies it.
- On every linked block (INSERT), XDATA under the application name
AVSTACKR holding the unit's instanceKey, hyphenated, and the hidden AVSTACKR_ID attribute with the same value when the block defines it. ATTSYNC can strip XDATA, so both are written and either is read back as the block's instanceKey.
A restamp with an instanceKey writes both; one with instanceKey: null removes both.
GET /api/v1/projects/{projectId}/cad/drawings
Returns the project's linked drawings in the order they were linked, each with how many units it places.
Path Parameters
| Field | Type | Required | Description |
projectId | integer | Yes | The project ID |
Example Request
GET /api/v1/projects/12/cad/drawings
Authorization: Bearer avs_YOUR_API_KEY
Response (200 - Success)
{
"success": true,
"data": [
{
"id": 4,
"projectId": 12,
"name": "L1-AV",
"viewType": "plan",
"fingerprintGuid": "3c9d2a71-8e4b-4f06-b1d5-7a2e9c0f4b18",
"filePathHint": "C:\\Projects\\Northwind\\L1-AV.dwg",
"placedUnits": 2,
"lastSubmitAt": "2026-10-12T15:04:05Z",
"lastSubmitVersionGuid": "d41e7b2c-6a90-4f3e-8c15-2b7f9e0a4d63",
"lastApplyAt": "2026-10-12T15:04:05Z",
"createdAt": "2026-10-12T14:30:00Z",
"createdBy": "8d3e5c1a-2f4b-4e6d-9a7c-1b2d3e4f5a6b",
"updatedAt": "2026-10-12T15:04:05Z"
}
],
"message": null,
"timestamp": "2026-10-12T15:10:00Z"
}
Drawing Fields
| Field | Type | Description |
id | integer | The drawing's ID, the one the plugin keeps in the DWG |
projectId | integer | The project the drawing belongs to |
name | string | 1 to 200 characters |
viewType | string | plan, ceiling, elevation or schematic: the view the drawing's blocks are resolved for |
fingerprintGuid | string (UUID) or null | AutoCAD's FINGERPRINTGUID, a re-link hint |
filePathHint | string or null | Where the plugin last read the DWG; display only |
placedUnits | integer | How many units the drawing places |
lastSubmitAt | string (ISO 8601) or null | When the plugin last submitted the drawing |
lastSubmitVersionGuid | string (UUID) or null | AutoCAD's VERSIONGUID sent with that submit; it changes on every save |
lastApplyAt | string (ISO 8601) or null | When decisions or safe actions were last applied |
createdAt | string (ISO 8601) | When the drawing was linked |
createdBy | string or null | The user ID of the person who linked it |
updatedAt | string (ISO 8601) | When the drawing last changed |
Error Responses
| Status | Code | Description |
| 404 | PROJECT_NOT_FOUND | The project is not one of yours |
POST /api/v1/projects/{projectId}/cad/drawings
Links a drawing to the project. The returned id is what the plugin keeps in the DWG. The signed-in user is recorded as createdBy.
Path Parameters
| Field | Type | Required | Description |
projectId | integer | Yes | The project ID |
Request Body
| Field | Type | Required | Description |
name | string | Yes | Trimmed, 1 to 200 characters. The plugin sends the file name without its extension. |
viewType | string | Yes | plan, ceiling, elevation or schematic, in any case |
fingerprintGuid | string (UUID) | No | AutoCAD's FINGERPRINTGUID |
filePathHint | string | No | The path the plugin read the DWG from, at most 500 characters |
Example Request
POST /api/v1/projects/12/cad/drawings
Authorization: Bearer avs_YOUR_API_KEY
Content-Type: application/json
{
"name": "L1-AV",
"viewType": "plan",
"fingerprintGuid": "3c9d2a71-8e4b-4f06-b1d5-7a2e9c0f4b18",
"filePathHint": "C:\\Projects\\Northwind\\L1-AV.dwg"
}
Response (201 - Created)
With a Location header pointing at the new drawing.
{
"success": true,
"data": {
"id": 4,
"projectId": 12,
"name": "L1-AV",
"viewType": "plan",
"fingerprintGuid": "3c9d2a71-8e4b-4f06-b1d5-7a2e9c0f4b18",
"filePathHint": "C:\\Projects\\Northwind\\L1-AV.dwg",
"placedUnits": 0,
"lastSubmitAt": null,
"lastSubmitVersionGuid": null,
"lastApplyAt": null,
"createdAt": "2026-10-12T14:30:00Z",
"createdBy": "8d3e5c1a-2f4b-4e6d-9a7c-1b2d3e4f5a6b",
"updatedAt": "2026-10-12T14:30:00Z"
},
"message": "Drawing 'L1-AV' linked",
"timestamp": "2026-10-12T14:30:00Z"
}
Error Responses
| Status | Code | Description |
| 400 | INVALID_INPUT | The body is missing, viewType is missing or not a view, the name is not 1 to 200 characters, or the path is over 500 |
| 400 | UNKNOWN_FIELDS | The body names a field not listed above |
| 403 | READ_ONLY_ACCESS | The caller has read-only access |
| 404 | PROJECT_NOT_FOUND | The project is not one of yours |
GET /api/v1/projects/{projectId}/cad/drawings/{id}
Returns one linked drawing with the fields above.
Path Parameters
| Field | Type | Required | Description |
projectId | integer | Yes | The project ID |
id | integer | Yes | The drawing ID |
Example Request
GET /api/v1/projects/12/cad/drawings/4
Authorization: Bearer avs_YOUR_API_KEY
Response (200 - Success)
{
"success": true,
"data": { "id": 4, "projectId": 12, "name": "L1-AV", "viewType": "plan", "placedUnits": 2, ... },
"message": null,
"timestamp": "2026-10-12T15:10:00Z"
}
Error Responses
| Status | Code | Description |
| 404 | PROJECT_NOT_FOUND | The project is not one of yours |
| 404 | DRAWING_NOT_FOUND | The project has no drawing with the ID |
PUT /api/v1/projects/{projectId}/cad/drawings/{id}
Renames a drawing.
Path Parameters
| Field | Type | Required | Description |
projectId | integer | Yes | The project ID |
id | integer | Yes | The drawing ID |
Request Body
| Field | Type | Required | Description |
name | string | Yes | Trimmed, 1 to 200 characters |
Example Request
PUT /api/v1/projects/12/cad/drawings/4
Authorization: Bearer avs_YOUR_API_KEY
Content-Type: application/json
{ "name": "L1-AV Rev B" }
Response (200 - Success)
{
"success": true,
"data": { "id": 4, "projectId": 12, "name": "L1-AV Rev B", ... },
"message": "Drawing renamed",
"timestamp": "2026-10-12T15:12:00Z"
}
Error Responses
| Status | Code | Description |
| 400 | INVALID_INPUT | The body is missing, or the name is not 1 to 200 characters |
| 400 | UNKNOWN_FIELDS | The body names a field other than name |
| 403 | READ_ONLY_ACCESS | The caller has read-only access |
| 404 | PROJECT_NOT_FOUND | The project is not one of yours |
| 404 | DRAWING_NOT_FOUND | The project has no drawing with the ID |
DELETE /api/v1/projects/{projectId}/cad/drawings/{id}
Removes the drawing link and its placements. The units stay in the project, with their tags and attributes. removedPlacements says how many placements went with it.
Path Parameters
| Field | Type | Required | Description |
projectId | integer | Yes | The project ID |
id | integer | Yes | The drawing ID |
Example Request
DELETE /api/v1/projects/12/cad/drawings/4
Authorization: Bearer avs_YOUR_API_KEY
Response (200 - Success)
{
"success": true,
"data": { "removedPlacements": 2 },
"message": "Drawing removed",
"timestamp": "2026-10-12T15:20:00Z"
}
Error Responses
| Status | Code | Description |
| 403 | READ_ONLY_ACCESS | The caller has read-only access |
| 404 | PROJECT_NOT_FOUND | The project is not one of yours |
| 404 | DRAWING_NOT_FOUND | The project has no drawing with the ID |
GET /api/v1/projects/{projectId}/cad/drawings/{id}/placements
Returns the units the drawing places, in the order they were placed, each with its tag, make, model and room in the project's active version and the block that draws it.
Path Parameters
| Field | Type | Required | Description |
projectId | integer | Yes | The project ID |
id | integer | Yes | The drawing ID |
Example Request
GET /api/v1/projects/12/cad/drawings/4/placements
Authorization: Bearer avs_YOUR_API_KEY
Response (200 - Success)
{
"success": true,
"data": [
{
"placementId": 17,
"drawingId": 4,
"drawingName": "L1-AV",
"view": "plan",
"instanceId": 501,
"instanceKey": "6f1c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f",
"deviceTag": "DISP-003",
"unitNumber": 3,
"make": "Samsung",
"model": "QM85C",
"room": "Conference Room A",
"handle": "2F3A",
"blockName": "AVS_DISP_WALL",
"cadBlockId": 9,
"layer": "AV-DISPLAY",
"visibilityState": "TOP",
"lastSeenAt": "2026-10-12T15:04:05Z",
"lastPushedFingerprint": "ec5e72b17d52f207ec87aaaaf1c27d84998755b6295427db8294070a16ec2ba7"
}
],
"message": null,
"timestamp": "2026-10-12T15:10:00Z"
}
Placement Fields
| Field | Type | Description |
placementId | integer | The placement's ID |
drawingId, drawingName | integer, string | The drawing |
view | string | The drawing's view |
instanceId, instanceKey | integer, string (UUID) | The unit and its permanent key, the one the block carries |
deviceTag | string or null | The unit's device tag; null while it has none |
unitNumber | integer | The unit's number among the room's units of the same make and model |
make, model | string | The unit's line in the active version; empty when the line is no longer there |
room | string | The unit's room in the active version; empty when the room is no longer there |
handle | string | The block's AutoCAD handle in this drawing |
blockName | string | The block definition name |
cadBlockId | integer or null | The library block, when the plugin named one of yours; null for a hand-drawn block |
layer, visibilityState | string, string or null | The block's layer and visibility state as last recorded |
lastSeenAt | string (ISO 8601) | The last submit that included the block |
lastPushedFingerprint | string | The unit's fingerprint when the placement was recorded |
Error Responses
| Status | Code | Description |
| 404 | PROJECT_NOT_FOUND | The project is not one of yours |
| 404 | DRAWING_NOT_FOUND | The project has no drawing with the ID |
PATCH /api/v1/projects/{projectId}/cad/drawings/{id}/placements/{placementId}
A live edit, without a plan: writes one placement's geometry, or one attribute the drawing owns, as the user changes it. Nothing is written when any part is refused.
geometry is written as given and must carry all seven numbers.
attribute is written to the unit only when its tag is an active attribute key's that syncs pull or both. A tag the project owns, a built-in tag or a push key's, is refused with 409 FIELD_PROJECT_OWNED, and error.details.projectValue holds the project's value (empty when it has none) for the plugin to write back into the block. A tag no active key carries, or a value over 500 characters, is 400 INVALID_INPUT.
Path Parameters
| Field | Type | Required | Description |
projectId | integer | Yes | The project ID |
id | integer | Yes | The drawing ID |
placementId | integer | Yes | The placement ID |
Request Body
| Field | Type | Required | Description |
geometry | object | One of the two | The block's whole geometry, with the fields below |
attribute | object | One of the two | One attribute the drawing changed, with the fields below |
Geometry Fields
| Field | Type | Required | Description |
x, y, z | number | Yes | The insertion point, in drawing units |
rotation | number | Yes | Radians, as AutoCAD reports it |
scaleX, scaleY, scaleZ | number | Yes | The scale factors |
layer | string | No | At most 255 characters |
visibilityState | string | No | A dynamic block's visibility state, at most 255 characters |
Attribute Fields
| Field | Type | Required | Description |
tag | string | Yes | The CAD tag, in any case |
value | string | No | The drawing's value, trimmed, at most 500 characters; empty or left out clears it |
Example Request
PATCH /api/v1/projects/12/cad/drawings/4/placements/17
Authorization: Bearer avs_YOUR_API_KEY
Content-Type: application/json
{ "attribute": { "tag": "FIRMWARE", "value": "2.3" } }
Response (200 - Success)
{
"success": true,
"data": { "ok": true },
"message": "Placement updated",
"timestamp": "2026-10-12T15:30:00Z"
}
Example Request: A Field the Project Owns
PATCH /api/v1/projects/12/cad/drawings/4/placements/17
Authorization: Bearer avs_YOUR_API_KEY
Content-Type: application/json
{
"geometry": { "x": 140.25, "y": 44, "z": 0, "rotation": 0, "scaleX": 1, "scaleY": 1, "scaleZ": 1, "layer": "AV-DISPLAY", "visibilityState": "TOP" },
"attribute": { "tag": "DEVICETAG", "value": "DISP-3" }
}
Response (409 - FIELD_PROJECT_OWNED)
{
"success": false,
"error": {
"code": "FIELD_PROJECT_OWNED",
"message": "The project owns DEVICETAG: the drawing takes the project's value.",
"details": { "projectValue": "DISP-003" }
},
"timestamp": "2026-10-12T15:31:00Z"
}
The geometry in that request is not written either: send it again on its own.
Error Responses
| Status | Code | Description |
| 400 | INVALID_INPUT | The body is missing or names neither part, a geometry number is missing or not finite, the tag is blank or no active key carries it, or the value is over 500 characters |
| 400 | UNKNOWN_FIELDS | The body, geometry or attribute names a field not listed above |
| 403 | READ_ONLY_ACCESS | The caller has read-only access |
| 404 | DRAWING_NOT_FOUND | The project has no drawing with the ID |
| 404 | PLACEMENT_NOT_FOUND | The drawing has no placement with the ID |
| 409 | FIELD_PROJECT_OWNED | The tag is one the project owns; error.details.projectValue holds its value |
DELETE /api/v1/projects/{projectId}/cad/drawings/{id}/placements/{placementId}
Unplaces one unit: deletes the placement. The unit stays in the project and its block stays in the drawing; a later submit places the unit again while the block still carries its key.
Path Parameters
| Field | Type | Required | Description |
projectId | integer | Yes | The project ID |
id | integer | Yes | The drawing ID |
placementId | integer | Yes | The placement ID |
Example Request
DELETE /api/v1/projects/12/cad/drawings/4/placements/18
Authorization: Bearer avs_YOUR_API_KEY
Response (200 - Success)
{
"success": true,
"data": { "ok": true },
"message": "Unit unplaced",
"timestamp": "2026-10-12T15:35:00Z"
}
Error Responses
| Status | Code | Description |
| 403 | READ_ONLY_ACCESS | The caller has read-only access |
| 404 | PROJECT_NOT_FOUND | The project is not one of yours |
| 404 | DRAWING_NOT_FOUND | The project has no drawing with the ID |
| 404 | PLACEMENT_NOT_FOUND | The drawing has no placement with the ID |
POST /api/v1/projects/{projectId}/cad/drawings/{id}/submit
Submits every AVstackr block in the drawing and answers with the plan. A block is AVstackr's when it carries the AVSTACKR XDATA or an AVSTACKR_ID or DEVICETAG attribute, or when the user linked it or asked to add it in the plugin. The submit records the drawing's submit time, versionGuid and filePathHint, and when each matched block was last seen. With autoApplySafe: true the safe actions are applied too, applied reports what that did (see the apply result below), and planVersion is the version after them, the one to send with the decisions; otherwise applied is null.
Path Parameters
| Field | Type | Required | Description |
projectId | integer | Yes | The project ID |
id | integer | Yes | The drawing ID |
Request Body
| Field | Type | Required | Description |
versionGuid | string (UUID) | No | AutoCAD's VERSIONGUID of the saved drawing |
savedAt | string (ISO 8601) | No | When the drawing was last saved, UTC |
filePathHint | string | No | The path the plugin read the DWG from, at most 500 characters |
autoApplySafe | boolean | No | Default false. true applies the plan's safe actions in the same call. |
blocks | array of object | Yes | Every AVstackr block in the drawing, with the fields below |
Block Fields
| Field | Type | Required | Description |
handle | string | Yes | The INSERT's handle, 1 to 16 hex digits; the block's key in the plan and in decisions |
blockName | string | Yes | The block definition name, 1 to 255 characters |
cadBlockId | integer | No | The library block the plugin inserted; leave it out for a hand-drawn block |
instanceKey | string (UUID) | No | The unit key read from the block's XDATA or AVSTACKR_ID; null when it has neither |
attributes | object | No | Every ATTRIB by tag, an empty one as "" |
x, y, z | number | Yes | The insertion point, in drawing units |
rotation | number | Yes | Radians |
scaleX, scaleY, scaleZ | number | Yes | The scale factors |
layer | string | No | The block's layer |
visibilityState | string | No | A dynamic block's visibility state |
requested | string | No | The palette action asked for on a block without a key, such as link or add; a hint only |
Example Request
Drawing 4 places DISP-003 at handle 2F3A and DISP-002 at 0B77. The Serial Number key syncs both and the Firmware key pull. Since the last sync, the block at 2F3A was moved, its tag text mistyped, its serial number and firmware edited in the drawing while someone also edited the serial number in AVstackr to SN-4471-A; it was copied to 9C01; a display drawn by hand at 9C02 was picked for linking; and DISP-002's block was erased.
POST /api/v1/projects/12/cad/drawings/4/submit
Authorization: Bearer avs_YOUR_API_KEY
Content-Type: application/json
{
"versionGuid": "d41e7b2c-6a90-4f3e-8c15-2b7f9e0a4d63",
"savedAt": "2026-10-12T15:02:40Z",
"filePathHint": "C:\\Projects\\Northwind\\L1-AV.dwg",
"autoApplySafe": true,
"blocks": [
{
"handle": "2F3A",
"blockName": "AVS_DISP_WALL",
"cadBlockId": 9,
"instanceKey": "6f1c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f",
"attributes": { "DEVICETAG": "DISP-3", "MANUFACTURER": "Samsung", "MODEL": "QM85C", "SERIAL_NUMBER": "SN-4471-B", "FIRMWARE": "2.2" },
"x": 132.0, "y": 44.0, "z": 0, "rotation": 0, "scaleX": 1, "scaleY": 1, "scaleZ": 1,
"layer": "AV-DISPLAY",
"visibilityState": "TOP",
"requested": null
},
{
"handle": "9C01",
"blockName": "AVS_DISP_WALL",
"cadBlockId": 9,
"instanceKey": "6f1c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f",
"attributes": { "DEVICETAG": "DISP-003", "MANUFACTURER": "Samsung", "MODEL": "QM85C", "SERIAL_NUMBER": "SN-4471", "FIRMWARE": "2.1" },
"x": 180.0, "y": 44.0, "z": 0, "rotation": 0, "scaleX": 1, "scaleY": 1, "scaleZ": 1,
"layer": "AV-DISPLAY",
"visibilityState": "TOP",
"requested": null
},
{
"handle": "9C02",
"blockName": "DISP_GENERIC",
"cadBlockId": null,
"instanceKey": null,
"attributes": { "MANUFACTURER": "Samsung", "MODEL": "QM85C", "LOCATION": "Conference Room A", "SERIAL_NUMBER": "" },
"x": 60.0, "y": 12.5, "z": 0, "rotation": 3.141592653589793, "scaleX": 1, "scaleY": 1, "scaleZ": 1,
"layer": "AV-DISPLAY",
"visibilityState": null,
"requested": "link"
}
]
}
Response (200 - Success)
{
"success": true,
"data": {
"plan": {
"drawingId": 4,
"planVersion": "f1080ca40d37e0adc56b2d6b37508f3ae6ac1597e706414fd12a5b18d0cfc0e9",
"generatedAt": "2026-10-12T15:04:05Z",
"safe": [
{
"kind": "geometry", "handle": "2F3A",
"instanceId": 501, "instanceKey": "6f1c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f", "deviceTag": "DISP-003",
"blockName": "AVS_DISP_WALL", "cadBlockId": 9,
"geometry": { "x": 132, "y": 44, "z": 0, "rotation": 0, "scaleX": 1, "scaleY": 1, "scaleZ": 1, "layer": "AV-DISPLAY", "visibilityState": "TOP" },
"tag": null, "value": null, "attributes": null
},
{
"kind": "pull", "handle": "2F3A",
"instanceId": 501, "instanceKey": "6f1c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f", "deviceTag": "DISP-003",
"blockName": null, "cadBlockId": null, "geometry": null,
"tag": "FIRMWARE", "value": "2.2", "attributes": null
},
{
"kind": "refresh", "handle": "2F3A",
"instanceId": 501, "instanceKey": "6f1c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f", "deviceTag": "DISP-003",
"blockName": null, "cadBlockId": null, "geometry": null,
"tag": null, "value": null, "attributes": { "DEVICETAG": "DISP-003" }
}
],
"proposals": [
{
"kind": "conflict", "handle": "2F3A",
"instanceId": 501, "instanceKey": "6f1c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f", "deviceTag": "DISP-003",
"field": "SERIAL_NUMBER", "drawingValue": "SN-4471-B", "projectValue": "SN-4471-A",
"options": [
{ "id": "keepDrawing", "label": "Keep the drawing's value", "blocked": null, "hint": null },
{ "id": "keepProject", "label": "Keep the project's value", "blocked": null, "hint": null }
],
"candidates": [], "lineCandidates": [], "productCandidates": []
},
{
"kind": "clone", "handle": "9C01",
"instanceId": 501, "instanceKey": "6f1c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f", "deviceTag": "DISP-003",
"field": null, "drawingValue": null, "projectValue": null,
"options": [
{ "id": "newUnit", "label": "Add a unit to the line", "blocked": null, "hint": null },
{ "id": "graphicOnly", "label": "Keep as a graphic only", "blocked": null, "hint": null },
{ "id": "ignore", "label": "Ignore for now", "blocked": null, "hint": null }
],
"candidates": [],
"lineCandidates": [
{ "productProjectRoomId": 88, "make": "Samsung", "model": "QM85C", "room": "Conference Room A", "quantity": 3 }
],
"productCandidates": []
},
{
"kind": "link", "handle": "9C02",
"instanceId": null, "instanceKey": null, "deviceTag": null,
"field": null, "drawingValue": null, "projectValue": null,
"options": [
{ "id": "linkTo", "label": "Link to an existing unit", "blocked": null, "hint": null },
{ "id": "newUnit", "label": "Add a unit to the line", "blocked": null, "hint": null },
{ "id": "newLine", "label": "Add a new line", "blocked": null, "hint": null },
{ "id": "graphicOnly", "label": "Keep as a graphic only", "blocked": null, "hint": null },
{ "id": "ignore", "label": "Ignore for now", "blocked": null, "hint": null }
],
"candidates": [
{ "instanceId": 499, "instanceKey": "2a7e4c19-0b3d-4e58-9f61-c4d8a2b7e350", "deviceTag": "DISP-001", "make": "Samsung", "model": "QM85C", "room": "Conference Room A", "score": 2 }
],
"lineCandidates": [
{ "productProjectRoomId": 88, "make": "Samsung", "model": "QM85C", "room": "Conference Room A", "quantity": 3 }
],
"productCandidates": [
{ "productId": 41, "make": "Samsung", "model": "QM85C", "part": "LH85QMCEBGCXZA" }
]
},
{
"kind": "missing", "handle": "0B77",
"instanceId": 500, "instanceKey": "9b0d3f6a-1c2e-4a7b-8d5f-e6a1c3b9d724", "deviceTag": "DISP-002",
"field": null, "drawingValue": null, "projectValue": null,
"options": [
{ "id": "unplace", "label": "Unplace the unit", "blocked": null, "hint": null },
{ "id": "keep", "label": "Keep the placement", "blocked": null, "hint": null }
],
"candidates": [], "lineCandidates": [], "productCandidates": []
}
],
"summary": { "place": 0, "geometry": 1, "pull": 1, "restamp": 0, "refresh": 1, "conflict": 1, "clone": 1, "link": 1, "missing": 1 }
},
"applied": {
"applied": { "placed": 0, "geometry": 1, "pulled": 1, "newUnits": 0, "newLines": 0, "unplaced": 0, "graphicOnly": 0 },
"skipped": [],
"restamp": [],
"refresh": [
{ "handle": "2F3A", "attributes": { "DEVICETAG": "DISP-003" } }
],
"planWasStale": false
}
},
"message": null,
"timestamp": "2026-10-12T15:04:05Z"
}
On a contracted version, newUnit and newLine are listed blocked and cannot be chosen: { "id": "newLine", "label": "Add a new line", "blocked": "LOCKED_VERSION", "hint": "This project version is locked. Create a Change Order to modify it." }.
Plan Fields
| Field | Type | Description |
plan | object | The plan, with the fields below |
drawingId | integer | The drawing |
planVersion | string | A SHA-256 of the drawing's placements and the units' fingerprints. Send it back with the decisions; a mismatch only marks the apply result planWasStale. |
generatedAt | string (ISO 8601) | When the plan was made |
safe | array of object | Safe actions, in posted block order |
proposals | array of object | Proposals, in posted block order, then the missing placements |
summary | object | How many entries of each kind: place, geometry, pull, restamp, refresh, conflict, clone, link and missing |
applied | object or null | What applying the safe actions did, in the shape of the apply result; null unless autoApplySafe was true |
Every safe action and every proposal carries every field, null or an empty list where its kind has none, so a client reads one shape per array. Each names the block by handle and, when the block is matched, its unit by instanceId, instanceKey and deviceTag.
Safe Actions
| Kind | When | What applying it does | Its fields |
place | A matched block whose unit has no placement on this drawing | Records the placement | blockName, cadBlockId, geometry |
geometry | A placement whose block moved, turned, was rescaled, changed layer or visibility state, or now has another handle | Updates the placement | blockName, cadBlockId, geometry |
pull | A field the drawing owns changed in the drawing | Writes the drawing's value to the unit | tag, value (empty when the drawing cleared it) |
restamp | A block matched by handle or device tag whose key is missing or wrong | Nothing in AVstackr; the result's restamp tells the plugin to write the key | instanceKey, the key to write |
refresh | Fields the project owns that differ in the drawing | Nothing in AVstackr; the result's refresh tells the plugin to rewrite them | attributes: tag to the project's value, an empty value clearing the attribute |
Proposals
| Kind | When | Options |
conflict | A field that syncs both changed in the drawing and in AVstackr since the last sync; one proposal per field, with field, drawingValue and projectValue | keepDrawing, keepProject |
clone | A second block carrying a key another block keeps: a copied block. The unit fields name the kept unit, and lineCandidates its line. | newUnit (when that line is in the units contract), graphicOnly, ignore |
link | A block that matches no unit. candidates are units with no placement on this drawing, at most ten, best first: score 3 when the block's DEVICETAG is the unit's tag, 2 when make, model and room (LOCATION) match, 1 when make and model do. lineCandidates are lines a unit can be added to, and productCandidates catalog products found by the block's MANUFACTURER and MODEL, or its PARTNUMBER. | linkTo (when there is a candidate), newUnit (when there is a line), newLine, graphicOnly, ignore |
missing | A recorded placement whose block is no longer in the drawing; handle is the placement's | unplace, keep |
Entry Fields
| Field | Type | Description |
kind | string | The safe action's or the proposal's kind, as in the tables above |
handle | string | The block's handle; for missing, the recorded placement's |
instanceId, instanceKey, deviceTag | integer, string, string | The matched unit; null for link |
blockName, cadBlockId, geometry | string, integer, object | A safe action's block to record; geometry holds x, y, z, rotation, scaleX, scaleY, scaleZ, layer and visibilityState |
tag, value | string | A pull's CAD tag and the drawing's value |
attributes | object | A refresh's values to write, by upper-case tag |
field, drawingValue, projectValue | string | A conflict's CAD tag and the two values |
options | array of object | The answers, in display order: id (the option to send back), label, and blocked and hint, which name the reason and the way out when it cannot be chosen, else null |
candidates | array of object | Units to link to: instanceId, instanceKey, deviceTag, make, model, room, score |
lineCandidates | array of object | Lines a unit can be added to: productProjectRoomId, make, model, room, quantity |
productCandidates | array of object | Catalog products for a new line: productId, make, model, part |
Error Responses
| Status | Code | Description |
| 400 | INVALID_INPUT | The body or blocks is missing, a geometry number is missing or not finite, a handle is not 1 to 16 hex digits or is posted twice, a block name is not 1 to 255 characters, or the path is over 500 |
| 400 | UNKNOWN_FIELDS | The body or a block names a field not listed above |
| 400 | TOO_MANY_BLOCKS | More than 10,000 blocks |
| 403 | READ_ONLY_ACCESS | The caller has read-only access |
| 404 | DRAWING_NOT_FOUND | The project has no drawing with the ID, or the project is not one of yours |
POST /api/v1/projects/{projectId}/cad/drawings/{id}/apply
Applies the person's answers, one decision per proposal answered. AVstackr reads the drawing's placements and the units again, plans again from what it has stored plus the blocks the decisions carry, and checks that each decision still holds: the placement still missing, the block still a copy or unmatched, the unit or line still there and not yet placed on this drawing, the option not blocked. A decision that no longer holds is skipped with a reason and the rest go ahead; nothing is rolled back for a skip.
- The writes go in this order: unplacing; moved blocks; new placements, then
linkTo, newUnit, newLine and graphicOnly in request order; pulled values, then keepDrawing and keepProject.
- A
planVersion that no longer matches does not stop the apply; the result says planWasStale: true so the plugin can submit again and show a fresh plan.
- With
applySafe: true, the safe actions of the blocks the decisions carry are applied as well, for each block whose every decision holds.
- An apply never deletes a unit or a line, lowers a quantity, changes a make, model, part or device tag, or moves a block.
Path Parameters
| Field | Type | Required | Description |
projectId | integer | Yes | The project ID |
id | integer | Yes | The drawing ID |
Request Body
| Field | Type | Required | Description |
planVersion | string | No | The planVersion the decisions were made on |
decisions | array of object | No | One per proposal answered, at most 10,000, with the fields below |
applySafe | boolean | No | Default false. true applies the carried blocks' safe actions too. |
Decision Fields
| Field | Type | Description |
handle | string | The proposal's handle |
option | string | One of the proposal's option ids |
field | string | For keepDrawing and keepProject: the conflict's field |
instanceId | integer | For linkTo: the unit, one of the proposal's candidates |
lineId | integer | For newUnit: one of the proposal's lineCandidates. A copy uses its kept unit's line when this is left out. |
productId, roomId | integer | For newLine: the catalog product and a room of the active version |
block | object | The block as submitted, with the submit's block fields. Required for linkTo, newUnit, newLine and keepDrawing. |
projectValue | string | For keepDrawing and keepProject: the conflict's projectValue, sent back. The decision is skipped when the project's value has changed since. Without it, a keepDrawing is skipped whenever the plan is stale. |
Options
| Option | Label | What applying it does |
keepDrawing | Keep the drawing's value | Writes the drawing's value to the unit |
keepProject | Keep the project's value | Nothing in AVstackr; the result's refresh carries the project's value for the block |
newUnit | Add a unit to the line | Raises the line's quantity by one, creates the new unit, which gets its device tag as any new unit does, places it at the block and restamps the block with its key |
linkTo | Link to an existing unit | Places the unit at the block and restamps the block with its key |
newLine | Add a new line | Adds a line of one for the product in the room, in the system the block's SYSTEM names when it names one of yours, places its unit at the block and restamps the block |
graphicOnly | Keep as a graphic only | Nothing in AVstackr; the result's restamp has instanceKey: null, and the plugin strips the block's key |
ignore | Ignore for now | Nothing; the block comes back on the next submit |
unplace | Unplace the unit | Deletes the placement; the unit stays |
keep | Keep the placement | Nothing; the block may be on a frozen layer or cut for now |
Example Request
The answers to the four proposals of the submit example above.
POST /api/v1/projects/12/cad/drawings/4/apply
Authorization: Bearer avs_YOUR_API_KEY
Content-Type: application/json
{
"planVersion": "f1080ca40d37e0adc56b2d6b37508f3ae6ac1597e706414fd12a5b18d0cfc0e9",
"applySafe": false,
"decisions": [
{
"handle": "2F3A",
"option": "keepDrawing",
"field": "SERIAL_NUMBER",
"projectValue": "SN-4471-A",
"block": {
"handle": "2F3A",
"blockName": "AVS_DISP_WALL",
"cadBlockId": 9,
"instanceKey": "6f1c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f",
"attributes": { "DEVICETAG": "DISP-3", "MANUFACTURER": "Samsung", "MODEL": "QM85C", "SERIAL_NUMBER": "SN-4471-B", "FIRMWARE": "2.2" },
"x": 132.0, "y": 44.0, "z": 0, "rotation": 0, "scaleX": 1, "scaleY": 1, "scaleZ": 1,
"layer": "AV-DISPLAY",
"visibilityState": "TOP"
}
},
{ "handle": "9C01", "option": "graphicOnly" },
{
"handle": "9C02",
"option": "linkTo",
"instanceId": 499,
"block": {
"handle": "9C02",
"blockName": "DISP_GENERIC",
"attributes": { "MANUFACTURER": "Samsung", "MODEL": "QM85C", "LOCATION": "Conference Room A", "SERIAL_NUMBER": "" },
"x": 60.0, "y": 12.5, "z": 0, "rotation": 3.141592653589793, "scaleX": 1, "scaleY": 1, "scaleZ": 1,
"layer": "AV-DISPLAY",
"requested": "link"
}
},
{ "handle": "0B77", "option": "unplace" }
]
}
Response (200 - Success)
Between the plan and the apply, someone changed DISP-003's serial number in AVstackr again, to SN-4471-C. The echoed projectValue no longer matches, so keepDrawing is skipped, and the plan is stale for the same reason. The other three answers hold.
{
"success": true,
"data": {
"applied": { "placed": 1, "geometry": 0, "pulled": 0, "newUnits": 0, "newLines": 0, "unplaced": 1, "graphicOnly": 1 },
"skipped": [
{ "handle": "2F3A", "option": "keepDrawing", "reason": "The project value changed since the plan; resubmit the drawing and decide again." }
],
"restamp": [
{ "handle": "9C01", "instanceKey": null },
{ "handle": "9C02", "instanceKey": "2a7e4c19-0b3d-4e58-9f61-c4d8a2b7e350" }
],
"refresh": [],
"planWasStale": true
},
"message": null,
"timestamp": "2026-10-12T15:07:30Z"
}
Result Fields
| Field | Type | Description |
applied | object | What was written: placed (placements recorded, by place and linkTo), geometry (placements updated), pulled (values written to units), newUnits, newLines, unplaced and graphicOnly |
placed, geometry, pulled, newUnits, newLines, unplaced, graphicOnly | integer | The counts in applied |
skipped | array of object | The decisions, and safe actions, that no longer held: handle, option (a skipped safe action reads place, geometry or pull) and reason, a code such as LOCKED_VERSION, CONCURRENT_MODIFICATION or INVALID_INPUT, or a sentence |
option, reason | string | A skipped entry's option and why it was skipped |
restamp | array of object | Blocks whose key the plugin writes into the XDATA and AVSTACKR_ID: handle and instanceKey, which is null to strip the key |
refresh | array of object | Blocks whose attributes the plugin rewrites: handle and attributes, tag to value |
planWasStale | boolean | true when the request's planVersion no longer matched |
Error Responses
| Status | Code | Description |
| 400 | INVALID_INPUT | The body is missing, or a decision's block lacks a geometry number. A decision missing what its option needs is not an error: it is skipped with INVALID_INPUT. |
| 400 | UNKNOWN_FIELDS | The body, a decision or a decision's block names a field not listed above |
| 400 | TOO_MANY_BLOCKS | More than 10,000 decisions |
| 403 | READ_ONLY_ACCESS | The caller has read-only access |
| 404 | DRAWING_NOT_FOUND | The project has no drawing with the ID, or the project is not one of yours |
Sync Error Codes
The drawing sync's own codes, with the status each answers with. CONCURRENT_MODIFICATION and LOCKED_VERSION reach a client as skip reasons inside an apply result, which is a 200.
| Code | Status | Meaning |
INVALID_INPUT | 400 | A name, view, handle, block, geometry or attribute breaks its rule; the message names it. As a skip reason, a decision lacks what its option needs. |
TOO_MANY_BLOCKS | 400 | A submit carries more than 10,000 blocks, or an apply more than 10,000 decisions |
DRAWING_NOT_FOUND | 404 | The project has no drawing with the ID |
PLACEMENT_NOT_FOUND | 404 | The drawing has no placement with the ID |
FIELD_PROJECT_OWNED | 409 | A live edit writes an attribute the project owns; error.details.projectValue holds the project's value |
CONCURRENT_MODIFICATION | 409 | A skip reason: another writer placed the unit, took the handle or removed the placement first |
LOCKED_VERSION | 409 | A skip reason: the project's version is contracted, so a unit or line cannot be added; use a change order |
MCP clients list drawings with list_cad_drawings, list placements with list_cad_placements and unplace a unit with unplace_cad_unit; see the MCP Server page. Submit and apply are not offered through MCP: an agent has no drawing.