From ffa5eb8e0733b52e3cc9bebb189f782f501a1ade Mon Sep 17 00:00:00 2001 From: Lorentz Hinrichsen Date: Mon, 27 Oct 2025 23:42:53 -0400 Subject: [PATCH] Initial commit: Autotask API utilities and Docker setup --- AUTOTASK_API_GUIDE.md | 511 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 511 insertions(+) create mode 100644 AUTOTASK_API_GUIDE.md diff --git a/AUTOTASK_API_GUIDE.md b/AUTOTASK_API_GUIDE.md new file mode 100644 index 0000000..e217fce --- /dev/null +++ b/AUTOTASK_API_GUIDE.md @@ -0,0 +1,511 @@ +# 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. \ No newline at end of file