Documentation

Products API

Create, read, update, and delete products in your organization's catalog.

GET /api/v1/products

Returns a paginated list of products from your organization's catalog. Use query parameters to filter and search.

Query Parameters

Parameter Type Default Description
page integer 1 Page number
pageSize integer 20 Items per page (max 100)
search string Search by Part# or Model
category string Filter by category
productType string Filter by product type
activeOnly boolean true Only return active products

Example Request

GET /api/v1/products?page=1&pageSize=10&search=microphone
Authorization: Bearer YOUR_JWT_TOKEN

Response (200 - Success)

{
    "success": true,
    "data": {
        "products": [
            {
                "id": 123,
                "make": "Shure",
                "model": "MXA910",
                "description": "Ceiling array microphone",
                "price": 2999.00,
                "productType": "Microphone",
                "part": "MXA910-60CM",
                "category": "Audio",
                "isActive": true
            }
        ],
        "pagination": {
            "currentPage": 1,
            "pageSize": 10,
            "totalCount": 150,
            "totalPages": 15,
            "hasPrevious": false,
            "hasNext": true
        }
    },
    "timestamp": "2024-12-25T12:00:00Z"
}

GET /api/v1/products/{id}

Returns a single product by its unique identifier.

Response (200 - Success)

{
    "success": true,
    "data": {
        "product": {
            "id": 123,
            "make": "Shure",
            "model": "MXA910",
            "description": "Ceiling array microphone",
            "longDescription": "Professional ceiling array microphone with 60cm pickup pattern...",
            "enrichedDescription": "AI-generated detailed description...",
            "price": 2999.00,
            "vendor": "TD SYNNEX",
            "imageUrl": "https://example.com/mxa910.jpg",
            "productType": "Microphone",
            "part": "MXA910-60CM",
            "category": "Audio",
            "roomType": "Conference Room",
            "tags": "microphone,ceiling,array,beamforming",
            "specifications": "60cm pickup pattern, 8 lobes, PoE+",
            "isActive": true,
            "dateUpdated": "2024-12-20T10:30:00Z",
            "hasEmbedding": true
        }
    },
    "timestamp": "2024-12-25T12:00:00Z"
}

Error Responses

Status Code Description
404 PRODUCT_NOT_FOUND Product with specified ID does not exist

Variant Group Fields

A variant group links catalog products that are the same device in different finishes. Each product in the responses of GET /api/v1/products and GET /api/v1/products/{id} carries four fields that describe its group. All four are null when the product is in no group.

Field Type Description
variantGroupId integer or null The ID of the product's variant group
variantGroupName string or null The name of the group, for example C-IC6LP
variantOption string or null The group's option name, which says what differs between its members, for example Colour
variantLabel string or null This product's value for that option, for example White

A product that is in a group, with its other fields left out:

{
    "id": 412,
    "make": "Biamp",
    "model": "C-IC6LP-W",
    "variantGroupId": 12,
    "variantGroupName": "C-IC6LP",
    "variantOption": "Colour",
    "variantLabel": "White"
}

The four fields are read-only on products. To change a product's group or label, use the Variant Groups API.

  • PUT /api/v1/products/{id} refuses a body that names any of the four fields. It returns 400 UNKNOWN_FIELDS and changes nothing.
  • POST /api/v1/products does not read the four fields. A new product is always created in no group.
  • The make of a product that is in a group cannot be changed. PUT /api/v1/products/{id} with a different make returns 400 VARIANT_GROUP_RULE with the problem Ungroup this product before changing its make. Remove the product from its group first. A change of letter case or of surrounding spaces is the same make, so it is accepted.

Device Category Fields

A product's device category is the category its device tags are named from, such as Display / DISP. It is separate from the free-text category and productType. Each product in the responses of GET /api/v1/products and GET /api/v1/products/{id} carries the fields below. cadCategoryId and cadCategory are null when the product is unclassified, and cadBlocks is empty when the product has no CAD block of its own.

Field Type Description
cadCategoryId integer or null The ID of the product's device category, from GET /api/v1/cad-categories
cadCategory object or null The category's id, name and abbreviation
cadBlocks array The product's own CAD block for each view it sets, in view order: view, id (the block's ID), name, blockName and visibilityState (null for the block's default state). A view it does not list uses the category's default block. A block archived since it was chosen is still listed but no longer used. Set with PUT /api/v1/products/{id}/cad-blocks below.

A classified product with a block for the plan view, its other fields left out:

{
    "id": 123,
    "make": "Samsung",
    "model": "QM65C",
    "cadCategoryId": 7,
    "cadCategory": { "id": 7, "name": "Display", "abbreviation": "DISP" },
    "cadBlocks": [
        { "view": "plan", "id": 9, "name": "Display wall", "blockName": "AVS_DISP_WALL", "visibilityState": "TOP" }
    ]
}

POST /api/v1/products and PUT /api/v1/products/{id} take cadCategoryId. On PUT, a null or missing value leaves the category unchanged, so clear it with PUT /api/v1/products/{id}/category below. A cadCategoryId that is not one of your categories returns 404 NOT_FOUND and nothing is written. The category list itself is in the Device Tags API.


POST /api/v1/products

Creates a new product in your organization's catalog.

Request Body

Field Type Required Description
make string Yes Manufacturer name (max 100 chars)
model string Yes Model number (max 100 chars)
description string No Short description (max 255 chars)
longDescription string No Extended description (max 2000 chars)
price decimal No MSRP price (0-9,999,999)
vendor string No Distributor or vendor name
part string No Part number / SKU
productType string No Equipment type (e.g., Microphone, Display)
category string No Product category (e.g., Audio, Video)
roomType string No Suggested room type for this product
imageUrl string No URL to product image
tags string No Comma-separated tags for search
specifications string No Technical specifications text
cadCategoryId integer No The device category to classify the product with (see Device Category Fields above). Leave it out to create the product unclassified.
isActive boolean No Whether product is active (default: true)

Response (201 - Created)

{
    "success": true,
    "productId": 124,
    "message": "Product created successfully",
    "timestamp": "2024-12-25T12:00:00Z"
}

PUT /api/v1/products/{id}

Updates an existing product. All fields from the POST request body are accepted.


DELETE /api/v1/products/{id}

Permanently removes a product from your catalog.

Response (200 - Success)

{
    "success": true,
    "productId": 123,
    "message": "Product deleted successfully"
}

PUT /api/v1/products/{id}/metadata

Replace a product's connection metadata — input/output ports, device type, category, and power requirements. Used to configure products for schematic wiring.

Request Body

Send a full ConnectionMetadata JSON object. Port types are validated against the port type registry — use GET /api/v1/products/port-types to see valid options; an invalid one fails the whole request (see Error Responses below).

A port states its PoE with two fields, and it carries at most one of them:

  • poeSource — the tier this jack supplies to whatever plugs into it: "PoE", "PoE+", "PoE++", or "none" to state that it supplies nothing. Omit it, or send null, when the jack was never described.
  • poeDraw — the tier this jack draws from the far end. This jack is the one being powered.

Neither field fails the request. Both are checked against the port type registry and saved as null when the connector cannot carry PoE — only rj45 and ethercon can. A port that arrives with both set keeps poeDraw and has its poeSource cleared: a jack being powered is what decides which way the watts run.

The device-level power is derived from the jacks on save. When any port draws, power becomes the strongest tier drawn, whatever you sent. When no port draws, the power you sent is kept as-is — and a PoE tier sent with no drawing jack behind it raises a notice on the wiring report, because nothing says which jack is fed. In the example below, the AVB/Ctrl jack is what backs the "power": "PoE+".

{
  "category": "Processing",
  "deviceType": "Digital Signal Processor",
  "power": "PoE+",
  "inputPorts": [
    { "portType": "rj45", "innerLabel": "AVB/Ctrl", "poeDraw": "PoE+" },
    { "portType": "dante_rj45", "innerLabel": "DANTE IN 1" },
    { "portType": "xlr_3pin_female", "innerLabel": "MIC 1" }
  ],
  "outputPorts": [
    { "portType": "dante_rj45", "innerLabel": "DANTE OUT 1" },
    { "portType": "speakon_nl4", "innerLabel": "AMP OUT" }
  ]
}

Response (200 — Success)

{
  "success": true,
  "data": {
    "success": true,
    "productId": 157,
    "message": "Product metadata updated"
  }
}

Error Responses

StatusCodeDescription
400INVALID_PORT_TYPESOne or more port types are not valid
404PRODUCT_NOT_FOUNDProduct ID not found for your tenant

POST /api/v1/products/{id}/metadata/apply-to-variants

Copies this product's saved connection metadata onto other members of its variant group. The copy replaces the metadata of each target. Each copy keeps the target's own make and model, and the target's own description when it has one.

The product in the URL is the source. It must be in a variant group, and it must have connection metadata. The endpoint copies the metadata that is saved, so save the source first with PUT /api/v1/products/{id}/metadata. Devices already placed on drawings keep their own copy of the metadata.

Path Parameters

Field Type Required Description
id integer Yes The catalog product ID of the source product, whose metadata is copied

Request Body

Field Type Required Description
targetProductIds array of integer No The product IDs to copy onto. Each one must be another member of the source's variant group. An ID that is not one is skipped, not refused. When the list is empty or left out, nothing is copied and the response is still 200.
includeBlockSkin boolean No Also copies the source's block skin, which is the custom device block pinned to the product. The default is true. When the source has no block skin, each target keeps its own.

Example Request

{
    "targetProductIds": [413, 77],
    "includeBlockSkin": true
}

Response (200 - Success)

{
    "success": true,
    "data": {
        "sourceProductId": 412,
        "updatedProductIds": [413],
        "skippedProductIds": [77],
        "skinNotCopied": false
    },
    "message": "Metadata applied to 1 variants; 1 skipped",
    "timestamp": "2026-10-08T15:30:00Z"
}
Field Type Description
sourceProductId integer The product the metadata was copied from
updatedProductIds array of integer The targets that received the metadata
skippedProductIds array of integer The requested targets that were left untouched, because they are not another member of the source's variant group. The source's own ID is skipped too.
skinNotCopied boolean true when includeBlockSkin was true and the source has no block skin, so the targets kept their own. false when the skin was copied or was not requested.

Error Responses

StatusCodeDescription
400VARIANT_GROUP_RULEThe source product has no connection metadata. Nothing is copied. The body is described under Errors in the Variant Groups API.
403READ_ONLY_ACCESSThe caller has read-only access
404PRODUCT_NOT_FOUNDThe source product does not exist, or it is in no variant group

PUT /api/v1/products/{id}/category

Sets or clears one product's device category. This is the way to clear a category: PUT /api/v1/products/{id} reads a null cadCategoryId as "unchanged". Existing device tags of the product's units never change; units tagged afterwards use the new category.

Path Parameters

Field Type Required Description
id integer Yes The catalog product ID

Request Body

Field Type Required Description
cadCategoryId integer or null No The category to set. null, or leaving the field out, clears the category. Any other field name is refused with 400 UNKNOWN_FIELDS, so a misspelling cannot clear a category by accident.

Example Request

{
    "cadCategoryId": 7
}

Response (200 - Success)

{
    "success": true,
    "data": {
        "productId": 123,
        "cadCategoryId": 7,
        "cadCategory": { "id": 7, "name": "Display", "abbreviation": "DISP" }
    },
    "message": "Category set to Display",
    "timestamp": "2026-10-11T15:30:00Z"
}

When the category is cleared, cadCategoryId and cadCategory are null and the message is Category cleared.

Error Responses

StatusCodeDescription
400UNKNOWN_FIELDSThe body names a field other than cadCategoryId
403READ_ONLY_ACCESSThe caller has read-only access
404NOT_FOUNDThe product or the category does not exist. The message says which, for example Category 99 not found.

POST /api/v1/products/set-category

Sets one device category on many products in one save, or clears it from all of them. IDs that are not your products are skipped.

Request Body

Field Type Required Description
productIds array of integer Yes The products to set. Must not be empty.
cadCategoryId integer or null No The category to set on every listed product. null, or leaving the field out, clears it.

Example Request

{
    "productIds": [123, 124, 130],
    "cadCategoryId": 7
}

Response (200 - Success)

{
    "success": true,
    "data": {
        "updatedCount": 3,
        "cadCategoryId": 7,
        "cadCategory": { "id": 7, "name": "Display", "abbreviation": "DISP" }
    },
    "message": "Category set on 3 products",
    "timestamp": "2026-10-11T15:30:00Z"
}

updatedCount is how many of the listed IDs are your products. All of them now carry the category, including any that already had it.

Error Responses

StatusCodeDescription
400INVALID_INPUTproductIds is missing or empty
400UNKNOWN_FIELDSThe body names a field other than productIds and cadCategoryId
403READ_ONLY_ACCESSThe caller has read-only access
404NOT_FOUNDcadCategoryId is not one of your categories. Nothing is written.

PUT /api/v1/products/{id}/cad-blocks

Replaces the product's own CAD block for each view. A view you leave out is cleared and uses the category's default block again, or a generic box when the category has none; an empty array clears every view. The block library and the views are in the CAD Blocks API.

Path Parameters

Field Type Required Description
id integer Yes The catalog product ID

Request Body

Field Type Required Description
assignments array of object Yes One item per view the product sets, each view at most once: view (plan, ceiling, elevation or schematic, any case), blockId, and an optional visibilityState, one of a dynamic block's states (null or empty for the block's default state). Any other field name, on the body or on an item, is refused with 400 UNKNOWN_FIELDS.

Example Request

{
    "assignments": [
        { "view": "plan", "blockId": 9, "visibilityState": "TOP" },
        { "view": "elevation", "blockId": 9, "visibilityState": "FRONT" }
    ]
}

Response (200 - Success)

{
    "success": true,
    "data": [
        { "view": "plan", "blockId": 9, "blockName": "AVS_DISP_WALL", "name": "Display wall", "visibilityState": "TOP" },
        { "view": "elevation", "blockId": 9, "blockName": "AVS_DISP_WALL", "name": "Display wall", "visibilityState": "FRONT" }
    ],
    "message": "CAD blocks set for 2 view(s)",
    "timestamp": "2026-10-12T15:30:00Z"
}

The product's blocks as saved, in view order. An empty array returns "data": [] with the message CAD blocks cleared. The product's cadBlocks field reports the same set, with the block's ID as id.

Error Responses

StatusCodeDescription
400INVALID_INPUTThe body or assignments is missing, or an item is null, has no view or an unknown one, has no blockId, or repeats a view
400UNKNOWN_FIELDSThe body or an item names a field not listed above
400BLOCK_ARCHIVEDA new or changed item names an archived block. An item that repeats the product's saved block and state for its view passes, even when the block was archived since.
400VIEW_MISMATCHThe block does not list the item's view
400INVALID_VISIBILITY_STATEThe block has no visibility state of that name; the message lists the ones it has
403READ_ONLY_ACCESSThe caller has read-only access
404PRODUCT_NOT_FOUNDThe product is not one of yours
404BLOCK_NOT_FOUNDA blockId is not one of your blocks

GET /api/v1/products/port-types

Returns all valid port types grouped by category (Video, Audio, Data, Fiber, Power, Control). Use these IDs in the portType field when setting connection metadata.

Response (200 — Success)

{
  "success": true,
  "data": {
    "port_types": {
      "Video": [
        { "id": "hdmi_a", "name": "HDMI Type A" },
        { "id": "displayport", "name": "DisplayPort" }
      ],
      "Audio": [
        { "id": "xlr_3pin_male", "name": "XLR 3-Pin Male" },
        { "id": "speakon_nl4", "name": "Speakon NL4" }
      ],
      "Data": [
        { "id": "rj45", "name": "RJ-45 (Ethernet)" },
        { "id": "usb_c", "name": "USB Type-C" }
      ]
    }
  }
}