448 lines
12 KiB
Markdown
448 lines
12 KiB
Markdown
# 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: <AUTOTASK_USERNAME>
|
|
Secret: <AUTOTASK_SECRET>
|
|
APIIntegrationcode: <AUTOTASK_API_INTEGRATION_CODE>
|
|
Content-Type: application/json
|
|
Accept: application/json
|
|
```
|
|
|
|
Optional delegation header:
|
|
|
|
```
|
|
ImpersonationResourceID: <resourceId>
|
|
```
|
|
|
|
**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<Company>('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<Ticket>('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<T>(entity, params) // single page
|
|
client.queryEntityPaginated<T>(entity, params, pageSize) // all pages
|
|
client.getEntityById<T>(entity, id)
|
|
client.createEntity<T>(entity, data)
|
|
client.updateEntity<T>(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=<base64>`
|
|
- 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/<entity>` 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)
|