d7b374f8ab
New features: - ActivityController and manager for CW sales activities (CRUD) - ForecastProductController for opportunity forecast/product lines - CW member cache with dual-layer (in-memory + Redis) resolution - Catalog category/subcategory/ecosystem taxonomy module - Quote statuses type definitions with CW mapping - User-defined fields (UDF) module with cache and event refresh - Company sites CW module with serialization - Procurement manager filters (category, ecosystem, manufacturer, price, stock) - Opportunity notes CRUD and product line management via CW API - Opportunity type definitions endpoint Updates: - OpportunityController: CW refresh, company hydration, activities, custom fields - UserController: cwIdentifier field for CW member linking - CatalogItemController: category/subcategory fields from CW - PermissionNodes: sales note/product CRUD nodes, subCategories, collectPermissions - API routes: procurement categories/filters, sales notes/products, opportunity types - Global events: UDF and member refresh intervals on startup Tests (414 passing): - ActivityController, ForecastProductController, OpportunityController unit tests - UserController cwIdentifier tests - catalogCategories, companySites, memberCache, procurement module tests - activityTypes, opportunityTypes, quoteStatuses type tests - permissionNodes subCategories and getAllPermissionNodes tests - Updated test setup with redis mock, API method mocks, and builder helpers
4695 lines
109 KiB
Markdown
4695 lines
109 KiB
Markdown
# 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 `<scope>.<field>` — only fields the user has permission for are included in the response. Grant `<scope>.*` 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",
|
|
"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",
|
|
"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.
|
|
|
|
**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 sequence order of products (forecast items) on an opportunity. Sends a `sequenceNumber` PATCH to each forecast item in ConnectWise.
|
|
|
|
**Authentication Required:** Yes
|
|
|
|
**Required Permissions:** `sales.opportunity.product.update`
|
|
|
|
**Path Parameters:**
|
|
|
|
- `identifier` — Internal ID (cuid) or ConnectWise opportunity ID (numeric)
|
|
|
|
**Request Body:**
|
|
|
|
```json
|
|
{
|
|
"orderedIds": [31846, 31847, 31848]
|
|
}
|
|
```
|
|
|
|
- `orderedIds` — Array of forecast item IDs in the desired sequence order. Position in the array determines the `sequenceNumber` (1-based).
|
|
|
|
**Response:**
|
|
|
|
```json
|
|
{
|
|
"status": 200,
|
|
"message": "Product sequence updated successfully!",
|
|
"data": {
|
|
"products": [
|
|
{
|
|
"id": 31850,
|
|
"forecastDescription": "Service",
|
|
"sequenceNumber": 1,
|
|
"..."
|
|
}
|
|
],
|
|
"idMap": {
|
|
"31846": 31850,
|
|
"31847": 31851,
|
|
"31848": 31852
|
|
}
|
|
},
|
|
"successful": true
|
|
}
|
|
```
|
|
|
|
- `data.products` — Full updated product objects (IDs may change after PUT to ConnectWise).
|
|
- `data.idMap` — Maps each original forecast item ID (from the request) to the new ID returned by ConnectWise. Use this to update references in the UI.
|
|
|
|
---
|
|
|
|
### 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.<field>`
|
|
|
|
This route uses `processObjectValuePerms` to filter each WLAN object on a per-field basis. Only fields whose corresponding `unifi.site.wifi.read.<field>` 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.
|
|
|
|
<details>
|
|
<summary>All available field-level permission nodes</summary>
|
|
|
|
| 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` |
|
|
|
|
</details>
|
|
|
|
**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 <access_token>
|
|
```
|
|
|
|
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.
|