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