wulf-pulse/AUTOTASK_API_GUIDE.md
root 6eee14f8af Add comprehensive admin features and multi-system integration
- 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
2025-11-19 14:18:16 -05:00

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

// 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 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:

// ❌ 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):

// ❌ 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:

// 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:

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


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.