# Autotask API — Claude Code Skill Guide A reference for working with the Autotask REST API in this codebase. Read this before adding, modifying, or debugging any Autotask integration. --- ## Quick Reference | Item | Value | |------|-------| | Base URL | `https://webservices1.autotask.net/atservicesrest/v1.0` | | Auth style | Custom headers (not Basic Auth) | | Query method | `POST /Entity/query` with JSON body | | Rate limit | 10 req/s (enforced internally) | | Page size | 500 records max | | Client file | `lib/services/autotask-client.ts` | | Factory | `lib/services/autotask-factory.ts` | | Types | `lib/types/autotask.ts` | --- ## Authentication All requests use three custom headers — **not** a Bearer token or Basic Auth. ``` Username: Secret: APIIntegrationcode: Content-Type: application/json Accept: application/json ``` Optional delegation header: ``` ImpersonationResourceID: ``` **Env vars required:** ``` AUTOTASK_API_URL=https://webservices1.autotask.net/atservicesrest/v1.0 AUTOTASK_USERNAME=user@domain.com AUTOTASK_SECRET=api-password AUTOTASK_API_INTEGRATION_CODE=your-integration-code AUTOTASK_WEBHOOK_SECRET=webhook-hmac-secret WEBHOOK_BASE_URL=https://your-public-domain.com ``` --- ## Client Usage Always get the client via the factory — never instantiate `AutotaskClient` directly. ```typescript import { getAutotaskClient } from '@/lib/services/autotask-factory'; const client = getAutotaskClient(); ``` `isMsgraphConfigured()` pattern does not apply here — Autotask client always initialises if env vars are present. --- ## Entity Endpoints ### Standard CRUD | Operation | Method | Path | |-----------|--------|------| | Query / list | `POST` | `/Entity/query` | | Get by ID | `GET` | `/Entity/{id}` | | Create | `POST` | `/Entity` | | Update | `PUT` | `/Entity` | | Delete | `DELETE` | `/Entity/{id}` | | Field metadata | `GET` | `/Entity/entityInformation/fields` | | Attachments | `GET/POST` | `/Entity/{id}/Attachments` | ### Supported Entities ``` Companies Tickets TicketNotes Tasks Projects Resources Contacts ConfigurationItems Contracts ContractServices Services TimeEntries TicketTagAssociations TagGroups Tags Statuses IssueTypes SubIssueTypes WorkTypes Queues Priorities TicketCategories BillingItems ClassificationIcons ``` ### Webhook Entities (separate registration endpoints) ``` CompanyWebhooks ContactWebhooks ConfigurationItemWebhooks TicketWebhooks TicketNoteWebhooks /{WebhookEntity}/{id}/Fields /{WebhookEntity}/{id}/ExcludedResources ``` --- ## Querying All list/search operations use `POST /Entity/query` with a JSON body — **not** query string parameters. ### Request shape ```typescript const params = { MaxRecords: 500, filter: [ { op: 'eq', field: 'isActive', value: true }, { op: 'gte', field: 'lastActivityDate', value: '2024-01-01T00:00:00Z' } ] }; const result = await client.queryEntity('Companies', params); // result.items: Company[] // result.pageDetails: { count, requestCount, nextPageUrl, prevPageUrl } ``` ### Filter operators | Operator | Meaning | |----------|---------| | `eq` | Equal | | `noteq` | Not equal | | `gt` / `gte` | Greater than / or equal | | `lt` / `lte` | Less than / or equal | | `contains` | String contains | | `beginsWith` | String starts with | | `endsWith` | String ends with | | `exist` | Field is not null | ### Common filter fields | Field | Used on | |-------|---------| | `isActive` | Companies, Resources, ConfigItems, Contacts, Services, Tags | | `status` | Tickets, Tasks, Projects, Contracts | | `companyID` | Most child entities | | `assignedResourceID` | Tickets, Tasks | | `createDate` / `createDateTime` | Tickets, Tasks | | `lastActivityDate` / `lastModifiedDateTime` | Most entities | | `resourceID` / `ticketID` / `taskID` / `projectID` | TimeEntries | | `id` | Any entity | --- ## Pagination Use `queryEntityPaginated` — it handles `nextPageUrl` automatically and yields all records. ```typescript const allTickets = await client.queryEntityPaginated('Tickets', { MaxRecords: 500, filter: [{ op: 'eq', field: 'status', value: 1 }] }); ``` Manual pagination: check `result.pageDetails.nextPageUrl` and issue a GET to that URL. --- ## Response Shapes ### Single entity (GET by ID) ```json { "item": { "id": 123, ... } } ``` ### Query / list (POST /query) ```json { "items": [ { "id": 1, ... }, { "id": 2, ... } ], "pageDetails": { "count": 500, "requestCount": 1, "nextPageUrl": "https://...?pageDetails=...", "prevPageUrl": null } } ``` ### Create / update ```json { "itemId": 456, "item": { ... } } ``` ### Error ```json { "message": "Human-readable error", "errors": [ { "message": "Detail", "field": "fieldName" } ] } ``` --- ## Client Methods ### Generic CRUD ```typescript client.queryEntity(entity, params) // single page client.queryEntityPaginated(entity, params, pageSize) // all pages client.getEntityById(entity, id) client.createEntity(entity, data) client.updateEntity(entity, id, data) // PUT then GET client.deleteEntity(entity, id) client.getFieldInfo(entity) // field metadata ``` ### Typed convenience methods ```typescript // Resources client.getResourceByEmail(email) client.getAllResources() // Tickets client.getOpenTicketsByResource(resourceId) client.getTicketsByCompany(companyId) client.createTicket(data) client.updateTicket(id, data) // Tasks client.getTasksByResource(resourceId) client.getTasksByProject(projectId) client.createTask(data) client.updateTask(id, data) // Companies client.getAllCompanies() client.getCompanyById(id) // Configuration Items client.getConfigurationItemsByCompany(companyId) client.getAllConfigurationItems() client.getConfigurationItemById(id) client.createConfigurationItem(data) client.updateConfigurationItem(id, data) // Time Entries client.getTimeEntriesByResource(resourceId) client.getTimeEntriesByTicket(ticketId) client.getTimeEntriesByTask(taskId) client.getTimeEntriesByProject(projectId) client.getTimeEntriesByCompany(companyId) client.getTimeEntriesByDateRange(start, end) client.createTimeEntry(data) client.updateTimeEntry(id, data) client.deleteTimeEntry(id) // Picklists client.getPicklistValues(entity, field) client.getTicketStatusPicklist() client.getTicketPriorityPicklist() client.getTaskStatusPicklist() // Attachments client.uploadAttachment(entity, id, file, filename, contentType) client.getAttachments(entity, id) // Misc client.getClassificationIcons() ``` --- ## Entity Sync The sync layer lives in `lib/services/entity-sync.ts`. It handles both full and incremental syncs from Autotask → PostgreSQL. ### Sync types | Type | Description | |------|-------------| | **Full** | Fetch all records (with active/status filters) | | **Incremental** | Fetch only records changed since last sync timestamp | ### Incremental filter fields by entity | Entity | Incremental field | Active filter | Date window | |--------|-------------------|---------------|-------------| | Companies | `lastActivityDate` | `isActive=true` | None | | Tickets | `lastActivityDate` | None | `createDate` (2 yr) | | Tasks | `lastActivityDateTime` | None | `createDateTime` (2 yr) | | TimeEntries | `lastModifiedDateTime` | None | `lastModifiedDateTime` (2 yr) | | Contracts | `lastModifiedDateTime` | `status=1` | None | | ContractServices | `lastModifiedDate` | None | None | | ConfigurationItems | `lastModifiedTime` | `isActive=true` | None | | Resources | `lastModifiedDate` | `isActive=true` | No incremental | | Tags | `lastModifiedDateTime` | `isActive=true` | None | | TagGroups | `lastModifiedDate` | `isActive=true` | None | | TicketTagAssociations | N/A | `id > 0` | N/A | | BillingItems | `itemDate` | None | `itemDate` (2 yr) | | Contacts | `lastModifiedDate` | `isActive=true` | None | | Projects | `lastActivityDateTime` | `status != 5` | None | | Picklists (Statuses, etc.) | `lastModifiedDate` | `isActive=true` | None | ### Sync order (dependency chain) ``` 1. Companies — parent entity 2. Resources — parent entity 3. Priorities, Statuses, IssueTypes, SubIssueTypes, WorkTypes, Queues, TicketCategories 4. Contacts, Projects, Tickets, Tasks 5. ConfigurationItems, Contracts, TimeEntries 6. ContractServices, BillingItems 7. TagGroups, Tags, TicketTagAssociations ``` --- ## Webhooks ### Incoming webhook - Route: `POST /api/webhooks/autotask` - Verification: HMAC-SHA1 of raw body, header `x-hook-signature: sha1=` - Service: `lib/services/webhook-service.ts` ### Processing pipeline 1. Verify HMAC signature 2. Normalise raw Autotask payload → internal format 3. Fetch full entity from API (webhook payloads are partial) 4. Upsert into PostgreSQL 5. Fire workflow engine (new tickets only, fire-and-forget) 6. Log event ### Webhook payload (raw from Autotask) ```json { "Action": "Create | Update | Delete", "Guid": "unique-event-guid", "EntityType": "Ticket | Company | Contact | ConfigurationItem | TicketNote", "Id": 123, "Fields": { "status": "1", "title": "..." }, "EventTime": "2024-03-21T10:30:00Z", "SequenceNumber": 1, "PersonId": 456 } ``` ### Supported webhook entities - Companies, Contacts, ConfigurationItems, Tickets, TicketNotes - **Not supported** (returns 404): TimeEntries, Tasks, Projects, Contracts ### Registration ``` POST /api/webhooks/register — register all webhooks POST /api/webhooks/register?entity=Tickets — register single entity DELETE /api/webhooks/register — deregister all GET /api/webhooks/status — view registered webhooks ``` Webhook manager: `lib/services/autotask-webhook-manager.ts` --- ## Rate Limiting Built into `AutotaskClient` via a `RateLimiter` class — **do not add external throttling**. - Limit: 10 requests/second - Implementation: token bucket tracking request timestamps - Behaviour: auto-delays if limit is hit; transparent to callers --- ## Internal API Routes ### Sync ``` POST /api/sync/full — trigger full sync of all entities POST /api/sync/incremental — trigger incremental sync GET /api/sync/status — current sync status GET /api/sync/history — sync history log ``` ### Cached data (read from PostgreSQL, not Autotask directly) ``` GET /api/data/tickets GET /api/data/companies GET /api/data/resources GET /api/data/tasks GET /api/data/configuration-items GET /api/data/time-entries GET /api/data/contacts GET /api/data/contracts GET /api/data/billing-items ``` --- ## Key File Map | Purpose | File | |---------|------| | Main API client | `lib/services/autotask-client.ts` | | Client factory (singleton) | `lib/services/autotask-factory.ts` | | Webhook registration | `lib/services/autotask-webhook-manager.ts` | | Sync orchestration | `lib/services/entity-sync.ts` | | Webhook receiver + verifier | `lib/services/webhook-service.ts` | | Sync helpers (filters, dates) | `lib/utils/sync-helpers.ts` | | Entity field mapper | `lib/utils/entity-mapper.ts` | | TypeScript types | `lib/types/autotask.ts` | | Webhook types | `lib/types/webhook.ts` | | Webhook route handler | `app/api/webhooks/autotask/route.ts` | | Webhook register route | `app/api/webhooks/register/route.ts` | | Tags migration | `migrations/057_create_autotask_tags_tables.sql` | --- ## Common Patterns ### Adding a new synced entity 1. Add types to `lib/types/autotask.ts` 2. Add a fetch method to `AutotaskClient` or use the generic `queryEntityPaginated` 3. Add sync logic in `lib/services/entity-sync.ts` following the incremental filter table above 4. Create a migration for the PostgreSQL table 5. Add a `/api/data/` route if UI needs it 6. Register a webhook in `autotask-webhook-manager.ts` if the entity supports it ### Adding a new client method Follow the pattern in `autotask-client.ts` — use `this.queryEntityPaginated()` for lists, `this.getEntityById()` for single records, `this.updateEntity()` for writes. Rate limiting and auth headers are applied automatically. ### Debugging sync issues - Check `GET /api/sync/status` for in-progress or failed syncs - Incremental syncs use the last sync timestamp stored in PostgreSQL — if stale, run a full sync - Webhook delivery failures appear in the webhook event log (`webhook_events` table)