feat: add Autotask API guide, Pulse DB skill, and Datto RMM OpenClaw skill
- AUTOTASK_API_GUIDE.md — auth, query patterns, entity examples, gotchas - PULSE_DATABASE_SKILL.md — full DB schema reference for all data domains - DATTO_RMM_OPENCLAW_SKILL.md — read-only OpenClaw API for devices/sites/alerts
This commit is contained in:
commit
d1fb9db9d4
3 changed files with 1802 additions and 0 deletions
748
AUTOTASK_API_GUIDE.md
Normal file
748
AUTOTASK_API_GUIDE.md
Normal file
|
|
@ -0,0 +1,748 @@
|
|||
# Autotask REST API Development Guide
|
||||
|
||||
## Overview
|
||||
This guide provides comprehensive instructions for developing applications that integrate with the Autotask REST API v1.0. It's based on real-world implementation experience and includes patterns for authentication, caching, error handling, and user impersonation.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required Credentials
|
||||
You'll need the following from your Autotask instance:
|
||||
- **API Username**: Format typically `apiuser@YOURDOMAIN.COM`
|
||||
- **API Secret/Password**: Strong password for API authentication
|
||||
- **API Integration Code**: Tracking identifier for your integration
|
||||
- **API Base URL**: Usually `https://webservices{X}.autotask.net/atservicesrest/v1.0` where X is your zone number
|
||||
|
||||
### Environment Variables
|
||||
Store these securely in a `.env` file:
|
||||
```bash
|
||||
AUTOTASK_API_URL=https://webservices1.autotask.net/atservicesrest/v1.0
|
||||
AUTOTASK_USERNAME=your-api-username@yourdomain.com
|
||||
AUTOTASK_SECRET=your-api-password
|
||||
AUTOTASK_API_INTEGRATION_CODE=your-tracking-code
|
||||
```
|
||||
|
||||
## Core Implementation
|
||||
|
||||
### 1. Basic Authentication Headers
|
||||
|
||||
Every API request requires these headers:
|
||||
|
||||
```javascript
|
||||
getAuthHeaders(impersonationResourceId = null) {
|
||||
const credentials = Buffer.from(
|
||||
`${this.username}:${this.password}`
|
||||
).toString('base64');
|
||||
|
||||
const headers = {
|
||||
'Authorization': `Basic ${credentials}`,
|
||||
'ApiIntegrationcode': this.apiIntegrationCode,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
// Add impersonation header if needed
|
||||
if (impersonationResourceId) {
|
||||
headers['ImpersonationResourceId'] = impersonationResourceId.toString();
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Common API Patterns
|
||||
|
||||
#### Query Pattern
|
||||
Most Autotask entities support query operations with filters:
|
||||
|
||||
```javascript
|
||||
async queryEntity(entityName, filters = {}) {
|
||||
const filterArray = [];
|
||||
|
||||
// Build filter array based on provided criteria
|
||||
if (filters.fieldName) {
|
||||
filterArray.push({
|
||||
op: 'eq', // Operations: eq, noteq, gt, lt, gte, lte, contains, beginsWith, endsWith
|
||||
field: 'fieldName',
|
||||
value: filters.fieldName
|
||||
});
|
||||
}
|
||||
|
||||
// Construct query
|
||||
const query = filterArray.length > 0 ? { filter: filterArray } : {};
|
||||
const queryString = filterArray.length > 0
|
||||
? `?search=${encodeURIComponent(JSON.stringify(query))}`
|
||||
: '';
|
||||
|
||||
const url = `${this.baseUrl}/${entityName}/query${queryString}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: this.getAuthHeaders()
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
return result.items || [];
|
||||
}
|
||||
```
|
||||
|
||||
#### Get by ID Pattern
|
||||
```javascript
|
||||
async getEntityById(entityName, id) {
|
||||
const url = `${this.baseUrl}/${entityName}/${id}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: this.getAuthHeaders()
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
return result.item;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Working with Specific Entities
|
||||
|
||||
#### Resources (Users)
|
||||
```javascript
|
||||
// Get resource by email
|
||||
async getResourceByEmail(email) {
|
||||
const query = {
|
||||
filter: [{
|
||||
op: 'eq',
|
||||
field: 'email',
|
||||
value: email
|
||||
}]
|
||||
};
|
||||
|
||||
const url = `${this.baseUrl}/Resources/query?search=${encodeURIComponent(JSON.stringify(query))}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: this.getAuthHeaders()
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.items && result.items.length > 0) {
|
||||
const resource = result.items[0];
|
||||
return {
|
||||
id: resource.id,
|
||||
firstName: resource.firstName,
|
||||
lastName: resource.lastName,
|
||||
email: resource.email
|
||||
};
|
||||
}
|
||||
throw new Error('Resource not found');
|
||||
}
|
||||
```
|
||||
|
||||
#### Tickets
|
||||
```javascript
|
||||
// Get open tickets for a resource (exclude status 5 = Complete)
|
||||
async getOpenTicketsByResource(resourceId) {
|
||||
const query = {
|
||||
filter: [
|
||||
{ op: 'eq', field: 'assignedResourceID', value: resourceId },
|
||||
{ op: 'noteq', field: 'status', value: 5 }
|
||||
]
|
||||
};
|
||||
|
||||
return this.queryEntity('Tickets', query);
|
||||
}
|
||||
```
|
||||
|
||||
#### Tasks
|
||||
```javascript
|
||||
// Get tasks for a resource (exclude status 5 = Complete)
|
||||
async getTasksByResource(resourceId) {
|
||||
const query = {
|
||||
filter: [
|
||||
{ op: 'eq', field: 'assignedResourceID', value: resourceId },
|
||||
{ op: 'noteq', field: 'status', value: 5 }
|
||||
]
|
||||
};
|
||||
|
||||
return this.queryEntity('Tasks', query);
|
||||
}
|
||||
```
|
||||
|
||||
#### Companies
|
||||
```javascript
|
||||
// Get all active companies
|
||||
async getAllCompanies() {
|
||||
const query = {
|
||||
filter: [{ op: 'eq', field: 'isActive', value: true }]
|
||||
};
|
||||
|
||||
const companies = await this.queryEntity('Companies', query);
|
||||
return companies.map(company => ({
|
||||
id: company.id,
|
||||
name: company.companyName || company.name
|
||||
})).sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
```
|
||||
|
||||
#### Configuration Items
|
||||
Configuration Items represent physical or virtual assets (computers, servers, network devices, etc.) associated with companies.
|
||||
|
||||
```javascript
|
||||
// Get configuration items for a company
|
||||
async getConfigurationItemsByCompany(companyId) {
|
||||
const query = {
|
||||
filter: [
|
||||
{ op: 'eq', field: 'companyID', value: companyId },
|
||||
{ op: 'eq', field: 'isActive', value: true } // Only active items
|
||||
]
|
||||
};
|
||||
|
||||
return this.queryEntity('ConfigurationItems', query);
|
||||
}
|
||||
|
||||
// Get a single configuration item by ID
|
||||
async getConfigurationItemById(id) {
|
||||
return this.getEntityById('ConfigurationItems', id);
|
||||
}
|
||||
|
||||
// Update a configuration item
|
||||
async updateConfigurationItem(id, updates) {
|
||||
// IMPORTANT: Must fetch current item first to get required fields
|
||||
const currentItem = await this.getConfigurationItemById(id);
|
||||
|
||||
// Autotask requires certain fields even for updates
|
||||
const updateData = {
|
||||
id: id,
|
||||
companyID: currentItem.companyID, // Required
|
||||
productID: currentItem.productID, // Required
|
||||
referenceTitle: updates.referenceTitle ?? currentItem.referenceTitle,
|
||||
isActive: updates.isActive !== undefined ? updates.isActive : currentItem.isActive,
|
||||
// Optional fields
|
||||
serialNumber: updates.serialNumber ?? currentItem.serialNumber,
|
||||
referenceNumber: updates.referenceNumber ?? currentItem.referenceNumber,
|
||||
location: updates.location ?? currentItem.location,
|
||||
notes: updates.notes ?? currentItem.notes
|
||||
};
|
||||
|
||||
const url = `${this.baseUrl}/ConfigurationItems`;
|
||||
|
||||
const response = await this.makeApiCall(url, {
|
||||
method: 'PUT',
|
||||
headers: this.getAuthHeaders(),
|
||||
body: JSON.stringify(updateData)
|
||||
});
|
||||
|
||||
// IMPORTANT: Autotask returns { itemId: X } on successful update, not { item: {...} }
|
||||
// You must fetch the updated item separately
|
||||
if (response.itemId) {
|
||||
return this.getConfigurationItemById(response.itemId);
|
||||
}
|
||||
|
||||
throw new Error('Failed to update configuration item - no itemId in response');
|
||||
}
|
||||
```
|
||||
|
||||
**Configuration Item Key Fields:**
|
||||
- `id` - Unique identifier
|
||||
- `companyID` - Associated company (required)
|
||||
- `productID` - Product/asset type (required, even for updates)
|
||||
- `referenceTitle` - Display name
|
||||
- `serialNumber` - Device serial number
|
||||
- `isActive` - Active status (true/false)
|
||||
- `contactID` - Primary contact for the device
|
||||
- `rmmDeviceUID` - RMM system unique identifier
|
||||
- `rmmDeviceAuditIPAddress` - IP address from RMM
|
||||
- `dattoSerialNumber` - Datto RMM serial number
|
||||
- `dattoInternalIP` / `dattoRemoteIP` - Datto IP addresses
|
||||
|
||||
#### Contacts
|
||||
Contacts can be associated with configuration items to track device ownership/responsibility.
|
||||
|
||||
```javascript
|
||||
// Get contacts for a company
|
||||
async getContactsByCompany(companyId) {
|
||||
const query = {
|
||||
filter: [
|
||||
{ op: 'eq', field: 'companyID', value: companyId },
|
||||
{ op: 'eq', field: 'isActive', value: 1 }
|
||||
]
|
||||
};
|
||||
|
||||
return this.queryEntity('Contacts', query);
|
||||
}
|
||||
|
||||
// Batch fetch contacts to avoid rate limits
|
||||
async getContactsBatch(contactIds) {
|
||||
const contacts = {};
|
||||
|
||||
// Fetch in parallel but respect rate limits
|
||||
const promises = contactIds.map(async (id) => {
|
||||
try {
|
||||
const contact = await this.getEntityById('Contacts', id);
|
||||
if (contact) {
|
||||
contacts[id] = contact;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch contact ${id}:`, error.message);
|
||||
// Continue with other contacts
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
return contacts;
|
||||
}
|
||||
```
|
||||
|
||||
#### Billing Items
|
||||
Billing items represent products/services that can be billed to customers.
|
||||
|
||||
```javascript
|
||||
// Get billing items (products) for a company
|
||||
async getBillingItemsByCompany(companyId) {
|
||||
const query = {
|
||||
filter: [
|
||||
{ op: 'eq', field: 'companyID', value: companyId }
|
||||
]
|
||||
};
|
||||
|
||||
return this.queryEntity('BillingItems', query);
|
||||
}
|
||||
|
||||
// Get all products (for product catalog)
|
||||
async getAllProducts() {
|
||||
return this.queryEntity('Products', {});
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Working with Picklists
|
||||
|
||||
Picklists provide dropdown values for fields like status, priority, etc.
|
||||
|
||||
```javascript
|
||||
async getPicklistValues(entityName, fieldName) {
|
||||
const url = `${this.baseUrl}/${entityName}/entityInformation/fields`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: this.getAuthHeaders()
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const field = result.fields.find(f => f.name === fieldName);
|
||||
|
||||
if (field && field.picklistValues) {
|
||||
const picklistMap = {};
|
||||
field.picklistValues.forEach(item => {
|
||||
picklistMap[item.value] = item.label;
|
||||
});
|
||||
return picklistMap;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// Example: Get ticket status labels
|
||||
async getTicketStatusPicklist() {
|
||||
return this.getPicklistValues('Tickets', 'status');
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Uploading Attachments
|
||||
|
||||
```javascript
|
||||
async uploadAttachment(entityName, entityId, fileBuffer, fileName, impersonatorEmail = null) {
|
||||
// Get impersonator resource ID if email provided
|
||||
let impersonationResourceId = null;
|
||||
if (impersonatorEmail) {
|
||||
const resource = await this.getResourceByEmail(impersonatorEmail);
|
||||
impersonationResourceId = resource.id;
|
||||
}
|
||||
|
||||
// Convert file to base64
|
||||
const base64Data = fileBuffer.toString('base64');
|
||||
|
||||
// Prepare payload
|
||||
const payload = {
|
||||
id: 0,
|
||||
attachmentType: 'FILE_ATTACHMENT',
|
||||
fullPath: fileName,
|
||||
title: fileName,
|
||||
publish: 1, // 1 = All Autotask Users, 2 = Internal Users Only
|
||||
data: base64Data
|
||||
};
|
||||
|
||||
const url = `${this.baseUrl}/${entityName}/${entityId}/Attachments`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: this.getAuthHeaders(impersonationResourceId),
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to upload attachment: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Implementing Caching with Redis
|
||||
|
||||
To reduce API calls and improve performance:
|
||||
|
||||
```javascript
|
||||
import { createClient } from 'redis';
|
||||
|
||||
class CacheService {
|
||||
constructor() {
|
||||
this.client = createClient({
|
||||
url: process.env.REDIS_URL || 'redis://localhost:6379'
|
||||
});
|
||||
this.client.connect();
|
||||
}
|
||||
|
||||
async getCachedData(key, ttlSeconds = 3600) {
|
||||
const cached = await this.client.get(key);
|
||||
if (cached) {
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async setCachedData(key, data, ttlSeconds = 3600) {
|
||||
await this.client.setex(key, ttlSeconds, JSON.stringify(data));
|
||||
}
|
||||
|
||||
// Cache with TTL strategy
|
||||
async getWithCache(key, fetchFunction, ttlSeconds = 3600) {
|
||||
const cached = await this.getCachedData(key);
|
||||
if (cached) return cached;
|
||||
|
||||
const fresh = await fetchFunction();
|
||||
await this.setCachedData(key, fresh, ttlSeconds);
|
||||
return fresh;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage example
|
||||
async getCompaniesWithCache() {
|
||||
return cache.getWithCache(
|
||||
'companies:all',
|
||||
() => this.getAllCompanies(),
|
||||
86400 // 24 hour TTL
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Error Handling
|
||||
|
||||
Implement robust error handling for API responses:
|
||||
|
||||
```javascript
|
||||
async makeApiCall(url, options) {
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
const responseText = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Autotask API error: ${response.status} - ${responseText}`);
|
||||
|
||||
// Try to parse error details
|
||||
let errorMessage = `API Error: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = JSON.parse(responseText);
|
||||
if (errorData.errors && errorData.errors.length > 0) {
|
||||
errorMessage = errorData.errors.map(e => e.message).join(', ');
|
||||
}
|
||||
} catch (parseError) {
|
||||
errorMessage = responseText || errorMessage;
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// Parse successful response
|
||||
try {
|
||||
return JSON.parse(responseText);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse API response:', parseError.message);
|
||||
throw new Error('Invalid JSON response from Autotask API');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('API call failed:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Rate Limiting Considerations
|
||||
|
||||
Autotask has rate limits. Implement throttling:
|
||||
|
||||
```javascript
|
||||
class RateLimiter {
|
||||
constructor(maxRequestsPerSecond = 10) {
|
||||
this.maxRequestsPerSecond = maxRequestsPerSecond;
|
||||
this.requestTimes = [];
|
||||
}
|
||||
|
||||
async throttle() {
|
||||
const now = Date.now();
|
||||
const oneSecondAgo = now - 1000;
|
||||
|
||||
// Remove old request times
|
||||
this.requestTimes = this.requestTimes.filter(t => t > oneSecondAgo);
|
||||
|
||||
// If at limit, wait
|
||||
if (this.requestTimes.length >= this.maxRequestsPerSecond) {
|
||||
const oldestRequest = this.requestTimes[0];
|
||||
const waitTime = 1000 - (now - oldestRequest);
|
||||
if (waitTime > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||
}
|
||||
}
|
||||
|
||||
this.requestTimes.push(Date.now());
|
||||
}
|
||||
}
|
||||
|
||||
// Use before API calls
|
||||
await rateLimiter.throttle();
|
||||
const response = await fetch(url, options);
|
||||
```
|
||||
|
||||
## Docker/Container Setup
|
||||
|
||||
### Docker Compose Configuration
|
||||
```yaml
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: app-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
command: redis-server --save 60 1
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
container_name: app-backend
|
||||
ports:
|
||||
- "5001:5001"
|
||||
environment:
|
||||
- AUTOTASK_API_URL=${AUTOTASK_API_URL}
|
||||
- AUTOTASK_USERNAME=${AUTOTASK_USERNAME}
|
||||
- AUTOTASK_SECRET=${AUTOTASK_SECRET}
|
||||
- AUTOTASK_API_INTEGRATION_CODE=${AUTOTASK_API_INTEGRATION_CODE}
|
||||
- REDIS_URL=redis://redis:6379
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- backend-node-modules:/app/node_modules
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
backend-node-modules:
|
||||
```
|
||||
|
||||
## Common Gotchas and Solutions
|
||||
|
||||
### 1. Password Special Characters
|
||||
If your API password contains `$`, escape it in `.env` files (but NOT in docker-compose.yml environment variables).
|
||||
|
||||
### 2. Entity Status Values
|
||||
- Tickets: Status 5 = Complete
|
||||
- Tasks: Status 5 = Complete
|
||||
- Use picklists to get human-readable labels
|
||||
|
||||
### 3. Query Limits
|
||||
- Default max results: 500 items
|
||||
- Implement pagination for large datasets
|
||||
- Use specific filters to reduce result sets
|
||||
|
||||
### 4. Field Names
|
||||
- Field names in queries are case-sensitive
|
||||
- Common fields: `id`, `companyID`, `assignedResourceID`, `status`
|
||||
- Check entity documentation for exact field names
|
||||
|
||||
### 5. Impersonation
|
||||
- Use `ImpersonationResourceId` header to act as another user
|
||||
- Useful for attachments to show correct "uploaded by" user
|
||||
- Requires resource ID, not email (lookup required)
|
||||
|
||||
### 6. Time Zones
|
||||
- Autotask uses UTC for all timestamps
|
||||
- Convert to local timezone for display
|
||||
- Send in UTC for updates
|
||||
|
||||
### 7. Null Values
|
||||
- Many fields can be null
|
||||
- Always check for null before accessing nested properties
|
||||
- Use optional chaining: `ticket?.companyID`
|
||||
|
||||
### 8. Configuration Item Updates - CRITICAL
|
||||
**The update response format is different from other entities:**
|
||||
|
||||
```javascript
|
||||
// ❌ WRONG - This will fail
|
||||
const response = await updateConfigurationItem(id, data);
|
||||
return response.item; // item doesn't exist!
|
||||
|
||||
// ✅ CORRECT - Autotask returns itemId, not item
|
||||
const response = await updateConfigurationItem(id, data);
|
||||
if (response.itemId) {
|
||||
// Must fetch the updated item separately
|
||||
return await getConfigurationItemById(response.itemId);
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- PUT requests to ConfigurationItems return `{ itemId: 12345 }` not `{ item: {...} }`
|
||||
- You MUST fetch the updated item with a separate GET request
|
||||
- Always include `productID` and `companyID` even for updates (required fields)
|
||||
- Fetch the current item first to preserve required fields you're not updating
|
||||
|
||||
### 9. Rate Limiting with Batch Operations
|
||||
When fetching multiple related entities (like contacts for devices):
|
||||
|
||||
```javascript
|
||||
// ❌ BAD - Sequential requests, very slow
|
||||
for (const contactId of contactIds) {
|
||||
const contact = await getContact(contactId);
|
||||
}
|
||||
|
||||
// ✅ BETTER - Parallel requests, but can hit rate limits
|
||||
await Promise.all(contactIds.map(id => getContact(id)));
|
||||
|
||||
// ✅ BEST - Batch fetch on server side, cache results
|
||||
// Fetch all contacts for a company once, then lookup by ID
|
||||
const allContacts = await getContactsByCompany(companyId);
|
||||
const contactMap = {};
|
||||
allContacts.forEach(c => contactMap[c.id] = c);
|
||||
```
|
||||
|
||||
**Strategy for avoiding rate limits:**
|
||||
1. Fetch related data in bulk when possible (e.g., all contacts for a company)
|
||||
2. Cache frequently accessed data (companies, products, picklists)
|
||||
3. Use batch endpoints when available
|
||||
4. Implement request throttling/queuing for parallel operations
|
||||
|
||||
### 10. Configuration Item Filtering by Status
|
||||
When querying configuration items, the `isActive` field behaves differently:
|
||||
|
||||
```javascript
|
||||
// Get only active items
|
||||
const query = {
|
||||
filter: [
|
||||
{ op: 'eq', field: 'companyID', value: companyId },
|
||||
{ op: 'eq', field: 'isActive', value: true } // Boolean true
|
||||
]
|
||||
};
|
||||
|
||||
// Get only inactive items
|
||||
const query = {
|
||||
filter: [
|
||||
{ op: 'eq', field: 'companyID', value: companyId },
|
||||
{ op: 'eq', field: 'isActive', value: false } // Boolean false
|
||||
]
|
||||
};
|
||||
|
||||
// Get all items (active and inactive)
|
||||
const query = {
|
||||
filter: [
|
||||
{ op: 'eq', field: 'companyID', value: companyId }
|
||||
// Don't filter by isActive
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### 11. RMM Integration Fields
|
||||
Configuration Items have special fields for RMM system integration:
|
||||
|
||||
- `rmmDeviceUID` - Unique identifier from RMM system (use for matching)
|
||||
- `rmmDeviceAuditIPAddress` - IP address reported by RMM
|
||||
- `rmmDeviceAuditHostname` - Hostname from RMM
|
||||
- `dattoSerialNumber` - Datto-specific serial number
|
||||
- `dattoInternalIP` / `dattoRemoteIP` - Datto-specific IP addresses
|
||||
|
||||
**Best practice for matching RMM devices to Autotask:**
|
||||
1. First try matching by `rmmDeviceUID`
|
||||
2. Fall back to `serialNumber` or `dattoSerialNumber`
|
||||
3. Last resort: match by IP address (less reliable)
|
||||
|
||||
### 12. Contact Association
|
||||
Contacts can be linked to configuration items via `contactID`:
|
||||
|
||||
```javascript
|
||||
// Update configuration item with contact
|
||||
await updateConfigurationItem(itemId, {
|
||||
contactID: contactId // Links device to a specific contact
|
||||
});
|
||||
|
||||
// Remove contact association
|
||||
await updateConfigurationItem(itemId, {
|
||||
contactID: null // Removes contact link
|
||||
});
|
||||
```
|
||||
|
||||
**Note:** Contact must belong to the same company as the configuration item.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Start with read-only operations** (GET requests)
|
||||
2. **Test with a sandbox/development Autotask instance** if available
|
||||
3. **Cache aggressively** to avoid hitting rate limits during development
|
||||
4. **Log all API calls** during development for debugging
|
||||
5. **Implement retry logic** for transient failures
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never commit credentials** to version control
|
||||
2. **Use environment variables** for all sensitive data
|
||||
3. **Implement proper authentication** in your application
|
||||
4. **Validate and sanitize** all user inputs
|
||||
5. **Use HTTPS** for all API communications
|
||||
6. **Rotate API credentials** regularly
|
||||
7. **Implement audit logging** for API operations
|
||||
|
||||
## Sample Project Structure
|
||||
|
||||
```
|
||||
project/
|
||||
├── backend/
|
||||
│ ├── src/
|
||||
│ │ ├── server.js
|
||||
│ │ ├── routes/
|
||||
│ │ │ ├── tickets.js
|
||||
│ │ │ └── tasks.js
|
||||
│ │ └── services/
|
||||
│ │ ├── autotask.js
|
||||
│ │ └── cache.js
|
||||
│ ├── package.json
|
||||
│ └── Dockerfile
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
│ │ └── components/
|
||||
│ ├── package.json
|
||||
│ └── Dockerfile
|
||||
├── docker-compose.yml
|
||||
├── .env
|
||||
└── .env.example
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Autotask REST API Documentation](https://ww1.autotask.net/help/DeveloperHelp/Content/APIs/REST/REST_API_Home.htm)
|
||||
- [Autotask Entity Documentation](https://ww1.autotask.net/help/DeveloperHelp/Content/APIs/REST/Entities/Entities_overview.htm)
|
||||
- Use browser DevTools Network tab to inspect API calls in Autotask UI for field discovery
|
||||
|
||||
---
|
||||
|
||||
This guide should provide a comprehensive starting point for any AI model to help you develop Autotask API integrations. Adapt the patterns to your specific use case and requirements.
|
||||
Loading…
Add table
Add a link
Reference in a new issue