- 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
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.