Documentation

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

  1. 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.
  2. 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.
  3. 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.
  4. 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:

  1. By key: instanceKey names a unit of the project.
  2. 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.
  3. 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.

FieldOwnerOn submit
AVSTACKR_ID, MANUFACTURER, MODEL, PARTNUMBER, DESCRIPTION, CATEGORY, LOCATION, SYSTEM, UNIT, QTY, RACKUNITS, HEIGHT, WIDTH, DEPTH, WEIGHT, PROJECTProjectA different drawing value is a refresh. Never pulled.
DEVICETAGProjectrefresh. 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, visibilityStateDrawingRecorded on the placement (geometry); never pushed, and AVstackr never moves a block.
Attribute key, pushProjectrefresh when the drawing differs.
Attribute key, pullDrawingpull 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, bothSharedOnly 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 tagNeitherNot 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

FieldTypeRequiredDescription
projectIdintegerYesThe 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

FieldTypeDescription
idintegerThe drawing's ID, the one the plugin keeps in the DWG
projectIdintegerThe project the drawing belongs to
namestring1 to 200 characters
viewTypestringplan, ceiling, elevation or schematic: the view the drawing's blocks are resolved for
fingerprintGuidstring (UUID) or nullAutoCAD's FINGERPRINTGUID, a re-link hint
filePathHintstring or nullWhere the plugin last read the DWG; display only
placedUnitsintegerHow many units the drawing places
lastSubmitAtstring (ISO 8601) or nullWhen the plugin last submitted the drawing
lastSubmitVersionGuidstring (UUID) or nullAutoCAD's VERSIONGUID sent with that submit; it changes on every save
lastApplyAtstring (ISO 8601) or nullWhen decisions or safe actions were last applied
createdAtstring (ISO 8601)When the drawing was linked
createdBystring or nullThe user ID of the person who linked it
updatedAtstring (ISO 8601)When the drawing last changed

Error Responses

StatusCodeDescription
404PROJECT_NOT_FOUNDThe 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

FieldTypeRequiredDescription
projectIdintegerYesThe project ID

Request Body

FieldTypeRequiredDescription
namestringYesTrimmed, 1 to 200 characters. The plugin sends the file name without its extension.
viewTypestringYesplan, ceiling, elevation or schematic, in any case
fingerprintGuidstring (UUID)NoAutoCAD's FINGERPRINTGUID
filePathHintstringNoThe 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

StatusCodeDescription
400INVALID_INPUTThe body is missing, viewType is missing or not a view, the name is not 1 to 200 characters, or the path is over 500
400UNKNOWN_FIELDSThe body names a field not listed above
403READ_ONLY_ACCESSThe caller has read-only access
404PROJECT_NOT_FOUNDThe project is not one of yours

GET /api/v1/projects/{projectId}/cad/drawings/{id}

Returns one linked drawing with the fields above.

Path Parameters

FieldTypeRequiredDescription
projectIdintegerYesThe project ID
idintegerYesThe 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

StatusCodeDescription
404PROJECT_NOT_FOUNDThe project is not one of yours
404DRAWING_NOT_FOUNDThe project has no drawing with the ID

PUT /api/v1/projects/{projectId}/cad/drawings/{id}

Renames a drawing.

Path Parameters

FieldTypeRequiredDescription
projectIdintegerYesThe project ID
idintegerYesThe drawing ID

Request Body

FieldTypeRequiredDescription
namestringYesTrimmed, 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

StatusCodeDescription
400INVALID_INPUTThe body is missing, or the name is not 1 to 200 characters
400UNKNOWN_FIELDSThe body names a field other than name
403READ_ONLY_ACCESSThe caller has read-only access
404PROJECT_NOT_FOUNDThe project is not one of yours
404DRAWING_NOT_FOUNDThe 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

FieldTypeRequiredDescription
projectIdintegerYesThe project ID
idintegerYesThe 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

StatusCodeDescription
403READ_ONLY_ACCESSThe caller has read-only access
404PROJECT_NOT_FOUNDThe project is not one of yours
404DRAWING_NOT_FOUNDThe 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

FieldTypeRequiredDescription
projectIdintegerYesThe project ID
idintegerYesThe 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

FieldTypeDescription
placementIdintegerThe placement's ID
drawingId, drawingNameinteger, stringThe drawing
viewstringThe drawing's view
instanceId, instanceKeyinteger, string (UUID)The unit and its permanent key, the one the block carries
deviceTagstring or nullThe unit's device tag; null while it has none
unitNumberintegerThe unit's number among the room's units of the same make and model
make, modelstringThe unit's line in the active version; empty when the line is no longer there
roomstringThe unit's room in the active version; empty when the room is no longer there
handlestringThe block's AutoCAD handle in this drawing
blockNamestringThe block definition name
cadBlockIdinteger or nullThe library block, when the plugin named one of yours; null for a hand-drawn block
layer, visibilityStatestring, string or nullThe block's layer and visibility state as last recorded
lastSeenAtstring (ISO 8601)The last submit that included the block
lastPushedFingerprintstringThe unit's fingerprint when the placement was recorded

Error Responses

StatusCodeDescription
404PROJECT_NOT_FOUNDThe project is not one of yours
404DRAWING_NOT_FOUNDThe 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

FieldTypeRequiredDescription
projectIdintegerYesThe project ID
idintegerYesThe drawing ID
placementIdintegerYesThe placement ID

Request Body

FieldTypeRequiredDescription
geometryobjectOne of the twoThe block's whole geometry, with the fields below
attributeobjectOne of the twoOne attribute the drawing changed, with the fields below

Geometry Fields

FieldTypeRequiredDescription
x, y, znumberYesThe insertion point, in drawing units
rotationnumberYesRadians, as AutoCAD reports it
scaleX, scaleY, scaleZnumberYesThe scale factors
layerstringNoAt most 255 characters
visibilityStatestringNoA dynamic block's visibility state, at most 255 characters

Attribute Fields

FieldTypeRequiredDescription
tagstringYesThe CAD tag, in any case
valuestringNoThe 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

StatusCodeDescription
400INVALID_INPUTThe 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
400UNKNOWN_FIELDSThe body, geometry or attribute names a field not listed above
403READ_ONLY_ACCESSThe caller has read-only access
404DRAWING_NOT_FOUNDThe project has no drawing with the ID
404PLACEMENT_NOT_FOUNDThe drawing has no placement with the ID
409FIELD_PROJECT_OWNEDThe 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

FieldTypeRequiredDescription
projectIdintegerYesThe project ID
idintegerYesThe drawing ID
placementIdintegerYesThe 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

StatusCodeDescription
403READ_ONLY_ACCESSThe caller has read-only access
404PROJECT_NOT_FOUNDThe project is not one of yours
404DRAWING_NOT_FOUNDThe project has no drawing with the ID
404PLACEMENT_NOT_FOUNDThe 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

FieldTypeRequiredDescription
projectIdintegerYesThe project ID
idintegerYesThe drawing ID

Request Body

FieldTypeRequiredDescription
versionGuidstring (UUID)NoAutoCAD's VERSIONGUID of the saved drawing
savedAtstring (ISO 8601)NoWhen the drawing was last saved, UTC
filePathHintstringNoThe path the plugin read the DWG from, at most 500 characters
autoApplySafebooleanNoDefault false. true applies the plan's safe actions in the same call.
blocksarray of objectYesEvery AVstackr block in the drawing, with the fields below

Block Fields

FieldTypeRequiredDescription
handlestringYesThe INSERT's handle, 1 to 16 hex digits; the block's key in the plan and in decisions
blockNamestringYesThe block definition name, 1 to 255 characters
cadBlockIdintegerNoThe library block the plugin inserted; leave it out for a hand-drawn block
instanceKeystring (UUID)NoThe unit key read from the block's XDATA or AVSTACKR_ID; null when it has neither
attributesobjectNoEvery ATTRIB by tag, an empty one as ""
x, y, znumberYesThe insertion point, in drawing units
rotationnumberYesRadians
scaleX, scaleY, scaleZnumberYesThe scale factors
layerstringNoThe block's layer
visibilityStatestringNoA dynamic block's visibility state
requestedstringNoThe 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

FieldTypeDescription
planobjectThe plan, with the fields below
drawingIdintegerThe drawing
planVersionstringA 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.
generatedAtstring (ISO 8601)When the plan was made
safearray of objectSafe actions, in posted block order
proposalsarray of objectProposals, in posted block order, then the missing placements
summaryobjectHow many entries of each kind: place, geometry, pull, restamp, refresh, conflict, clone, link and missing
appliedobject or nullWhat 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

KindWhenWhat applying it doesIts fields
placeA matched block whose unit has no placement on this drawingRecords the placementblockName, cadBlockId, geometry
geometryA placement whose block moved, turned, was rescaled, changed layer or visibility state, or now has another handleUpdates the placementblockName, cadBlockId, geometry
pullA field the drawing owns changed in the drawingWrites the drawing's value to the unittag, value (empty when the drawing cleared it)
restampA block matched by handle or device tag whose key is missing or wrongNothing in AVstackr; the result's restamp tells the plugin to write the keyinstanceKey, the key to write
refreshFields the project owns that differ in the drawingNothing in AVstackr; the result's refresh tells the plugin to rewrite themattributes: tag to the project's value, an empty value clearing the attribute

Proposals

KindWhenOptions
conflictA field that syncs both changed in the drawing and in AVstackr since the last sync; one proposal per field, with field, drawingValue and projectValuekeepDrawing, keepProject
cloneA 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
linkA 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
missingA recorded placement whose block is no longer in the drawing; handle is the placement'sunplace, keep

Entry Fields

FieldTypeDescription
kindstringThe safe action's or the proposal's kind, as in the tables above
handlestringThe block's handle; for missing, the recorded placement's
instanceId, instanceKey, deviceTaginteger, string, stringThe matched unit; null for link
blockName, cadBlockId, geometrystring, integer, objectA safe action's block to record; geometry holds x, y, z, rotation, scaleX, scaleY, scaleZ, layer and visibilityState
tag, valuestringA pull's CAD tag and the drawing's value
attributesobjectA refresh's values to write, by upper-case tag
field, drawingValue, projectValuestringA conflict's CAD tag and the two values
optionsarray of objectThe 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
candidatesarray of objectUnits to link to: instanceId, instanceKey, deviceTag, make, model, room, score
lineCandidatesarray of objectLines a unit can be added to: productProjectRoomId, make, model, room, quantity
productCandidatesarray of objectCatalog products for a new line: productId, make, model, part

Error Responses

StatusCodeDescription
400INVALID_INPUTThe 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
400UNKNOWN_FIELDSThe body or a block names a field not listed above
400TOO_MANY_BLOCKSMore than 10,000 blocks
403READ_ONLY_ACCESSThe caller has read-only access
404DRAWING_NOT_FOUNDThe 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

FieldTypeRequiredDescription
projectIdintegerYesThe project ID
idintegerYesThe drawing ID

Request Body

FieldTypeRequiredDescription
planVersionstringNoThe planVersion the decisions were made on
decisionsarray of objectNoOne per proposal answered, at most 10,000, with the fields below
applySafebooleanNoDefault false. true applies the carried blocks' safe actions too.

Decision Fields

FieldTypeDescription
handlestringThe proposal's handle
optionstringOne of the proposal's option ids
fieldstringFor keepDrawing and keepProject: the conflict's field
instanceIdintegerFor linkTo: the unit, one of the proposal's candidates
lineIdintegerFor newUnit: one of the proposal's lineCandidates. A copy uses its kept unit's line when this is left out.
productId, roomIdintegerFor newLine: the catalog product and a room of the active version
blockobjectThe block as submitted, with the submit's block fields. Required for linkTo, newUnit, newLine and keepDrawing.
projectValuestringFor 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

OptionLabelWhat applying it does
keepDrawingKeep the drawing's valueWrites the drawing's value to the unit
keepProjectKeep the project's valueNothing in AVstackr; the result's refresh carries the project's value for the block
newUnitAdd a unit to the lineRaises 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
linkToLink to an existing unitPlaces the unit at the block and restamps the block with its key
newLineAdd a new lineAdds 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
graphicOnlyKeep as a graphic onlyNothing in AVstackr; the result's restamp has instanceKey: null, and the plugin strips the block's key
ignoreIgnore for nowNothing; the block comes back on the next submit
unplaceUnplace the unitDeletes the placement; the unit stays
keepKeep the placementNothing; 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

FieldTypeDescription
appliedobjectWhat 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, graphicOnlyintegerThe counts in applied
skippedarray of objectThe 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, reasonstringA skipped entry's option and why it was skipped
restamparray of objectBlocks whose key the plugin writes into the XDATA and AVSTACKR_ID: handle and instanceKey, which is null to strip the key
refresharray of objectBlocks whose attributes the plugin rewrites: handle and attributes, tag to value
planWasStalebooleantrue when the request's planVersion no longer matched

Error Responses

StatusCodeDescription
400INVALID_INPUTThe 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.
400UNKNOWN_FIELDSThe body, a decision or a decision's block names a field not listed above
400TOO_MANY_BLOCKSMore than 10,000 decisions
403READ_ONLY_ACCESSThe caller has read-only access
404DRAWING_NOT_FOUNDThe 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.

CodeStatusMeaning
INVALID_INPUT400A 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_BLOCKS400A submit carries more than 10,000 blocks, or an apply more than 10,000 decisions
DRAWING_NOT_FOUND404The project has no drawing with the ID
PLACEMENT_NOT_FOUND404The drawing has no placement with the ID
FIELD_PROJECT_OWNED409A live edit writes an attribute the project owns; error.details.projectValue holds the project's value
CONCURRENT_MODIFICATION409A skip reason: another writer placed the unit, took the handle or removed the placement first
LOCKED_VERSION409A 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.