From 9459d65e02bd32df885133e25ece3918ec94ce59 Mon Sep 17 00:00:00 2001 From: lorentz Date: Sat, 21 Mar 2026 18:07:30 -0400 Subject: [PATCH] docs: add AUTOTASK_API_GUIDE.md to docs folder --- docs/AUTOTASK_API_GUIDE.md | 448 +++++++++++++++++++++++++++++++++++++ 1 file changed, 448 insertions(+) create mode 100644 docs/AUTOTASK_API_GUIDE.md diff --git a/docs/AUTOTASK_API_GUIDE.md b/docs/AUTOTASK_API_GUIDE.md new file mode 100644 index 0000000..d64a633 --- /dev/null +++ b/docs/AUTOTASK_API_GUIDE.md @@ -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: +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)