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