# 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)); } ``` ### 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` ## 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.