docs: add AUTOTASK_API_GUIDE.md to docs folder
This commit is contained in:
parent
679fe3871c
commit
9459d65e02
1 changed files with 448 additions and 0 deletions
448
docs/AUTOTASK_API_GUIDE.md
Normal file
448
docs/AUTOTASK_API_GUIDE.md
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue