- Add admin dashboard with sync controls and data browser - Implement RMM, Auvik, and Addigy organization mappings - Add chunked ticket sync with progress tracking - Implement entity sync service with rate limiting - Add analytics engine and performance optimizer - Create data browser for all PSA entities - Add navigation components and UI improvements - Implement background processing and sync services - Add comprehensive documentation and migration scripts - Update configuration items with multi-system support - Enhance contact management and purchase history - Add issue type assignment and LLM analyzer - Improve error handling and logging utilities
20 KiB
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.0where X is your zone number
Environment Variables
Store these securely in a .env file:
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:
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:
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
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)
// 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
// 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
// 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
// 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.
// 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 identifiercompanyID- Associated company (required)productID- Product/asset type (required, even for updates)referenceTitle- Display nameserialNumber- Device serial numberisActive- Active status (true/false)contactID- Primary contact for the devicermmDeviceUID- RMM system unique identifierrmmDeviceAuditIPAddress- IP address from RMMdattoSerialNumber- Datto RMM serial numberdattoInternalIP/dattoRemoteIP- Datto IP addresses
Contacts
Contacts can be associated with configuration items to track device ownership/responsibility.
// 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.
// 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.
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
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:
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:
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:
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
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
ImpersonationResourceIdheader 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:
// ❌ 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
productIDandcompanyIDeven 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):
// ❌ 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:
- Fetch related data in bulk when possible (e.g., all contacts for a company)
- Cache frequently accessed data (companies, products, picklists)
- Use batch endpoints when available
- Implement request throttling/queuing for parallel operations
10. Configuration Item Filtering by Status
When querying configuration items, the isActive field behaves differently:
// 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 RMMrmmDeviceAuditHostname- Hostname from RMMdattoSerialNumber- Datto-specific serial numberdattoInternalIP/dattoRemoteIP- Datto-specific IP addresses
Best practice for matching RMM devices to Autotask:
- First try matching by
rmmDeviceUID - Fall back to
serialNumberordattoSerialNumber - Last resort: match by IP address (less reliable)
12. Contact Association
Contacts can be linked to configuration items via contactID:
// 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
- Start with read-only operations (GET requests)
- Test with a sandbox/development Autotask instance if available
- Cache aggressively to avoid hitting rate limits during development
- Log all API calls during development for debugging
- Implement retry logic for transient failures
Security Best Practices
- Never commit credentials to version control
- Use environment variables for all sensitive data
- Implement proper authentication in your application
- Validate and sanitize all user inputs
- Use HTTPS for all API communications
- Rotate API credentials regularly
- 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
- Autotask Entity Documentation
- 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.