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