# Optima API Routes Documentation This document provides a comprehensive overview of all API routes available in the Optima API. ## Base URL ``` http://localhost:3000/v1 ``` --- ## Object Type Field-Level Gating All fetch and fetchAll endpoints gate response object keys via `processObjectValuePerms`. Each key on the returned object is checked against `.` — only fields the user has permission for are included in the response. Grant `.*` to see all fields. | Object Type | Scope | Affected Routes | | --------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Company | `obj.company` | `GET /company/companies`, `GET /company/companies/:identifier` | | Credential | `obj.credential` | `GET /credential/credentials/:id`, `GET /credential/credentials/company/:companyId`, `GET /credential/credentials/:id/sub-credentials`, `GET /credential-type/:id/credentials` | | Credential Type | `obj.credentialType` | `GET /credential-type/:identifier`, `GET /credential-type/` | | User | `obj.user` | `GET /user/@me`, `GET /user/users/:identifier`, `GET /user/users`, `GET /role/:identifier/users` | | Role | `obj.role` | `GET /role/:identifier`, `GET /role/`, `GET /user/users/:identifier/roles` | | Catalog Item | `obj.catalogItem` | `GET /procurement/items`, `GET /procurement/items/:identifier`, `GET /procurement/items/:identifier/linked` | | Opportunity | `obj.opportunity` | `GET /sales/opportunities`, `GET /sales/opportunities/:identifier` | | UniFi Site | `obj.unifiSite` | `GET /unifi/sites`, `GET /unifi/site/:id`, `GET /company/companies/:identifier/unifi/sites` | | WiFi Network | `unifi.site.wifi.read` | `GET /unifi/site/:id/wifi` | See [PERMISSIONS.md](PERMISSIONS.md) for the full list of field-level permission nodes within each scope. --- ## Authentication Routes ### Get Authentication URI **GET** `/auth/uri` Get the Microsoft OAuth authentication URI for user login. **Authentication Required:** No **Response:** ```json { "status": 200, "message": "Successfully fetch Auth URI", "data": { "uri": "https://login.microsoftonline.com/...", "callbackKey": "ck123..." }, "successful": true } ``` --- ### OAuth Redirect Handler **GET** `/auth/redirect` Handles the OAuth redirect callback from Microsoft. This endpoint processes the authorization code and creates a user session. **Authentication Required:** No **Query Parameters:** - `code` - Authorization code from Microsoft - `state` - Callback key for WebSocket notification **Response:** Closes the browser window and emits authentication tokens via WebSocket. --- ### Refresh Access Token **POST** `/auth/refresh` Refresh an expired access token using a valid refresh token. **Authentication Required:** Yes (Refresh Token) **Headers:** - `x-refresh-token` - The refresh token **Response:** ```json { "status": 201, "message": "Token refreshed successfully!", "data": { "accessToken": "eyJhbGc...", "refreshToken": "eyJhbGc..." }, "successful": true } ``` --- ## User Routes ### Get Current User **GET** `/user/@me` Fetch the currently authenticated user's information. **Authentication Required:** Yes **Required Scopes:** `user.read` **Field-Level Gating:** `obj.user` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **Response:** ```json { "status": 200, "message": "Fetched user.", "data": { "id": "ckx...", "name": "John Doe", "email": "john.doe@example.com", "login": "john.doe", "image": "https://...", "roles": ["admin"], "createdAt": "2026-01-01T00:00:00.000Z", "updatedAt": "2026-02-14T00:00:00.000Z" }, "successful": true } ``` --- ### Update Current User **PATCH** `/user/@me` Update the currently authenticated user's information. **Authentication Required:** Yes **Required Scopes:** `user.write` **Request Body:** ```json { "name": "Jane Doe", "image": "https://example.com/avatar.jpg" } ``` **Response:** ```json { "status": 200, "message": "Successfully updated user.", "data": { "id": "ckx...", "name": "Jane Doe", "email": "jane.doe@example.com", "image": "https://example.com/avatar.jpg" }, "successful": true } ``` --- ### Check User Permissions **POST** `/user/@me/check-permission` Check if the currently authenticated user has specific permissions. Accepts an array of permission nodes and returns the status for each. **Authentication Required:** Yes **Required Scopes:** `user.read` **Request Body:** ```json { "permissions": ["user.read", "company.create", "credential.write"] } ``` **Response:** ```json { "status": 200, "message": "Permission check completed.", "data": { "results": [ { "permission": "user.read", "hasPermission": true }, { "permission": "company.create", "hasPermission": false }, { "permission": "credential.write", "hasPermission": true } ] }, "successful": true } ``` --- ## Other User Routes ### Get All Users **GET** `/user/users` Fetch a list of all users. **Authentication Required:** Yes **Required Permissions:** `user.read.other`, `user.list.other` **Field-Level Gating:** `obj.user` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **Response:** ```json { "status": 200, "message": "Users Fetched Successfully!", "data": [ { "id": "ckx...", "name": "John Doe", "email": "john.doe@example.com", "login": "john.doe", "image": "https://...", "roles": ["admin"] } ], "successful": true } ``` --- ### Get User by ID **GET** `/user/users/:identifier` Fetch a specific user by their ID. **Authentication Required:** Yes **Required Permissions:** `user.read.other` **Field-Level Gating:** `obj.user` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **Path Parameters:** - `identifier` - The user's ID **Response:** ```json { "status": 200, "message": "User Fetched Successfully!", "data": { "id": "ckx...", "name": "John Doe", "email": "john.doe@example.com", "login": "john.doe", "image": "https://...", "roles": ["admin"] }, "successful": true } ``` **Error Response (404):** ```json { "status": 404, "message": "User with identifier 'ckx...' was not found.", "error": "UserNotFound", "successful": false } ``` --- ### Update User by ID **PATCH** `/user/users/:identifier` Update a specific user's information. Supports updating profile fields, roles, and direct permissions. **Authentication Required:** Yes **Required Permissions:** `user.write.other` **Conditional Permissions:** - If `roles` is included in the body: `user.roles.other` is also required - If `permissions` is included in the body: `user.permissions.other` is also required **Path Parameters:** - `identifier` - The user's ID **Request Body:** All fields are optional. Include only the fields you want to update. ```json { "name": "Jane Doe", "image": "https://example.com/avatar.jpg", "roles": ["admin", "moderator"], "permissions": ["credential.fetch", "company.fetch"] } ``` | Field | Type | Description | | ------------- | ---------- | -------------------------------------------------------------- | | `name` | `string` | The user's display name | | `image` | `string` | URL to the user's avatar image | | `roles` | `string[]` | Array of role ids or monikers to assign (replaces all roles) | | `permissions` | `string[]` | Array of permission nodes to assign (replaces all permissions) | **Response:** ```json { "status": 200, "message": "User Updated Successfully!", "data": { "id": "ckx...", "name": "Jane Doe", "email": "jane.doe@example.com", "login": "jane.doe", "image": "https://example.com/avatar.jpg", "roles": ["admin", "moderator"] }, "successful": true } ``` **Error Response (403 - Missing role permission):** ```json { "status": 403, "message": "You do not have permission to modify roles on another user.", "error": "InsufficientPermission", "successful": false } ``` --- ### Delete User by ID **DELETE** `/user/users/:identifier` Delete a specific user. **Authentication Required:** Yes **Required Permissions:** `user.delete.other` **Path Parameters:** - `identifier` - The user's ID **Response:** ```json { "status": 200, "message": "User Deleted Successfully!", "data": { "id": "ckx...", "name": "John Doe", "email": "john.doe@example.com", "login": "john.doe", "image": "https://...", "roles": ["admin"] }, "successful": true } ``` --- ### Get User Roles **GET** `/user/users/:identifier/roles` Fetch all roles assigned to a specific user. **Authentication Required:** Yes **Required Permissions:** `user.read.other`, `role.read` **Field-Level Gating:** `obj.role` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **Path Parameters:** - `identifier` - The user's ID **Response:** ```json { "status": 200, "message": "User Roles Fetched Successfully!", "data": [ { "id": "uuid...", "title": "Administrator", "moniker": "admin", "permissions": ["*"] } ], "successful": true } ``` --- ### Check User Permissions (Other User) **POST** `/user/users/:identifier/check-permission` Check if a specific user has certain permissions. **Authentication Required:** Yes **Required Permissions:** `user.read.other` **Path Parameters:** - `identifier` - The user's ID **Request Body:** ```json { "permissions": ["user.read", "company.fetch", "credential.write"] } ``` **Response:** ```json { "status": 200, "message": "Permission check completed.", "data": { "results": [ { "permission": "user.read", "hasPermission": true }, { "permission": "company.fetch", "hasPermission": false }, { "permission": "credential.write", "hasPermission": true } ] }, "successful": true } ``` --- ## Company Routes ### Get All Companies **GET** `/company/companies` Fetch a paginated list of all companies with optional search functionality. **Authentication Required:** Yes **Required Permissions:** `company.fetch.many` **Field-Level Gating:** `obj.company` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **Query Parameters:** - `page` (optional) - Page number (default: 1) - `rpp` (optional) - Records per page (default: 30) - `search` (optional) - Search query to filter companies **Response:** ```json { "status": 200, "message": "Companies Fetched Successfully!", "data": [ { "id": "ckx...", "name": "Acme Corp", "cw_CompanyId": 12345, "cw_Identifier": "AcmeCorp" } ], "pagination": { "previousPage": null, "currentPage": 1, "nextPage": 2, "totalPages": 10, "totalRecords": 300, "listedRecords": 30 }, "successful": true } ``` --- ### Get Company by ID **GET** `/company/companies/:identifier` Fetch a single company by its ID. Automatically fetches fresh data from ConnectWise and returns it along with internal company data. **Authentication Required:** Yes **Required Permissions:** - `company.fetch` (base permission) - `company.fetch.address` (required when `includeAddress=true`) - `company.fetch.contacts` (required when `includeAllContacts=true`) **Field-Level Gating:** `obj.company` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **URL Parameters:** - `identifier` - Company ID (internal database ID) **Query Parameters:** - `includeAddress` (optional) - Set to "true" to include full address information. Requires `company.fetch.address` permission. (default: false) - `includePrimaryContact` (optional) - Set to "true" to include the company's default contact from ConnectWise. (default: false) - `includeAllContacts` (optional) - Set to "true" to include all contacts for the company from ConnectWise. Requires `company.fetch.contacts` permission. (default: false) **Response (without optional query params):** ```json { "status": 200, "message": "Company Fetched Successfully!", "data": { "id": "ckx...", "name": "Acme Corp", "cw_CompanyId": 12345, "cw_Identifier": "AcmeCorp", "cw_Data": {} }, "successful": true } ``` **Response (with includeAddress=true):** ```json { "status": 200, "message": "Company Fetched Successfully!", "data": { "id": "ckx...", "name": "Acme Corp", "cw_CompanyId": 12345, "cw_Identifier": "AcmeCorp", "cw_Data": { "address": { "line1": "123 Main St", "line2": null, "city": "Springfield", "state": "IL", "zip": "62701", "country": "United States" } } }, "successful": true } ``` **Response (with includePrimaryContact=true):** ```json { "status": 200, "message": "Company Fetched Successfully!", "data": { "id": "ckx...", "name": "Acme Corp", "cw_CompanyId": 12345, "cw_Identifier": "AcmeCorp", "cw_Data": { "primaryContact": { "firstName": "John", "lastName": "Doe", "cwId": 456, "inactive": false, "title": "IT Manager", "phone": "555-0123", "email": "john.doe@acmecorp.com" } } }, "successful": true } ``` **Response (with includeAllContacts=true):** ```json { "status": 200, "message": "Company Fetched Successfully!", "data": { "id": "ckx...", "name": "Acme Corp", "cw_CompanyId": 12345, "cw_Identifier": "AcmeCorp", "cw_Data": { "allContacts": [ { "firstName": "John", "lastName": "Doe", "cwId": 456, "inactive": false, "title": "IT Manager", "phone": "555-0123", "email": "john.doe@acmecorp.com" }, { "firstName": "Jane", "lastName": "Smith", "cwId": 789, "inactive": false, "title": "CTO", "phone": "555-0456", "email": "jane.smith@acmecorp.com" } ] } }, "successful": true } ``` --- ### Get Company Configurations **GET** `/company/companies/:identifier/configurations` Fetch configurations for a specific company from ConnectWise. **Authentication Required:** Yes **Required Permissions:** `company.fetch`, `company.fetch.configurations` **URL Parameters:** - `identifier` - Company ID **Response:** ```json { "status": 200, "message": "Company Configurations Fetched Successfully!", "data": { // ConnectWise configuration data }, "successful": true } ``` --- ### Get Company UniFi Sites **GET** `/company/companies/:identifier/unifi/sites` Fetch all UniFi sites linked to a specific company. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `company.fetch` **Field-Level Gating:** `obj.unifiSite` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **URL Parameters:** - `identifier` - Company ID **Response:** ```json { "status": 200, "message": "Company UniFi Sites Fetched Successfully!", "data": [ { "id": "ckx...", "name": "Main Office", "siteId": "abc123", "companyId": "ckx...", "createdAt": "2025-01-01T00:00:00.000Z", "updatedAt": "2025-01-01T00:00:00.000Z" } ], "successful": true } ``` --- ## Credential Routes ### Get Value Types **GET** `/credential/valuetypes` Returns all available field value types for credential type fields. **Authentication Required:** Yes **Response:** ```json { "status": 200, "message": "Value Types Fetched Successfully!", "data": [ "plain_text", "license_key", "ip_address", "generic_secret", "bitlocker_key", "password", "multi_credential" ], "successful": true } ``` --- ### Get Credential by ID **GET** `/credential/credentials/:id` Fetch a single credential by its ID. **Authentication Required:** Yes **Required Permissions:** `credential.fetch` **Field-Level Gating:** `obj.credential` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **URL Parameters:** - `id` - Credential ID **Response:** ```json { "status": 200, "message": "Credential Fetched Successfully!", "data": { "id": "ckx...", "name": "AWS Credentials", "notes": null, "typeId": "cky...", "companyId": "ckz...", "fields": [ { "id": "accessKeyId", "name": "Access Key ID", "secure": false, "required": true, "valueType": "plain_text", "value": "AKIAIOSFODNN7EXAMPLE" }, { "id": "secretAccessKey", "name": "Secret Access Key", "secure": true, "required": true, "valueType": "password", "value": null } ], "type": { "id": "cky...", "name": "AWS", "fields": [...], "permissionScope": "aws.credentials" }, "company": { "id": "ckz...", "name": "Acme Corp" }, "createdAt": "2026-01-01T00:00:00.000Z", "updatedAt": "2026-02-14T00:00:00.000Z" }, "successful": true } ``` --- ### Get Credentials by Company **GET** `/credential/credentials/company/:companyId` Fetch all credentials associated with a specific company. **Authentication Required:** Yes **Required Permissions:** `credential.fetch.many` **Field-Level Gating:** `obj.credential` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **URL Parameters:** - `companyId` - Company ID **Response:** ```json { "status": 200, "message": "Company Credentials Fetched Successfully!", "data": [ { "id": "ckx...", "name": "AWS Credentials", "notes": null, "typeId": "cky...", "companyId": "ckz...", "fields": [ { "id": "accessKeyId", "name": "Access Key ID", "secure": false, "required": true, "valueType": "plain_text", "value": "AKIAIOSFODNN7EXAMPLE" }, { "id": "secretAccessKey", "name": "Secret Access Key", "secure": true, "required": true, "valueType": "password", "value": null } ], "type": {...}, "company": {...} } ], "successful": true } ``` --- ### Create Credential **POST** `/credential/credentials` Create a new credential with validated and encrypted fields. **Authentication Required:** Yes **Required Permissions:** `credential.create` **Request Body:** ```json { "name": "Production AWS Credentials", "notes": "Used for production S3 access", "typeId": "cky...", "companyId": "ckz...", "fields": [ { "id": "ckx1...", "fieldId": "accessKeyId", "value": "AKIAIOSFODNN7EXAMPLE" }, { "id": "ckx2...", "fieldId": "secretAccessKey", "value": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" } ], "subCredentials": { "tunnels": [ { "name": "Tunnel 1", "fields": [ { "fieldId": "server", "value": "vpn1.example.com" }, { "fieldId": "port", "value": "443" } ] } ] } } ``` | Field | Type | Required | Description | | ---------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | Yes | Display name for the credential | | `notes` | `string` | No | Optional notes | | `typeId` | `string` | Yes | The credential type ID | | `companyId` | `string` | Yes | The company ID this credential belongs to | | `fields` | `array` | Yes | Array of field values (`{ fieldId, value }`) | | `subCredentials` | `object` | No | Keyed by multi-credential field ID. Each value is an array of `{ name, fields }` objects for inline sub-credentials | ```` **Response:** ```json { "status": 201, "message": "Credential Created Successfully!", "data": { "id": "ckx...", "name": "Production AWS Credentials", "typeId": "cky...", "companyId": "ckz...", "fields": [ { "id": "accessKeyId", "name": "Access Key ID", "secure": false, "required": true, "valueType": "plain_text", "value": "AKIAIOSFODNN7EXAMPLE" }, { "id": "secretAccessKey", "name": "Secret Access Key", "secure": true, "required": true, "valueType": "password", "value": null } ], "type": {...}, "company": {...} }, "successful": true } ```` --- ### Update Credential **PATCH** `/credential/credentials/:id` Update a credential's basic properties (name, notes) and/or field values. Secure fields are automatically encrypted. **Authentication Required:** Yes **Required Permissions:** `credential.update` **URL Parameters:** - `id` - Credential ID **Request Body:** All properties are optional. Include only the properties you want to update. ```json { "name": "Updated Credential Name", "notes": "Updated notes for this credential", "fields": [ { "fieldId": "accessKeyId", "value": "AKIAIOSFODNN7EXAMPLE" }, { "fieldId": "secretAccessKey", "value": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" } ] } ``` **Response:** ```json { "status": 200, "message": "Credential Updated Successfully!", "data": { "id": "ckx...", "name": "Updated Credential Name", "notes": "Updated notes for this credential", "typeId": "cky...", "companyId": "ckz...", "fields": [ { "id": "accessKeyId", "name": "Access Key ID", "secure": false, "required": true, "valueType": "plain_text", "value": "AKIAIOSFODNN7EXAMPLE" } ], "type": {...}, "company": {...} }, "successful": true } ``` --- ### Update Credential Fields **PUT** `/credential/credentials/:id/fields` Validate and update credential field values. Secure fields are automatically encrypted. **Authentication Required:** Yes **Required Permissions:** `credential.update`, `credential.fields.update` **URL Parameters:** - `id` - Credential ID **Request Body:** ```json { "fields": [ { "fieldId": "accessKeyId", "value": "AKIAIOSFODNN7NEWVALUE" }, { "fieldId": "secretAccessKey", "value": "newSecretKeyValue123" } ] } ``` **Response:** ```json { "status": 200, "message": "Credential Fields Updated Successfully!", "data": { "id": "ckx...", "name": "Production AWS Credentials", "notes": null, "typeId": "cky...", "companyId": "ckz...", "fields": [ { "id": "accessKeyId", "name": "Access Key ID", "secure": false, "required": true, "valueType": "plain_text", "value": "AKIAIOSFODNN7NEWVALUE" }, { "id": "secretAccessKey", "name": "Secret Access Key", "secure": true, "required": true, "valueType": "password", "value": null } ], "type": {...}, "company": {...} }, "successful": true } ``` --- ### Get Credential Fields **GET** `/credential/credentials/:id/fields` Fetch all field values for a credential (secure fields returned encrypted). **Authentication Required:** Yes **Required Permissions:** `credential.fetch`, `credential.fields.fetch` **URL Parameters:** - `id` - Credential ID **Response:** ```json { "status": 200, "message": "Credential Fields Fetched Successfully!", "data": [ { "id": "ckx-accessKeyId", "fieldId": "accessKeyId", "value": "AKIAIOSFODNN7EXAMPLE" }, { "id": "ckx1...", "fieldId": "secretAccessKey", "value": "base64EncryptedValue==" } ], "successful": true } ``` --- ### Read Secure Values **GET** `/credential/credentials/:id/secure-values` Decrypt and return all secure field values for a credential. **Authentication Required:** Yes **Required Permissions:** `credential.fetch`, `credential.secure_values.read` **URL Parameters:** - `id` - Credential ID **Response:** ```json { "status": 200, "message": "Secure Values Fetched Successfully!", "data": { "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "apiKey": "sk_live_123456789abcdef" }, "successful": true } ``` --- ### Read Single Secure Value **GET** `/credential/credentials/:id/secure-values/:fieldId` Decrypt and return a single secure field value for a credential. **Authentication Required:** Yes **Required Permissions:** `credential.fetch`, `credential.secure_values.read` **URL Parameters:** - `id` - Credential ID - `fieldId` - The field ID of the secure value to read **Response:** ```json { "status": 200, "message": "Secure Value Fetched Successfully!", "data": { "fieldId": "secretAccessKey", "value": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" }, "successful": true } ``` **Error Response (404):** ```json { "status": 404, "message": "Secure field not found: unknownField", "error": "SecureFieldNotFound", "successful": false } ``` --- ### Delete Credential **DELETE** `/credential/credentials/:id` Delete a credential and all associated secure values. Sub-credentials are cascade-deleted automatically. **Authentication Required:** Yes **Required Permissions:** `credential.delete` **URL Parameters:** - `id` - Credential ID **Response:** ```json { "status": 200, "message": "Credential Deleted Successfully!", "data": null, "successful": true } ``` --- ### Get Sub-Credentials **GET** `/credential/credentials/:id/sub-credentials` Fetch all sub-credentials that belong to a specific parent credential. **Authentication Required:** Yes **Required Permissions:** `credential.fetch`, `credential.sub_credentials.fetch` **Field-Level Gating:** `obj.credential` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **URL Parameters:** - `id` - Parent Credential ID **Response:** ```json { "status": 200, "message": "Sub-Credentials Fetched Successfully!", "data": [ { "id": "ckx1...", "name": "Tunnel 1", "notes": null, "typeId": "cky...", "companyId": "ckz...", "subCredentialOfId": "ckx...", "fields": [ { "id": "server", "value": "vpn1.example.com", "secure": false }, { "id": "port", "value": "443", "secure": false } ], "type": { "..." }, "company": { "..." }, "createdAt": "2026-02-20T00:00:00.000Z", "updatedAt": "2026-02-20T00:00:00.000Z" } ], "successful": true } ``` --- ### Add Sub-Credential **POST** `/credential/credentials/:id/sub-credentials` Create a new sub-credential under an existing parent credential for a specific multi-credential field. **Authentication Required:** Yes **Required Permissions:** `credential.fetch`, `credential.sub_credentials.create` **URL Parameters:** - `id` - Parent Credential ID **Request Body:** ```json { "fieldId": "tunnels", "name": "Tunnel 2", "fields": [ { "fieldId": "server", "value": "vpn2.example.com" }, { "fieldId": "port", "value": "1194" } ] } ``` | Field | Type | Required | Description | | --------- | -------- | -------- | --------------------------------------------------------------- | | `fieldId` | `string` | Yes | The multi-credential field ID on the parent credential type | | `name` | `string` | Yes | Display name for the sub-credential | | `fields` | `array` | Yes | Array of field values matching the multi-credential's subFields | **Response:** ```json { "status": 201, "message": "Sub-Credential Created Successfully!", "data": { "id": "ckx2...", "name": "Tunnel 2", "typeId": "cky...", "companyId": "ckz...", "subCredentialOfId": "ckx...", "fields": [ { "id": "server", "value": "vpn2.example.com", "secure": false }, { "id": "port", "value": "1194", "secure": false } ], "type": { "..." }, "company": { "..." }, "createdAt": "2026-02-20T00:00:00.000Z", "updatedAt": "2026-02-20T00:00:00.000Z" }, "successful": true } ``` --- ### Remove Sub-Credential **DELETE** `/credential/credentials/:id/sub-credentials/:subId` Delete a sub-credential and remove its reference from the parent credential's multi-credential field. **Authentication Required:** Yes **Required Permissions:** `credential.fetch`, `credential.sub_credentials.delete` **URL Parameters:** - `id` - Parent Credential ID - `subId` - Sub-Credential ID to remove **Response:** ```json { "status": 200, "message": "Sub-Credential Removed Successfully!", "data": null, "successful": true } ``` **Error Response (404):** ```json { "status": 404, "message": "Sub-credential not found", "error": "SubCredentialNotFound", "successful": false } ``` --- ## Credential Type Routes ### Get Credential Type by ID or Name **GET** `/credential-type/:identifier` Fetch a single credential type by its ID or name. **Authentication Required:** Yes **Required Permissions:** `credential_type.fetch` **Field-Level Gating:** `obj.credentialType` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **URL Parameters:** - `identifier` - Credential Type ID or name **Response:** ```json { "status": 200, "message": "Credential Type Fetched Successfully!", "data": { "id": "cky...", "name": "AWS", "permissionScope": "aws.credentials", "icon": "https://aws.amazon.com/favicon.ico", "fields": [ { "id": "accessKeyId", "name": "Access Key ID", "required": true, "secure": false, "valueType": "plain_text" }, { "id": "secretAccessKey", "name": "Secret Access Key", "required": true, "secure": true, "valueType": "password" } ], "credentialCount": 5, "createdAt": "2026-01-01T00:00:00.000Z", "updatedAt": "2026-02-14T00:00:00.000Z" }, "successful": true } ``` --- ### Get All Credential Types **GET** `/credential-type` Fetch all credential types in the system. **Authentication Required:** Yes **Required Permissions:** `credential_type.fetch.many` **Field-Level Gating:** `obj.credentialType` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **Response:** ```json { "status": 200, "message": "Credential Types Fetched Successfully!", "data": [ { "id": "cky...", "name": "AWS", "permissionScope": "aws.credentials", "icon": "https://aws.amazon.com/favicon.ico", "fields": [...], "credentialCount": 5 }, { "id": "ckz...", "name": "Azure", "permissionScope": "azure.credentials", "icon": "https://azure.microsoft.com/favicon.ico", "fields": [...], "credentialCount": 3 } ], "successful": true } ``` --- ### Create Credential Type **POST** `/credential-type` Create a new credential type with field definitions. **Authentication Required:** Yes **Required Permissions:** `credential_type.create` **Request Body:** ```json { "name": "GitHub", "permissionScope": "github.credentials", "icon": "https://github.com/favicon.ico", "fields": [ { "id": "username", "name": "Username", "required": true, "secure": false, "valueType": "plain_text" }, { "id": "personalAccessToken", "name": "Personal Access Token", "required": true, "secure": true, "valueType": "password" } ] } ``` **Multi-Credential Example:** Fields with `valueType: "multi_credential"` support an optional `subFields` array that defines the field structure for each sub-credential entry. Sub-fields use the same schema and can be nested recursively. ```json { "name": "VPN Config", "permissionScope": "vpn.credentials", "icon": "https://example.com/vpn.ico", "fields": [ { "id": "hostname", "name": "Hostname", "required": true, "secure": false, "valueType": "plain_text" }, { "id": "tunnels", "name": "Tunnels", "required": false, "secure": false, "valueType": "multi_credential", "subFields": [ { "id": "server", "name": "Server", "required": true, "secure": false, "valueType": "plain_text" }, { "id": "psk", "name": "Pre-Shared Key", "required": true, "secure": true, "valueType": "password" } ] } ] } ``` | Field | Type | Required | Description | | -------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------- | | `fields[].id` | `string` | Yes | Unique identifier for the field | | `fields[].name` | `string` | Yes | Display name for the field | | `fields[].required` | `bool` | Yes | Whether the field is required when creating a credential | | `fields[].secure` | `bool` | Yes | Whether the field value should be encrypted at rest | | `fields[].valueType` | `string` | Yes | One of the supported value types (see `GET /credential/valuetypes`) | | `fields[].subFields` | `array` | No | Only for `multi_credential` fields. Defines the field structure for each nested sub-credential | **Response:** ```json { "status": 201, "message": "Credential Type Created Successfully!", "data": { "id": "ck1...", "name": "GitHub", "permissionScope": "github.credentials", "icon": "https://github.com/favicon.ico", "fields": [...] }, "successful": true } ``` --- ### Update Credential Type **PATCH** `/credential-type/:id` Update a credential type's properties or field definitions. **Authentication Required:** Yes **Required Permissions:** `credential_type.update` **URL Parameters:** - `id` - Credential Type ID **Request Body:** ```json { "name": "GitHub Enterprise", "icon": "https://github.enterprise.com/favicon.ico", "fields": [ { "id": "username", "name": "Username", "required": true, "secure": false, "valueType": "plain_text" }, { "id": "personalAccessToken", "name": "Personal Access Token", "required": true, "secure": true, "valueType": "password" }, { "id": "enterpriseUrl", "name": "Enterprise URL", "required": true, "secure": false, "valueType": "plain_text" } ] } ``` > **Note:** Fields with `valueType: "multi_credential"` support an optional `subFields` array — see the [Create Credential Type](#create-credential-type) section for the full schema and example. **Response:** ```json { "status": 200, "message": "Credential Type Updated Successfully!", "data": { "id": "ck1...", "name": "GitHub Enterprise", "fields": [...] }, "successful": true } ``` --- ### Delete Credential Type **DELETE** `/credential-type/:id` Delete a credential type. This will cascade delete all credentials of this type. **Authentication Required:** Yes **Required Permissions:** `credential_type.delete` **URL Parameters:** - `id` - Credential Type ID **Response:** ```json { "status": 200, "message": "Credential Type Deleted Successfully!", "data": null, "successful": true } ``` --- ### Get Credentials by Type **GET** `/credential-type/:id/credentials` Fetch all credentials that use a specific credential type. **Authentication Required:** Yes **Required Permissions:** `credential_type.fetch`, `credential.fetch.many` **Field-Level Gating:** `obj.credential` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **URL Parameters:** - `id` - Credential Type ID **Response:** ```json { "status": 200, "message": "Credentials Fetched Successfully!", "data": [ { "id": "ckx...", "name": "Production AWS", "typeId": "cky...", "companyId": "ckz...", "fields": {...} }, { "id": "ck2...", "name": "Staging AWS", "typeId": "cky...", "companyId": "ckz...", "fields": {...} } ], "successful": true } ``` --- ## Role Routes ### Create Role **POST** `/role` Create a new role with a title, moniker, and optional permissions. **Authentication Required:** Yes **Required Permissions:** `role.create` **Request Body:** ```json { "title": "System Administrator", "moniker": "system_admin", "permissions": [ "user.read", "user.write", "company.fetch", "credential.create" ] } ``` **Response:** ```json { "status": 201, "message": "Role Created Successfully!", "data": { "id": "ckx...", "title": "System Administrator", "moniker": "system_admin", "permissions": [ "user.read", "user.write", "company.fetch", "credential.create" ], "createdAt": "2026-02-17T00:00:00.000Z", "updatedAt": "2026-02-17T00:00:00.000Z" }, "successful": true } ``` --- ### Get Role by ID or Moniker **GET** `/role/:identifier` Fetch a single role by its ID or moniker. **Authentication Required:** Yes **Required Permissions:** `role.read` **Field-Level Gating:** `obj.role` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **URL Parameters:** - `identifier` - Role ID or moniker **Response:** ```json { "status": 200, "message": "Role Fetched Successfully!", "data": { "id": "ckx...", "title": "System Administrator", "moniker": "system_admin", "permissions": [ "user.read", "user.write", "company.fetch", "credential.create" ], "createdAt": "2026-02-17T00:00:00.000Z", "updatedAt": "2026-02-17T00:00:00.000Z" }, "successful": true } ``` --- ### Get All Roles **GET** `/role` Fetch all roles in the system. **Authentication Required:** Yes **Required Permissions:** `role.read`, `role.list` **Field-Level Gating:** `obj.role` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **Response:** ```json { "status": 200, "message": "Roles Fetched Successfully!", "data": [ { "id": "ckx...", "title": "System Administrator", "moniker": "system_admin", "permissions": ["user.read", "user.write"], "createdAt": "2026-02-17T00:00:00.000Z", "updatedAt": "2026-02-17T00:00:00.000Z" }, { "id": "cky...", "title": "Viewer", "moniker": "viewer", "permissions": ["user.read", "company.fetch"], "createdAt": "2026-02-17T00:00:00.000Z", "updatedAt": "2026-02-17T00:00:00.000Z" } ], "successful": true } ``` --- ### Update Role **PATCH** `/role/:identifier` Update a role's title, moniker, or permissions. **Authentication Required:** Yes **Required Permissions:** `role.modify` **URL Parameters:** - `identifier` - Role ID or moniker **Request Body:** ```json { "title": "Super Administrator", "moniker": "super_admin", "permissions": ["*"] } ``` **Response:** ```json { "status": 200, "message": "Role Updated Successfully!", "data": { "id": "ckx...", "title": "Super Administrator", "moniker": "super_admin", "permissions": ["*"], "createdAt": "2026-02-17T00:00:00.000Z", "updatedAt": "2026-02-17T12:00:00.000Z" }, "successful": true } ``` --- ### Delete Role **DELETE** `/role/:identifier` Delete a role. This will remove the role from all users that have it assigned. **Authentication Required:** Yes **Required Permissions:** `role.delete` **URL Parameters:** - `identifier` - Role ID or moniker **Response:** ```json { "status": 200, "message": "Role Deleted Successfully!", "data": { "id": "ckx...", "title": "System Administrator", "moniker": "system_admin", "createdAt": "2026-02-17T00:00:00.000Z", "updatedAt": "2026-02-17T12:00:00.000Z" }, "successful": true } ``` --- ### Add Permissions to Role **POST** `/role/:identifier/permissions` Add one or more permissions to an existing role. The new permissions will be merged with existing permissions. **Authentication Required:** Yes **Required Permissions:** `role.modify` **URL Parameters:** - `identifier` - Role ID or moniker **Request Body:** ```json { "permissions": ["credential.update", "credential.delete"] } ``` **Response:** ```json { "status": 200, "message": "Permissions Added Successfully!", "data": { "id": "ckx...", "title": "System Administrator", "moniker": "system_admin", "permissions": [ "user.read", "user.write", "company.fetch", "credential.create", "credential.update", "credential.delete" ], "createdAt": "2026-02-17T00:00:00.000Z", "updatedAt": "2026-02-17T12:30:00.000Z" }, "successful": true } ``` --- ### Remove Permissions from Role **DELETE** `/role/:identifier/permissions` Remove one or more permissions from an existing role. **Authentication Required:** Yes **Required Permissions:** `role.modify` **URL Parameters:** - `identifier` - Role ID or moniker **Request Body:** ```json { "permissions": ["credential.delete"] } ``` **Response:** ```json { "status": 200, "message": "Permissions Removed Successfully!", "data": { "id": "ckx...", "title": "System Administrator", "moniker": "system_admin", "permissions": [ "user.read", "user.write", "company.fetch", "credential.create", "credential.update" ], "createdAt": "2026-02-17T00:00:00.000Z", "updatedAt": "2026-02-17T12:45:00.000Z" }, "successful": true } ``` --- ### Get Users with Role **GET** `/role/:identifier/users` Fetch all users that have been assigned a specific role. **Authentication Required:** Yes **Required Permissions:** `role.read`, `user.read` **Field-Level Gating:** `obj.user` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **URL Parameters:** - `identifier` - Role ID or moniker **Response:** ```json { "status": 200, "message": "Users Fetched Successfully!", "data": [ { "id": "cku...", "name": "John Doe", "login": "john.doe", "roles": ["ckx..."], "createdAt": "2026-01-15T00:00:00.000Z", "updatedAt": "2026-02-10T00:00:00.000Z" }, { "id": "ckv...", "name": "Jane Smith", "login": "jane.smith", "roles": ["ckx...", "cky..."], "createdAt": "2026-01-20T00:00:00.000Z", "updatedAt": "2026-02-12T00:00:00.000Z" } ], "successful": true } ``` --- ## Permission Routes ### Get All Permission Nodes (Categorized) **GET** `/permissions` Fetch all permission nodes organized by category. Returns the full permission node definition object with categories as keys. **Authentication Required:** Yes **Required Permissions:** `role.read` **Response:** ```json { "status": 200, "message": "Permission Nodes Fetched Successfully!", "data": { "global": { "name": "Global Permissions", "description": "Global wildcard permissions that grant access to all resources", "permissions": [ { "node": "*", "description": "Full access to all resources and actions (administrator role)", "usedIn": [] } ] }, "company": { "..." }, "credential": { "..." }, "...additional categories": { "..." } }, "successful": true } ``` --- ### Get All Permission Nodes (Flat) **GET** `/permissions/nodes` Fetch a flat array of all permission nodes across all categories. **Authentication Required:** Yes **Required Permissions:** `role.read` **Response:** ```json { "status": 200, "message": "All Permission Nodes Fetched Successfully!", "data": [ { "node": "*", "description": "Full access to all resources and actions (administrator role)", "usedIn": [] }, { "node": "company.fetch", "description": "Fetch a single company", "usedIn": ["src/api/companies/[id]/fetch.ts"] }, "...additional nodes" ], "successful": true } ``` --- ### Get Permission Nodes by Category **GET** `/permissions/:category` Fetch all permission nodes for a specific category. **Authentication Required:** Yes **Required Permissions:** `role.read` **Path Parameters:** - `category` - The category key (e.g., `global`, `company`, `credential`, `credentialType`, `role`, `user`, `permission`, `uiNavigation`, `adminUI`) **Response (Success):** ```json { "status": 200, "message": "Permission Category Fetched Successfully!", "data": { "name": "Company Permissions", "description": "Permissions for accessing and managing company resources", "permissions": [ { "node": "company.fetch", "description": "Fetch a single company", "usedIn": ["src/api/companies/[id]/fetch.ts"] }, { "node": "company.fetch.address", "description": "View company address information", "usedIn": ["src/api/companies/[id]/fetch.ts"], "dependencies": ["company.fetch"] } ] }, "successful": true } ``` **Response (Not Found):** ```json { "status": 404, "message": "Permission category \"invalidCategory\" not found", "error": "NotFound", "successful": false } ``` --- ## Utility Routes ### Teapot **GET** `/teapot` A fun Easter egg endpoint that returns HTTP 418 (I'm a teapot). **Authentication Required:** No **Response:** ```json { "status": 418, "message": "I'm a teapot", "successful": false } ``` --- ## Procurement Routes ### Get All Catalog Items **GET** `/procurement/items` Fetch a paginated list of catalog items. Supports search. **Authentication Required:** Yes **Required Permissions:** `procurement.catalog.fetch.many` **Field-Level Gating:** `obj.catalogItem` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **Query Parameters:** - `page` (optional, default `1`) — Page number - `rpp` (optional, default `30`) — Records per page - `search` (optional) — Search by name, description, part number, vendor SKU, or manufacturer - `includeInactive` (optional, default `false`) — Include inactive catalog items in results - `category` (optional) — Filter by CW category name (e.g. `Technology`, `Field`, `General`) - `subcategory` (optional) — Filter by CW subcategory name (e.g. `Network-Switch`, `AlarmBurg-Panels`) - `group` (optional) — Filter by umbrella group name (e.g. `Network`, `AlarmBurg`, `Cables`). When used with `category`, returns items whose subcategory belongs to that group within the category. - `manufacturer` (optional) — Filter by manufacturer name (case-insensitive contains match) - `ecosystem` (optional) — Filter by ecosystem name (e.g. `Networking`, `Video Surveillance`, `Burg/Alarm`). Applies manufacturer + category + subcategory-prefix matching rules. - `inStock` (optional, default `false`) — When `true`, only return items with `onHand > 0` - `minPrice` (optional) — Minimum price filter - `maxPrice` (optional) — Maximum price filter **Response:** ```json { "status": 200, "message": "Catalog items fetched successfully!", "data": [ { "id": "clx...", "cwCatalogId": 123, "name": "Dell OptiPlex 7020", "description": "Dell OptiPlex 7020 SFF Desktop", "customerDescription": "Business Desktop Computer", "internalNotes": null, "category": "Technology", "categoryCwId": 18, "subcategory": "Computer-Desktop", "subcategoryCwId": 106, "manufacturer": "Dell", "manufactureCwId": 45, "partNumber": "OPT7020-SFF", "vendorName": "Dell Direct", "vendorSku": "DELL-OPT7020", "vendorCwId": 12, "price": 899.99, "cost": 650.0, "inactive": false, "salesTaxable": true, "onHand": 5, "cwLastUpdated": "2026-02-25T10:00:00.000Z", "createdAt": "2026-01-15T00:00:00.000Z", "updatedAt": "2026-02-25T10:00:00.000Z" } ], "meta": { "pagination": { "previousPage": null, "currentPage": 1, "nextPage": 2, "totalPages": 10, "totalRecords": 300, "listedRecords": 30 } }, "successful": true } ``` --- ### Get Catalog Item **GET** `/procurement/items/:identifier` Fetch a single catalog item by its internal ID or ConnectWise catalog ID. **Authentication Required:** Yes **Required Permissions:** `procurement.catalog.fetch` **Field-Level Gating:** `obj.catalogItem` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **Path Parameters:** - `identifier` — Internal ID (cuid) or ConnectWise catalog ID (numeric) **Query Parameters:** - `includeLinkedItems` (optional, default `false`) — Include linked catalog items in the response **Response:** ```json { "status": 200, "message": "Catalog item fetched successfully!", "data": { "id": "clx...", "cwCatalogId": 123, "name": "Dell OptiPlex 7020", "description": "Dell OptiPlex 7020 SFF Desktop", "customerDescription": "Business Desktop Computer", "internalNotes": null, "manufacturer": "Dell", "manufactureCwId": 45, "partNumber": "OPT7020-SFF", "vendorName": "Dell Direct", "vendorSku": "DELL-OPT7020", "vendorCwId": 12, "price": 899.99, "cost": 650.0, "inactive": false, "salesTaxable": true, "onHand": 5, "cwLastUpdated": "2026-02-25T10:00:00.000Z", "linkedItems": [ { "id": "clx...", "cwCatalogId": 456, "name": "Dell Warranty - 3 Year" } ], "createdAt": "2026-01-15T00:00:00.000Z", "updatedAt": "2026-02-25T10:00:00.000Z" }, "successful": true } ``` --- ### Get Catalog Item Count **GET** `/procurement/count` Get the total number of catalog items. **Authentication Required:** Yes **Required Permissions:** `procurement.catalog.fetch.many` **Query Parameters:** - `activeOnly` (optional, default `false`) — Only count active (non-inactive) items **Response:** ```json { "status": 200, "message": "Catalog item count fetched successfully!", "data": { "count": 300 }, "successful": true } ``` --- ### Refresh Catalog Item Inventory **POST** `/procurement/items/:identifier/refresh-inventory` Refresh the on-hand inventory count for a catalog item by fetching the latest data from ConnectWise. **Authentication Required:** Yes **Required Permissions:** `procurement.catalog.inventory.refresh` **Path Parameters:** - `identifier` — Internal ID (cuid) or ConnectWise catalog ID (numeric) **Response:** ```json { "status": 200, "message": "Inventory refreshed successfully!", "data": { "id": "clx...", "cwCatalogId": 123, "name": "Dell OptiPlex 7020", "onHand": 7, "price": 899.99, "cost": 650.0, "inactive": false, "createdAt": "2026-01-15T00:00:00.000Z", "updatedAt": "2026-02-26T12:00:00.000Z" }, "successful": true } ``` --- ### Get Linked Catalog Items **GET** `/procurement/items/:identifier/linked` Fetch all catalog items linked to a specific item. **Authentication Required:** Yes **Required Permissions:** `procurement.catalog.fetch` **Field-Level Gating:** `obj.catalogItem` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **Path Parameters:** - `identifier` — Internal ID (cuid), CW identifier string, or CW catalog ID (numeric) **Response:** ```json { "status": 200, "message": "Linked catalog items fetched successfully!", "data": [ { "id": "clx...", "cwCatalogId": 456, "identifier": "DELL-WAR-3YR", "name": "Dell Warranty - 3 Year", "description": "Dell 3 Year ProSupport Warranty", "price": 199.99, "cost": 120.0, "inactive": false, "onHand": 0, "createdAt": "2026-01-15T00:00:00.000Z", "updatedAt": "2026-02-25T10:00:00.000Z" } ], "successful": true } ``` --- ### Link Catalog Items **POST** `/procurement/items/:identifier/link` Link a target catalog item to the specified source item. The source item is identified by the URL parameter and the target by the request body. **Authentication Required:** Yes **Required Permissions:** `procurement.catalog.link` **Path Parameters:** - `identifier` — Internal ID (cuid), CW identifier string, or CW catalog ID (numeric) of the source item **Request Body:** ```json { "targetId": "clx..." } ``` **Response:** ```json { "status": 200, "message": "Catalog item linked successfully!", "data": { "id": "clx...", "cwCatalogId": 123, "identifier": "OPT7020-SFF", "name": "Dell OptiPlex 7020", "linkedItems": [ { "id": "clx...", "cwCatalogId": 456, "identifier": "DELL-WAR-3YR", "name": "Dell Warranty - 3 Year" } ], "createdAt": "2026-01-15T00:00:00.000Z", "updatedAt": "2026-02-26T12:00:00.000Z" }, "successful": true } ``` --- ### Unlink Catalog Items **POST** `/procurement/items/:identifier/unlink` Remove the link between a source catalog item and a target catalog item. **Authentication Required:** Yes **Required Permissions:** `procurement.catalog.link` **Path Parameters:** - `identifier` — Internal ID (cuid), CW identifier string, or CW catalog ID (numeric) of the source item **Request Body:** ```json { "targetId": "clx..." } ``` **Response:** ```json { "status": 200, "message": "Catalog item unlinked successfully!", "data": { "id": "clx...", "cwCatalogId": 123, "identifier": "OPT7020-SFF", "name": "Dell OptiPlex 7020", "linkedItems": [], "createdAt": "2026-01-15T00:00:00.000Z", "updatedAt": "2026-02-26T12:00:00.000Z" }, "successful": true } ``` --- ### Get Categories & Ecosystems **GET** `/procurement/categories` Fetch the full category tree and ecosystem tree. The category tree defines the three-level hierarchy (Category → Group/Subcategory → Subcategory) used for organizing catalog items. The ecosystem tree defines cross-cutting product groupings by manufacturer + category + subcategory-prefix rules. **Authentication Required:** Yes **Required Permissions:** `procurement.catalog.fetch.many` **Response:** ```json { "status": 200, "message": "Category and ecosystem data fetched successfully!", "data": { "categories": [ { "name": "Technology", "cwId": 18, "entries": [ { "type": "subcategory", "name": "GeneralEquip", "cwId": 57 }, { "type": "group", "name": "Network", "subcategories": [ { "name": "Network-Other", "cwId": 174 }, { "name": "Network-Router", "cwId": 119 }, { "name": "Network-Switch", "cwId": 112 }, { "name": "Network-Wireless", "cwId": 111 } ] } ] } ], "ecosystems": [ { "name": "Networking", "manufacturers": [ { "name": "Ubiquiti", "cwId": 248, "category": "Technology", "subcategoryPrefix": "Network-" }, { "name": "TP-Link", "cwId": 259, "category": "Technology", "subcategoryPrefix": "Network-" } ] } ] }, "successful": true } ``` --- ### Get Filter Values **GET** `/procurement/filters` Fetch the distinct values available for filter dropdowns (categories, subcategories, manufacturers) in the current dataset. Optionally scope the results with category/subcategory filters to cascade dependent dropdowns. **Authentication Required:** Yes **Required Permissions:** `procurement.catalog.fetch.many` **Query Parameters:** - `category` (optional) — Scope subcategories and manufacturers to items in this category - `subcategory` (optional) — Scope manufacturers to items in this subcategory - `includeInactive` (optional, default `false`) — Include inactive catalog items **Response:** ```json { "status": 200, "message": "Available filter values fetched successfully!", "data": { "categories": ["Field", "General", "Technology"], "subcategories": [ "Network-Other", "Network-Router", "Network-Switch", "Network-Wireless" ], "manufacturers": ["TP-Link", "Ubiquiti"] }, "successful": true } ``` --- ## Sales Routes Sales routes serve opportunity data stored locally and synced from ConnectWise. All opportunity responses include hydrated company data (address, contacts) fetched from ConnectWise when a linked company exists, as well as an `activities` array containing all ConnectWise activities linked to the opportunity (fetched live from CW at request time). Single-opportunity fetches additionally include full site details (address, phone, flags). Sub-resource routes (products, notes, contacts) fetch live data from ConnectWise using the opportunity's CW ID. ### Get Opportunity Types **GET** `/sales/opportunity-types` Fetch the list of all opportunity quote statuses (types). Returns a static list of canonical quote statuses with their ConnectWise IDs and legacy Optima equivalency mappings. **Authentication Required:** Yes **Required Permissions:** `sales.opportunity.fetch.many` **Response:** ```json { "status": 200, "message": "Opportunity Types Fetched Successfully!", "data": [ { "id": 51, "name": "00. FutureLead", "wonFlag": false, "lostFlag": false, "closedFlag": false, "inactiveFlag": false, "defaultFlag": false, "enteredBy": "crobinso", "dateEntered": "2023-07-11T23:13:19Z", "_info": { "lastUpdated": "2024-04-28T15:03:57Z", "updatedBy": "crobinso" }, "connectWiseId": "070f72a3-70d0-ef11-b2e0-000c29c55070", "optimaEquivalency": [35, 36] } ], "successful": true } ``` --- ### Get All Opportunities **GET** `/sales/opportunities` Fetch a paginated list of opportunities. Supports search. Each opportunity includes hydrated company data (with address and contacts from ConnectWise) when a linked company exists. **Authentication Required:** Yes **Required Permissions:** `sales.opportunity.fetch.many` **Field-Level Gating:** `obj.opportunity` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **Query Parameters:** - `page` (optional, default `1`) — Page number - `rpp` (optional, default `30`) — Records per page - `search` (optional) — Search by opportunity name - `includeClosed` (optional, default `false`) — Include closed opportunities in results **Response:** ```json { "status": 200, "message": "Opportunities fetched successfully!", "data": [ { "id": "clx...", "cwOpportunityId": 456, "name": "Acme Corp Network Refresh", "notes": "Full network redesign and hardware refresh", "type": { "id": 1, "name": "New" }, "stage": { "id": 3, "name": "Proposal" }, "status": { "id": 1, "name": "Open" }, "priority": { "id": 2, "name": "High" }, "rating": { "id": 1, "name": "Hot" }, "source": "Referral", "campaign": null, "primarySalesRep": { "id": 10, "identifier": "JDoe", "name": "John Doe" }, "secondarySalesRep": null, "company": { "id": "clx...", "name": "Acme Corp", "cw_Identifier": "AcmeCorp", "cw_CompanyId": 100, "cw_Data": { "address": { "line1": "123 Main St", "line2": null, "city": "Murray", "state": "Kentucky", "zip": "42071", "country": "United States" }, "allContacts": [ { "firstName": "Jane", "lastName": "Smith", "cwId": 200, "inactive": false, "title": "IT Manager", "phone": "555-0100", "email": "jane.smith@acme.com" } ] } }, "contact": { "id": 200, "name": "Jane Smith" }, "site": { "id": 50, "name": "Main Office" }, "customerPO": null, "totalSalesTax": 0, "location": { "id": 1, "name": "Murray" }, "department": { "id": 5, "name": "Sales" }, "expectedCloseDate": "2026-04-15T00:00:00.000Z", "pipelineChangeDate": "2026-02-20T00:00:00.000Z", "dateBecameLead": "2026-01-10T00:00:00.000Z", "closedDate": null, "closedFlag": false, "closedBy": null, "companyId": "clx...", "cwLastUpdated": "2026-02-26T10:00:00.000Z", "productSequence": [31848, 31846, 31847], "createdAt": "2026-02-01T00:00:00.000Z", "updatedAt": "2026-02-26T10:00:00.000Z", "customFields": [], "activities": [ { "cwActivityId": 789, "name": "Follow-up Call", "notes": "Discuss proposal details", "type": { "id": 1, "name": "Call" }, "status": { "id": 1, "name": "Open" }, "company": { "id": 100, "identifier": "AcmeCorp", "name": "Acme Corp" }, "contact": { "id": 200, "name": "Jane Smith" }, "phoneNumber": "555-0100", "email": "jane.smith@acme.com", "opportunity": { "id": 456, "name": "Acme Corp Network Refresh" }, "ticket": null, "agreement": null, "campaign": null, "assignTo": { "id": 10, "identifier": "JDoe", "name": "John Doe" }, "scheduleStatus": null, "reminder": null, "where": null, "dateStart": "2026-03-01T10:00:00.000Z", "dateEnd": "2026-03-01T10:30:00.000Z", "notifyFlag": false, "currency": null, "mobileGuid": null, "customFields": [], "cwLastUpdated": "2026-02-26T10:00:00.000Z", "cwDateEntered": "2026-02-20T09:00:00.000Z", "cwEnteredBy": "JDoe", "cwUpdatedBy": "JDoe" } ] } ], "meta": { "pagination": { "previousPage": null, "currentPage": 1, "nextPage": 2, "totalPages": 5, "totalRecords": 150, "listedRecords": 30 } }, "successful": true } ``` --- ### Get Opportunity Count **GET** `/sales/opportunities/count` Get the total number of opportunities. **Authentication Required:** Yes **Required Permissions:** `sales.opportunity.fetch.many` **Query Parameters:** - `openOnly` (optional, default `false`) — Only count open (non-closed) opportunities **Response:** ```json { "status": 200, "message": "Opportunity count fetched successfully!", "data": { "count": 150 }, "successful": true } ``` --- ### Get Opportunity **GET** `/sales/opportunities/:identifier` Fetch a single opportunity by its internal ID or ConnectWise opportunity ID. The response includes hydrated company data (with address and contacts from ConnectWise) and full site details (with address) when available. **Authentication Required:** Yes **Required Permissions:** `sales.opportunity.fetch` **Field-Level Gating:** `obj.opportunity` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **Path Parameters:** - `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric) **Response:** ```json { "status": 200, "message": "Opportunity fetched successfully!", "data": { "id": "clx...", "cwOpportunityId": 456, "name": "Acme Corp Network Refresh", "notes": "Full network redesign and hardware refresh", "type": { "id": 1, "name": "New" }, "stage": { "id": 3, "name": "Proposal" }, "status": { "id": 1, "name": "Open" }, "priority": { "id": 2, "name": "High" }, "rating": { "id": 1, "name": "Hot" }, "source": "Referral", "campaign": null, "primarySalesRep": { "id": 10, "identifier": "JDoe", "name": "John Doe" }, "secondarySalesRep": null, "company": { "id": "clx...", "name": "Acme Corp", "cw_Identifier": "AcmeCorp", "cw_CompanyId": 100, "cw_Data": { "address": { "line1": "123 Main St", "line2": null, "city": "Murray", "state": "Kentucky", "zip": "42071", "country": "United States" }, "allContacts": [ { "firstName": "Jane", "lastName": "Smith", "cwId": 200, "inactive": false, "title": "IT Manager", "phone": "555-0100", "email": "jane.smith@acme.com" } ] } }, "contact": { "id": 200, "name": "Jane Smith" }, "site": { "id": 50, "name": "Main Office", "address": { "line1": "123 Main St", "line2": null, "city": "Murray", "state": "Kentucky", "zip": "42071", "country": "United States" }, "phoneNumber": "555-0100", "faxNumber": null, "primaryAddressFlag": true, "defaultShippingFlag": true, "defaultBillingFlag": true, "defaultMailingFlag": true }, "customerPO": null, "totalSalesTax": 0, "location": { "id": 1, "name": "Murray" }, "department": { "id": 5, "name": "Sales" }, "expectedCloseDate": "2026-04-15T00:00:00.000Z", "pipelineChangeDate": "2026-02-20T00:00:00.000Z", "dateBecameLead": "2026-01-10T00:00:00.000Z", "closedDate": null, "closedFlag": false, "closedBy": null, "companyId": "clx...", "cwLastUpdated": "2026-02-26T10:00:00.000Z", "productSequence": [31848, 31846, 31847], "createdAt": "2026-02-01T00:00:00.000Z", "updatedAt": "2026-02-26T10:00:00.000Z", "customFields": [], "activities": [ { "cwActivityId": 789, "name": "Follow-up Call", "notes": "Discuss proposal details", "type": { "id": 1, "name": "Call" }, "status": { "id": 1, "name": "Open" }, "company": { "id": 100, "identifier": "AcmeCorp", "name": "Acme Corp" }, "contact": { "id": 200, "name": "Jane Smith" }, "phoneNumber": "555-0100", "email": "jane.smith@acme.com", "opportunity": { "id": 456, "name": "Acme Corp Network Refresh" }, "ticket": null, "agreement": null, "campaign": null, "assignTo": { "id": 10, "identifier": "JDoe", "name": "John Doe" }, "scheduleStatus": null, "reminder": null, "where": null, "dateStart": "2026-03-01T10:00:00.000Z", "dateEnd": "2026-03-01T10:30:00.000Z", "notifyFlag": false, "currency": null, "mobileGuid": null, "customFields": [], "cwLastUpdated": "2026-02-26T10:00:00.000Z", "cwDateEntered": "2026-02-20T09:00:00.000Z", "cwEnteredBy": "JDoe", "cwUpdatedBy": "JDoe" } ] }, "successful": true } ``` --- ### Refresh Opportunity **POST** `/sales/opportunities/:identifier/refresh` Refresh an opportunity's local data by fetching the latest from ConnectWise. The response includes hydrated company data and site details. **Authentication Required:** Yes **Required Permissions:** `sales.opportunity.refresh` **Path Parameters:** - `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric) **Response:** ```json { "status": 200, "message": "Opportunity refreshed from ConnectWise successfully!", "data": { "id": "clx...", "cwOpportunityId": 456, "name": "Acme Corp Network Refresh", "notes": "Updated notes from CW", "type": { "id": 1, "name": "New" }, "stage": { "id": 4, "name": "Negotiation" }, "status": { "id": 1, "name": "Open" }, "priority": { "id": 2, "name": "High" }, "rating": { "id": 1, "name": "Hot" }, "source": "Referral", "campaign": null, "primarySalesRep": { "id": 10, "identifier": "JDoe", "name": "John Doe" }, "secondarySalesRep": null, "company": { "id": "clx...", "name": "Acme Corp", "cw_Identifier": "AcmeCorp", "cw_CompanyId": 100, "cw_Data": { "address": { "line1": "123 Main St", "line2": null, "city": "Murray", "state": "Kentucky", "zip": "42071", "country": "United States" }, "allContacts": [ { "firstName": "Jane", "lastName": "Smith", "cwId": 200, "inactive": false, "title": "IT Manager", "phone": "555-0100", "email": "jane.smith@acme.com" } ] } }, "contact": { "id": 200, "name": "Jane Smith" }, "site": { "id": 50, "name": "Main Office" }, "customerPO": null, "totalSalesTax": 0, "location": { "id": 1, "name": "Murray" }, "department": { "id": 5, "name": "Sales" }, "expectedCloseDate": "2026-04-15T00:00:00.000Z", "pipelineChangeDate": "2026-02-25T00:00:00.000Z", "dateBecameLead": "2026-01-10T00:00:00.000Z", "closedDate": null, "closedFlag": false, "closedBy": null, "companyId": "clx...", "cwLastUpdated": "2026-02-26T14:00:00.000Z", "createdAt": "2026-02-01T00:00:00.000Z", "updatedAt": "2026-02-26T14:00:00.000Z", "customFields": [], "activities": [ { "cwActivityId": 789, "name": "Follow-up Call", "notes": "Discuss proposal details", "type": { "id": 1, "name": "Call" }, "status": { "id": 1, "name": "Open" }, "company": { "id": 100, "identifier": "AcmeCorp", "name": "Acme Corp" }, "contact": { "id": 200, "name": "Jane Smith" }, "phoneNumber": "555-0100", "email": "jane.smith@acme.com", "opportunity": { "id": 456, "name": "Acme Corp Network Refresh" }, "ticket": null, "agreement": null, "campaign": null, "assignTo": { "id": 10, "identifier": "JDoe", "name": "John Doe" }, "scheduleStatus": null, "reminder": null, "where": null, "dateStart": "2026-03-01T10:00:00.000Z", "dateEnd": "2026-03-01T10:30:00.000Z", "notifyFlag": false, "currency": null, "mobileGuid": null, "customFields": [], "cwLastUpdated": "2026-02-26T10:00:00.000Z", "cwDateEntered": "2026-02-20T09:00:00.000Z", "cwEnteredBy": "JDoe", "cwUpdatedBy": "JDoe" } ] }, "successful": true } ``` --- ### Get Opportunity Products **GET** `/sales/opportunities/:identifier/products` Fetch products (forecast/revenue line items) for an opportunity. Data is fetched live from ConnectWise using the opportunity's CW ID. Products are returned sorted by the opportunity's local `productSequence` array when set; otherwise, items are sorted by their ConnectWise `sequenceNumber`. **Authentication Required:** Yes **Required Permissions:** `sales.opportunity.fetch` **Path Parameters:** - `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric) **Response:** ```json { "status": 200, "message": "Opportunity products fetched successfully!", "data": [ { "id": 31846, "forecastDescription": "Service", "opportunity": { "id": 5150, "name": "Example Opportunity" }, "quantity": 1, "status": { "id": 24, "name": "01. New" }, "cancelled": false, "cancellationType": null, "quantityCancelled": 0, "cancelledReason": null, "cancelledDate": null, "catalogItem": { "id": 3756, "identifier": "Labor & Installation - Field" }, "productDescription": "Labor & Installation - Field", "productClass": "Service", "forecastType": "Service", "revenue": 650000, "cost": 0, "margin": 650000, "profit": 650000, "percentage": 100, "includeFlag": true, "linkFlag": true, "recurringFlag": false, "taxableFlag": true, "recurringRevenue": 0, "recurringCost": 0, "cycles": 0, "sequenceNumber": 1, "subNumber": 0, "cwLastUpdated": "2026-02-28T20:57:52.000Z", "cwUpdatedBy": "jroberts", "onHand": 12, "inStock": true } ], "successful": true } ``` **Cancellation Fields:** Product cancellation data is sourced from the ConnectWise procurement products endpoint (not the forecast endpoint). Each product includes: | Field | Type | Description | | ------------------- | ----------- | --------------------------------------------------------------------------------------- | | `cancelled` | boolean | Whether the product has been cancelled (fully or partially) | | `cancellationType` | string/null | `"full"` if all units cancelled, `"partial"` if some cancelled, `null` if not cancelled | | `quantityCancelled` | number | Number of units cancelled | | `cancelledReason` | string/null | Reason for cancellation (if provided) | | `cancelledDate` | string/null | ISO 8601 timestamp of when the item was cancelled | **Inventory Fields:** Internal inventory data is sourced from the local CatalogItem database. If the product's catalog item exists locally, these fields are populated; otherwise they are `null`. | Field | Type | Description | | --------- | ------------ | ---------------------------------------------------- | | `onHand` | number/null | Number of units currently on hand in local inventory | | `inStock` | boolean/null | Whether the item is in stock (`onHand > 0`) | --- ### Resequence Opportunity Products **PATCH** `/sales/opportunities/:identifier/products/sequence` Update the display order of products (forecast items) on an opportunity. The sequence is stored **locally** in the database (`productSequence` field on the Opportunity model) — no modifications are made to ConnectWise. This means forecast item IDs remain stable and procurement product linkages are unaffected. When a `productSequence` is set, `GET .../products` returns items in that order. Any forecast items not included in the array (e.g. newly added items) are appended at the end in CW `sequenceNumber` order. **Authentication Required:** Yes **Required Permissions:** `sales.opportunity.product.update` **Path Parameters:** - `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric) **Request Body:** ```json { "orderedIds": [31848, 31846, 31847] } ``` - `orderedIds` — Array of CW forecast item IDs in the desired display order. All IDs must exist on the opportunity's forecast in ConnectWise. **Response:** ```json { "status": 200, "message": "Product sequence updated successfully!", "data": { "products": [ { "id": 31848, "forecastDescription": "Hardware", "sequenceNumber": 3, "..." }, { "id": 31846, "forecastDescription": "Service", "sequenceNumber": 1, "..." }, { "id": 31847, "forecastDescription": "Licensing", "sequenceNumber": 2, "..." } ] }, "successful": true } ``` - `data.products` — Full product objects in the new display order. IDs are unchanged — CW `sequenceNumber` still reflects the original CW order, but the array order matches the locally stored sequence. --- ### Add Product to Opportunity **POST** `/sales/opportunities/:identifier/products` Add a new product (forecast item) to an opportunity in ConnectWise. The request body is validated with Zod, then each submitted field is gated against `sales.opportunity.product.field.` permissions — only fields the user has permission for are forwarded to ConnectWise. **Authentication Required:** Yes **Required Permissions:** `sales.opportunity.product.add` **Field-Level Permission Gating:** Yes — uses `processObjectValuePerms` with scope `sales.opportunity.product.field` on the **input body**. See the field-level permissions table under `sales.opportunity.product.add` in PERMISSIONS.md. **Path Parameters:** - `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric) **Request Body:** All fields are optional. Only fields the user has the corresponding `sales.opportunity.product.field.` permission for will be sent to ConnectWise. ```json { "catalogItem": { "id": 1234 }, "forecastDescription": "Managed Services", "productDescription": "Monthly managed services agreement", "quantity": 1, "status": { "id": 1 }, "productClass": "Agreement", "forecastType": "Product", "revenue": 500.0, "cost": 250.0, "includeFlag": true, "linkFlag": false, "recurringFlag": true, "taxableFlag": true, "recurringRevenue": 500.0, "recurringCost": 250.0, "cycles": 12, "sequenceNumber": 1 } ``` | Field | Type | Description | | --------------------- | ---------------- | ------------------------------------------------ | | `catalogItem` | `{ id: number }` | ConnectWise catalog item reference | | `forecastDescription` | string | Forecast description text | | `productDescription` | string | Product description text | | `quantity` | number | Quantity (must be positive) | | `status` | `{ id: number }` | ConnectWise status reference | | `productClass` | string | Product class (e.g. Product, Service, Agreement) | | `forecastType` | string | Forecast type | | `revenue` | number | Revenue amount | | `cost` | number | Cost amount | | `includeFlag` | boolean | Whether to include in forecast totals | | `linkFlag` | boolean | Whether the item is linked | | `recurringFlag` | boolean | Whether this is a recurring item | | `taxableFlag` | boolean | Whether this item is taxable | | `recurringRevenue` | number | Recurring revenue amount | | `recurringCost` | number | Recurring cost amount | | `cycles` | number | Number of recurring cycles (integer, min 0) | | `sequenceNumber` | number | Display sequence number (integer, min 0) | **Response:** ```json { "status": 201, "message": "Product added to opportunity successfully!", "data": { "id": 31855, "forecastDescription": "Managed Services", "opportunity": { "id": 5678, "name": "Example Opportunity" }, "quantity": 1, "status": { "id": 1, "name": "Open" }, "catalogItem": { "id": 1234, "identifier": "MSP-001" }, "productDescription": "Monthly managed services agreement", "productClass": "Agreement", "forecastType": "Product", "revenue": 500.0, "cost": 250.0, "margin": 250.0, "profit": 250.0, "percentage": 0, "includeFlag": true, "linkFlag": false, "recurringFlag": true, "taxableFlag": true, "recurringRevenue": 500.0, "recurringCost": 250.0, "cycles": 12, "sequenceNumber": 1, "subNumber": 0, "cwLastUpdated": "2026-03-01T00:00:00.000Z", "cwUpdatedBy": "Admin1", "cancelled": false, "cancellationType": null, "quantityCancelled": 0, "cancelledReason": null, "cancelledDate": null, "onHand": null, "inStock": null }, "successful": true } ``` --- ### Get Opportunity Notes **GET** `/sales/opportunities/:identifier/notes` Fetch notes for an opportunity. Data is fetched live from ConnectWise using the opportunity's CW ID. **Authentication Required:** Yes **Required Permissions:** `sales.opportunity.fetch` **Path Parameters:** - `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric) **Response:** ```json { "status": 200, "message": "Opportunity notes fetched successfully!", "data": [ { "id": 1, "text": "Client expressed interest in a full network refresh.", "type": { "id": 2, "name": "Discussion" }, "flagged": false, "enteredBy": { "id": "clx1abc123", "identifier": "jdoe", "name": "John Doe", "cwMemberId": 10 } } ], "successful": true } ``` --- ### Get Single Opportunity Note **GET** `/sales/opportunities/:identifier/notes/:noteId` Fetch a single note by its ConnectWise note ID for an opportunity. **Authentication Required:** Yes **Required Permissions:** `sales.opportunity.fetch` **Path Parameters:** - `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric) - `noteId` — The ConnectWise note ID (numeric) **Response:** ```json { "status": 200, "message": "Opportunity note fetched successfully!", "data": { "id": 1, "text": "Client expressed interest in a full network refresh.", "type": { "id": 2, "name": "Discussion" }, "flagged": false, "enteredBy": { "id": "clx1abc123", "identifier": "jdoe", "name": "John Doe", "cwMemberId": 10 } }, "successful": true } ``` --- ### Create Opportunity Note **POST** `/sales/opportunities/:identifier/notes` Create a new note on an opportunity in ConnectWise. **Authentication Required:** Yes **Required Permissions:** `sales.opportunity.note.create` **Path Parameters:** - `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric) **Request Body:** ```json { "text": "Follow up with client about pricing.", "flagged": false } ``` | Field | Type | Required | Description | | --------- | ------- | -------- | ------------------------------- | | `text` | string | Yes | The note text (min 1 character) | | `flagged` | boolean | No | Whether the note is flagged | **Response:** ```json { "status": 201, "message": "Opportunity note created successfully!", "data": { "id": 42, "text": "Follow up with client about pricing.", "type": null, "flagged": false, "enteredBy": { "id": "clx2def456", "identifier": "jroberts", "name": "John Roberts", "cwMemberId": 15 } }, "successful": true } ``` --- ### Update Opportunity Note **PATCH** `/sales/opportunities/:identifier/notes/:noteId` Update an existing note on an opportunity in ConnectWise. **Authentication Required:** Yes **Required Permissions:** `sales.opportunity.note.update` **Path Parameters:** - `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric) - `noteId` — The ConnectWise note ID (numeric) **Request Body:** ```json { "text": "Updated note text.", "flagged": true } ``` | Field | Type | Required | Description | | --------- | ------- | -------- | ----------------------------------- | | `text` | string | No | Updated note text (min 1 character) | | `flagged` | boolean | No | Updated flagged state | > At least one of `text` or `flagged` must be provided. **Response:** ```json { "status": 200, "message": "Opportunity note updated successfully!", "data": { "id": 42, "text": "Updated note text.", "type": null, "flagged": true, "enteredBy": { "id": "clx2def456", "identifier": "jroberts", "name": "John Roberts", "cwMemberId": 15 } }, "successful": true } ``` --- ### Delete Opportunity Note **DELETE** `/sales/opportunities/:identifier/notes/:noteId` Delete a note from an opportunity in ConnectWise. **Authentication Required:** Yes **Required Permissions:** `sales.opportunity.note.delete` **Path Parameters:** - `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric) - `noteId` — The ConnectWise note ID (numeric) **Response:** ```json { "status": 200, "message": "Opportunity note deleted successfully!", "successful": true } ``` --- ### Get Opportunity Contacts **GET** `/sales/opportunities/:identifier/contacts` Fetch contacts associated with an opportunity. Data is fetched live from ConnectWise using the opportunity's CW ID. **Authentication Required:** Yes **Required Permissions:** `sales.opportunity.fetch` **Path Parameters:** - `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric) **Response:** ```json { "status": 200, "message": "Opportunity contacts fetched successfully!", "data": [ { "id": 1, "contact": { "id": 200, "name": "Jane Smith" }, "company": { "id": 100, "identifier": "AcmeCorp", "name": "Acme Corp" }, "role": { "id": 1, "name": "Decision Maker" }, "notes": "Primary point of contact for this deal", "referralFlag": false } ], "successful": true } ``` --- ## UniFi Routes All UniFi routes require the `unifi.access` permission in addition to their route-specific permission. This acts as a gate for the entire UniFi API. ### Get All UniFi Sites **GET** `/unifi/sites` Fetch all UniFi site records from the database. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.sites.fetch.many` **Field-Level Gating:** `obj.unifiSite` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **Response:** ```json { "status": 200, "message": "UniFi Sites Fetched Successfully!", "data": [ { "id": "ckx...", "name": "Total Tech - Murray Office", "siteId": "km9b1v8i", "companyId": "ckx...", "company": { "id": "ckx...", "name": "Acme Corp" }, "createdAt": "2025-01-01T00:00:00.000Z", "updatedAt": "2025-01-01T00:00:00.000Z" } ], "successful": true } ``` --- ### Sync UniFi Sites **POST** `/unifi/sites/sync` Synchronize sites from the UniFi controller into the database. Creates new records for sites not yet tracked and updates names for existing ones. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.sites.sync` **Response:** ```json { "status": 200, "message": "UniFi Sites Synced Successfully!", "data": [ { "id": "ckx...", "name": "Total Tech - Murray Office", "siteId": "km9b1v8i", "companyId": null, "createdAt": "2025-01-01T00:00:00.000Z", "updatedAt": "2025-01-01T00:00:00.000Z" } ], "successful": true } ``` --- ### Create UniFi Site **POST** `/unifi/sites/create` Create a new site on the UniFi controller and track it in the database. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.sites.create` **Request Body:** ```json { "description": "New Office Site" } ``` | Field | Type | Required | Description | | ------------- | ------ | -------- | ---------------------------------------------- | | `description` | string | Yes | Human-readable name / description for the site | **Response:** ```json { "status": 200, "message": "UniFi Site Created Successfully!", "data": { "id": "ckx...", "name": "New Office Site", "siteId": "abc123", "companyId": null, "createdAt": "2025-01-01T00:00:00.000Z", "updatedAt": "2025-01-01T00:00:00.000Z" }, "successful": true } ``` --- ### Get UniFi Site **GET** `/unifi/site/:id` Fetch a single UniFi site record from the database by its internal ID. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.sites.fetch` **Field-Level Gating:** `obj.unifiSite` — see [Object Type Field-Level Gating](#object-type-field-level-gating) **URL Parameters:** - `id` - Internal UniFi site ID (database ID) **Response:** ```json { "status": 200, "message": "UniFi Site Fetched Successfully!", "data": { "id": "ckx...", "name": "Total Tech - Murray Office", "siteId": "km9b1v8i", "companyId": "ckx...", "createdAt": "2025-01-01T00:00:00.000Z", "updatedAt": "2025-01-01T00:00:00.000Z" }, "successful": true } ``` --- ### Get Site Overview **GET** `/unifi/site/:id/overview` Fetch live site overview data from the UniFi controller, including health status, system info, and site information. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.site.overview` **URL Parameters:** - `id` - Internal UniFi site ID (database ID) **Response:** ```json { "status": 200, "message": "UniFi Site Overview Fetched Successfully!", "data": { "health": [ { "subsystem": "wan", "status": "ok", "numAdopted": 1, "numGateway": 1 } ], "sysInfo": { "timezone": "America/Denver", "hostname": "UniFi-Controller", "version": "8.x.x" }, "siteInfo": { "description": "Total Tech - Murray Office", "name": "km9b1v8i" } }, "successful": true } ``` --- ### Get Site Devices **GET** `/unifi/site/:id/devices` Fetch live device list from the UniFi controller for a specific site. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.site.devices` **URL Parameters:** - `id` - Internal UniFi site ID (database ID) **Response:** ```json { "status": 200, "message": "UniFi Devices Fetched Successfully!", "data": [ { "id": "abc123...", "mac": "00:11:22:33:44:55", "model": "U6-Pro", "name": "Office AP", "type": "uap", "state": "connected", "ip": "192.168.1.10", "version": "6.x.x", "uptime": 123456, "radios": [], "uplink": {} } ], "successful": true } ``` --- ### Get Site WiFi Networks **GET** `/unifi/site/:id/wifi` Fetch live WiFi network (WLAN) configurations from the UniFi controller for a specific site. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.site.wifi` **Field-Level Gating:** `unifi.site.wifi.read.` This route uses `processObjectValuePerms` to filter each WLAN object on a per-field basis. Only fields whose corresponding `unifi.site.wifi.read.` permission the user holds are included in the response. For example, a user with `unifi.site.wifi.read.name` and `unifi.site.wifi.read.enabled` but _without_ `unifi.site.wifi.read.passphrase` will receive objects containing only `name` and `enabled`. Use `unifi.site.wifi.read.*` to grant access to all fields.
All available field-level permission nodes | Permission Node | Field | | -------------------------------------------------- | ----------------------------- | | `unifi.site.wifi.read.id` | `id` | | `unifi.site.wifi.read.name` | `name` | | `unifi.site.wifi.read.siteId` | `siteId` | | `unifi.site.wifi.read.enabled` | `enabled` | | `unifi.site.wifi.read.security` | `security` | | `unifi.site.wifi.read.wpaMode` | `wpaMode` | | `unifi.site.wifi.read.wpaEnc` | `wpaEnc` | | `unifi.site.wifi.read.wpa3Support` | `wpa3Support` | | `unifi.site.wifi.read.wpa3Transition` | `wpa3Transition` | | `unifi.site.wifi.read.wpa3FastRoaming` | `wpa3FastRoaming` | | `unifi.site.wifi.read.wpa3Enhanced192` | `wpa3Enhanced192` | | `unifi.site.wifi.read.passphrase` | `passphrase` | | `unifi.site.wifi.read.passphraseAutogenerated` | `passphraseAutogenerated` | | `unifi.site.wifi.read.hideSSID` | `hideSSID` | | `unifi.site.wifi.read.isGuest` | `isGuest` | | `unifi.site.wifi.read.band` | `band` | | `unifi.site.wifi.read.bands` | `bands` | | `unifi.site.wifi.read.networkconfId` | `networkconfId` | | `unifi.site.wifi.read.usergroupId` | `usergroupId` | | `unifi.site.wifi.read.apGroupIds` | `apGroupIds` | | `unifi.site.wifi.read.apGroupMode` | `apGroupMode` | | `unifi.site.wifi.read.pmfMode` | `pmfMode` | | `unifi.site.wifi.read.groupRekey` | `groupRekey` | | `unifi.site.wifi.read.dtimMode` | `dtimMode` | | `unifi.site.wifi.read.dtimNg` | `dtimNg` | | `unifi.site.wifi.read.dtimNa` | `dtimNa` | | `unifi.site.wifi.read.dtim6e` | `dtim6e` | | `unifi.site.wifi.read.l2Isolation` | `l2Isolation` | | `unifi.site.wifi.read.fastRoamingEnabled` | `fastRoamingEnabled` | | `unifi.site.wifi.read.bssTransition` | `bssTransition` | | `unifi.site.wifi.read.uapsdEnabled` | `uapsdEnabled` | | `unifi.site.wifi.read.iappEnabled` | `iappEnabled` | | `unifi.site.wifi.read.proxyArp` | `proxyArp` | | `unifi.site.wifi.read.mcastenhanceEnabled` | `mcastenhanceEnabled` | | `unifi.site.wifi.read.macFilterEnabled` | `macFilterEnabled` | | `unifi.site.wifi.read.macFilterPolicy` | `macFilterPolicy` | | `unifi.site.wifi.read.macFilterList` | `macFilterList` | | `unifi.site.wifi.read.radiusDasEnabled` | `radiusDasEnabled` | | `unifi.site.wifi.read.radiusMacAuthEnabled` | `radiusMacAuthEnabled` | | `unifi.site.wifi.read.radiusMacaclFormat` | `radiusMacaclFormat` | | `unifi.site.wifi.read.minrateSettingPreference` | `minrateSettingPreference` | | `unifi.site.wifi.read.minrateNgEnabled` | `minrateNgEnabled` | | `unifi.site.wifi.read.minrateNgDataRateKbps` | `minrateNgDataRateKbps` | | `unifi.site.wifi.read.minrateNgAdvertisingRates` | `minrateNgAdvertisingRates` | | `unifi.site.wifi.read.minrateNaEnabled` | `minrateNaEnabled` | | `unifi.site.wifi.read.minrateNaDataRateKbps` | `minrateNaDataRateKbps` | | `unifi.site.wifi.read.minrateNaAdvertisingRates` | `minrateNaAdvertisingRates` | | `unifi.site.wifi.read.settingPreference` | `settingPreference` | | `unifi.site.wifi.read.no2ghzOui` | `no2ghzOui` | | `unifi.site.wifi.read.privatePreSharedKeysEnabled` | `privatePreSharedKeysEnabled` | | `unifi.site.wifi.read.privatePreSharedKeys` | `privatePreSharedKeys` | | `unifi.site.wifi.read.saeGroups` | `saeGroups` | | `unifi.site.wifi.read.saePsk` | `saePsk` | | `unifi.site.wifi.read.schedule` | `schedule` | | `unifi.site.wifi.read.scheduleWithDuration` | `scheduleWithDuration` | | `unifi.site.wifi.read.bcFilterList` | `bcFilterList` | | `unifi.site.wifi.read.externalId` | `externalId` |
**URL Parameters:** - `id` - Internal UniFi site ID (database ID) **Response:** ```json { "status": 200, "message": "UniFi WiFi Networks Fetched Successfully!", "data": [ { "id": "66eb36e54bb53f0ae3fb82bd", "name": "TTAD", "siteId": "61ae856a8eae567c3905c54f", "enabled": true, "security": "wpapsk", "wpaMode": "wpa2", "wpaEnc": "ccmp", "wpa3Support": false, "wpa3Transition": false, "wpa3FastRoaming": false, "wpa3Enhanced192": false, "passphrase": "S3cur3Alarm!", "passphraseAutogenerated": false, "hideSSID": false, "isGuest": false, "band": "2g", "bands": ["2g"], "networkconfId": "66eb368d4bb53f0ae3fb7e0d", "usergroupId": "61ae856a8eae567c3905c55c", "apGroupIds": ["66eb36e44bb53f0ae3fb82bc"], "apGroupMode": "devices", "pmfMode": "disabled", "groupRekey": 0, "dtimMode": "default", "dtimNg": 1, "dtimNa": 3, "dtim6e": 3, "l2Isolation": false, "fastRoamingEnabled": false, "bssTransition": true, "uapsdEnabled": false, "iappEnabled": true, "proxyArp": false, "mcastenhanceEnabled": false, "macFilterEnabled": false, "macFilterPolicy": "allow", "macFilterList": [], "radiusDasEnabled": false, "radiusMacAuthEnabled": false, "radiusMacaclFormat": "none_lower", "minrateSettingPreference": "auto", "minrateNgEnabled": true, "minrateNgDataRateKbps": 1000, "minrateNgAdvertisingRates": false, "minrateNaEnabled": false, "minrateNaDataRateKbps": 6000, "minrateNaAdvertisingRates": false, "settingPreference": "manual", "no2ghzOui": true, "privatePreSharedKeysEnabled": false, "privatePreSharedKeys": [], "saeGroups": [], "saePsk": [], "schedule": [], "scheduleWithDuration": [], "bcFilterList": [], "externalId": "ba0f579c-6b21-4dfc-8d03-5a1a09243328" } ], "successful": true } ``` --- ### Update WiFi Network **PATCH** `/unifi/site/:id/wifi/:wlanId` Update a WiFi network configuration on the UniFi controller. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.site.wifi`, `unifi.site.wifi.update` **URL Parameters:** - `id` - Internal UniFi site ID (database ID) - `wlanId` - UniFi WLAN ID **Request Body:** ```json { "name": "NewSSIDName", "x_passphrase": "NewPassword123", "enabled": true, "security": "wpapsk", "wpa_mode": "wpa2", "hide_ssid": false, "is_guest": false, "band": "both" } ``` All fields are optional. Valid values: - `security`: `"wpapsk"`, `"wpaeap"`, `"open"` - `wpa_mode`: `"wpa2"`, `"wpa3"`, `"wpa2wpa3"` - `band`: `"both"`, `"2g"`, `"5g"` **Response:** ```json { "status": 200, "message": "UniFi WiFi Network Updated Successfully!", "data": { "id": "66eb36e54bb53f0ae3fb82bd", "name": "NewSSIDName", "siteId": "61ae856a8eae567c3905c54f", "enabled": true, "security": "wpapsk", "wpaMode": "wpa2", "wpaEnc": "ccmp", "wpa3Support": false, "wpa3Transition": false, "wpa3FastRoaming": false, "wpa3Enhanced192": false, "passphrase": "NewPassword123", "passphraseAutogenerated": false, "hideSSID": false, "isGuest": false, "band": "both", "bands": ["2g", "5g"], "networkconfId": "66eb368d4bb53f0ae3fb7e0d", "usergroupId": "61ae856a8eae567c3905c55c", "apGroupIds": ["66eb36e44bb53f0ae3fb82bc"], "apGroupMode": "devices", "pmfMode": "disabled", "groupRekey": 0, "dtimMode": "default", "dtimNg": 1, "dtimNa": 3, "dtim6e": 3, "l2Isolation": false, "fastRoamingEnabled": false, "bssTransition": true, "uapsdEnabled": false, "iappEnabled": true, "proxyArp": false, "mcastenhanceEnabled": false, "macFilterEnabled": false, "macFilterPolicy": "allow", "macFilterList": [], "radiusDasEnabled": false, "radiusMacAuthEnabled": false, "radiusMacaclFormat": "none_lower", "minrateSettingPreference": "auto", "minrateNgEnabled": true, "minrateNgDataRateKbps": 1000, "minrateNgAdvertisingRates": false, "minrateNaEnabled": false, "minrateNaDataRateKbps": 6000, "minrateNaAdvertisingRates": false, "settingPreference": "manual", "no2ghzOui": true, "privatePreSharedKeysEnabled": false, "privatePreSharedKeys": [], "saeGroups": [], "saePsk": [], "schedule": [], "scheduleWithDuration": [], "bcFilterList": [], "externalId": "ba0f579c-6b21-4dfc-8d03-5a1a09243328" }, "successful": true } ``` --- ### Get Site Networks **GET** `/unifi/site/:id/networks` Fetch live network configurations from the UniFi controller for a specific site. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.site.networks` **URL Parameters:** - `id` - Internal UniFi site ID (database ID) **Response:** ```json { "status": 200, "message": "UniFi Networks Fetched Successfully!", "data": [ { "id": "abc123...", "name": "LAN", "purpose": "corporate", "subnet": "192.168.1.0/24", "vlanId": null, "dhcpEnabled": true, "dhcpStart": "192.168.1.100", "dhcpStop": "192.168.1.254", "domainName": "localdomain", "isNat": true, "enabled": true } ], "successful": true } ``` --- ### Get WLAN Groups **GET** `/unifi/site/:id/wlan-groups` Fetch WLAN groups (AP groups) from the UniFi controller for a specific site. WLAN groups define which WLANs are broadcast on which access points. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.site.wlan-groups` **URL Parameters:** - `id` - Internal UniFi site ID (database ID) **Response:** ```json { "status": 200, "message": "UniFi WLAN Groups Fetched Successfully!", "data": [ { "id": "61ae856a8eae567c3905c55d", "name": "Default", "siteId": "61ae856a8eae567c3905c550", "noDelete": true, "noEdit": false, "hidden": false } ], "successful": true } ``` --- ### Create WLAN Group **POST** `/unifi/site/:id/wlan-groups` Create a new WLAN group (AP broadcasting group) on the UniFi controller. WLAN groups control which WLANs are broadcast on which access points. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.site.wlan-groups`, `unifi.site.wlan-groups.create` **URL Parameters:** - `id` - Internal UniFi site ID (database ID) **Request Body:** ```json { "name": "Lobby APs" } ``` | Field | Type | Required | Description | | ------ | ------ | -------- | ---------------------- | | `name` | string | Yes | Name of the WLAN group | **Response (201):** ```json { "status": 201, "message": "UniFi WLAN Group Created Successfully!", "data": { "id": "abc123...", "name": "Lobby APs", "siteId": "61ae856a8eae567c3905c550", "noDelete": false, "noEdit": false, "hidden": false }, "successful": true } ``` --- ### Get AP Groups **GET** `/unifi/site/:id/ap-groups` Fetch AP groups from the UniFi controller for a specific site. AP groups define collections of access points — individual WLANs can target specific AP groups to control which APs broadcast a given SSID. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.site.ap-groups` **URL Parameters:** - `id` - Internal UniFi site ID (database ID) **Response:** ```json { "status": 200, "message": "UniFi AP Groups Fetched Successfully!", "data": [ { "id": "61ae856a8eae567c3905c565", "name": "All APs", "deviceMacs": [ "18:e8:29:59:25:bc", "78:45:58:29:fa:87", "68:d7:9a:73:9a:28" ], "noDelete": true }, { "id": "63c5be017e957d08189ed997", "name": "301Andrus", "deviceMacs": ["78:45:58:29:fa:87", "18:e8:29:59:25:bc"], "noDelete": false } ], "successful": true } ``` --- ### Get Access Points **GET** `/unifi/site/:id/access-points` Fetch access points (UAPs only) from the UniFi controller for a specific site. This filters the full device list to only return wireless access points. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.site.access-points` **URL Parameters:** - `id` - Internal UniFi site ID (database ID) **Response:** ```json { "status": 200, "message": "UniFi Access Points Fetched Successfully!", "data": [ { "id": "abc123...", "mac": "00:11:22:33:44:55", "model": "U6-Pro", "type": "uap", "name": "Office AP", "state": 1, "adopted": true, "ip": "192.168.1.10", "version": "7.1.68" } ], "successful": true } ``` --- ### Get WiFi Limits **GET** `/unifi/site/:id/wifi-limits` Check the WiFi SSID limits per access point per radio band. UniFi access points support a maximum of 8 SSIDs per radio. This endpoint shows how many SSIDs are active on each radio and how many more can be added. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.site.wifi-limits` **URL Parameters:** - `id` - Internal UniFi site ID (database ID) **Response:** ```json { "status": 200, "message": "UniFi WiFi Limits Fetched Successfully!", "data": [ { "apId": "abc123...", "apName": "Office AP", "mac": "00:11:22:33:44:55", "model": "U6-Pro", "radios": [ { "radio": "ng", "band": "2g", "activeWlans": 3, "limit": 8, "remaining": 5, "wlanNames": ["Corporate", "Guest", "IoT"] }, { "radio": "na", "band": "5g", "activeWlans": 3, "limit": 8, "remaining": 5, "wlanNames": ["Corporate", "Guest", "IoT"] } ] } ], "successful": true } ``` --- ### Get Speed Profiles **GET** `/unifi/site/:id/speed-profiles` Fetch speed limit profiles (user groups) from the UniFi controller for a specific site. Speed profiles define bandwidth limits that can be applied to WiFi networks. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.site.speed-profiles` **URL Parameters:** - `id` - Internal UniFi site ID (database ID) **Response:** ```json { "status": 200, "message": "UniFi Speed Profiles Fetched Successfully!", "data": [ { "id": "61ae856a8eae567c3905c55e", "name": "Default", "siteId": "61ae856a8eae567c3905c550", "noDelete": true, "downloadLimitKbps": -1, "uploadLimitKbps": -1 }, { "id": "abc123...", "name": "Guest 10Mbps", "siteId": "61ae856a8eae567c3905c550", "noDelete": false, "downloadLimitKbps": 10000, "uploadLimitKbps": 5000 } ], "successful": true } ``` > **Note:** A value of `-1` for `downloadLimitKbps` or `uploadLimitKbps` means unlimited. --- ### Create Speed Profile **POST** `/unifi/site/:id/speed-profiles` Create a new speed limit profile (user group) on the UniFi controller. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.site.speed-profiles`, `unifi.site.speed-profiles.create` **URL Parameters:** - `id` - Internal UniFi site ID (database ID) **Request Body:** ```json { "name": "Guest 10Mbps", "downloadLimitKbps": 10000, "uploadLimitKbps": 5000 } ``` | Field | Type | Required | Description | | ------------------- | ------ | -------- | ----------------------------------------------- | | `name` | string | Yes | Name of the speed profile | | `downloadLimitKbps` | number | No | Download limit in Kbps (-1 or omit = unlimited) | | `uploadLimitKbps` | number | No | Upload limit in Kbps (-1 or omit = unlimited) | **Response (201):** ```json { "status": 201, "message": "UniFi Speed Profile Created Successfully!", "data": { "id": "abc123...", "name": "Guest 10Mbps", "siteId": "61ae856a8eae567c3905c550", "noDelete": false, "downloadLimitKbps": 10000, "uploadLimitKbps": 5000 }, "successful": true } ``` --- ### Get Private PSKs **GET** `/unifi/site/:id/wifi/:wlanId/ppsk` Fetch private pre-shared keys (PPSKs) for a specific WiFi network. PPSKs allow different devices to connect to the same SSID with unique passwords. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.site.wifi`, `unifi.site.wifi.ppsk` **URL Parameters:** - `id` - Internal UniFi site ID (database ID) - `wlanId` - UniFi WLAN configuration ID **Response:** ```json { "status": 200, "message": "UniFi Private PSKs Fetched Successfully!", "data": [ { "key": "mySecurePassword123", "name": "John's Laptop", "mac": null, "vlanId": null }, { "key": "anotherPassword456", "name": "IoT Device", "mac": "AA:BB:CC:DD:EE:FF", "vlanId": 100 } ], "successful": true } ``` --- ### Create Private PSK **POST** `/unifi/site/:id/wifi/:wlanId/ppsk` Create a private pre-shared key on a specific WiFi network. This adds a new PPSK to the WLAN's list and enables PPSK mode if not already enabled. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.site.wifi`, `unifi.site.wifi.ppsk`, `unifi.site.wifi.ppsk.create` **URL Parameters:** - `id` - Internal UniFi site ID (database ID) - `wlanId` - UniFi WLAN configuration ID **Request Body:** ```json { "key": "mySecurePassword123", "name": "John's Laptop", "mac": "AA:BB:CC:DD:EE:FF", "vlanId": 100 } ``` | Field | Type | Required | Description | | -------- | ------ | -------- | ------------------------------------------------- | | `key` | string | Yes | The pre-shared key (min 8 characters) | | `name` | string | Yes | Descriptive name for this PSK | | `mac` | string | No | MAC address to lock this PSK to a specific device | | `vlanId` | number | No | VLAN ID to assign to clients using this PSK | **Response (201):** ```json { "status": 201, "message": "UniFi Private PSK Created Successfully!", "data": [ { "key": "mySecurePassword123", "name": "John's Laptop", "mac": "AA:BB:CC:DD:EE:FF", "vlanId": 100 } ], "successful": true } ``` > **Note:** The response returns the full updated list of all PPSKs on the WLAN after creation. --- ### Link Site to Company **POST** `/unifi/site/:id/link` Link a UniFi site to a company. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.sites.link` **URL Parameters:** - `id` - Internal UniFi site ID (database ID) **Request Body:** ```json { "companyId": "ckx..." } ``` **Response:** ```json { "status": 200, "message": "UniFi Site Linked to Company Successfully!", "data": { "id": "ckx...", "name": "Total Tech - Murray Office", "siteId": "km9b1v8i", "companyId": "ckx...", "createdAt": "2025-01-01T00:00:00.000Z", "updatedAt": "2025-01-01T00:00:00.000Z" }, "successful": true } ``` --- ### Unlink Site from Company **POST** `/unifi/site/:id/unlink` Unlink a UniFi site from its associated company. **Authentication Required:** Yes **Required Permissions:** `unifi.access`, `unifi.sites.link` **URL Parameters:** - `id` - Internal UniFi site ID (database ID) **Response:** ```json { "status": 200, "message": "UniFi Site Unlinked from Company Successfully!", "data": { "id": "ckx...", "name": "Total Tech - Murray Office", "siteId": "km9b1v8i", "companyId": null, "createdAt": "2025-01-01T00:00:00.000Z", "updatedAt": "2025-01-01T00:00:00.000Z" }, "successful": true } ``` --- ## Error Responses All endpoints may return error responses in the following format: ### 400 Bad Request ```json { "status": 400, "message": "Validation error", "errors": [ { "path": ["field"], "message": "Field is required" } ], "successful": false } ``` ### 401 Unauthorized ```json { "status": 401, "message": "Unauthorized", "successful": false } ``` ### 403 Forbidden ```json { "status": 403, "message": "Insufficient permissions", "successful": false } ``` ### 404 Not Found ```json { "status": 404, "message": "Resource not found", "successful": false } ``` ### 500 Internal Server Error ```json { "status": 500, "message": "Internal server error", "successful": false } ``` --- ## Authentication Most endpoints require authentication via an access token in the request headers: ``` Authorization: Bearer ``` Tokens are obtained through the Microsoft OAuth flow: 1. Call `GET /auth/uri` to get the authentication URL 2. Redirect user to the Microsoft login page 3. User authenticates and is redirected to `/auth/redirect` 4. Access and refresh tokens are provided via WebSocket or response 5. Use the access token in subsequent API requests When the access token expires, use `POST /auth/refresh` with the refresh token to obtain a new access token. --- ## Permission System The API uses a granular permission system. Each endpoint requires specific permissions that are checked via the `authMiddleware`. Permissions are granted through roles assigned to users. Common permission patterns: - `resource.fetch` - Read a single resource - `resource.fetch.many` - Read multiple resources - `resource.create` - Create a new resource - `resource.update` - Update a resource - `resource.delete` - Delete a resource - `resource.field.action` - Perform specific field operations Users can have multiple roles, and permissions are accumulated from all assigned roles.