docs: add AUTOTASK_API_GUIDE.md to docs folder

This commit is contained in:
lorentz 2026-03-21 18:07:30 -04:00
parent 679fe3871c
commit 9459d65e02

448
docs/AUTOTASK_API_GUIDE.md Normal file
View file

@ -0,0 +1,448 @@
# 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)