From autotask
Covers Autotask REST API authentication, query building with 14 operators, pagination, zone detection, rate limiting, and error handling.
How this skill is triggered — by the user, by Claude, or both
Slash command
/autotask:api-patternsWhen to use
When working with authentication, query building, pagination, includes, rate limiting, and error handling in the Autotask REST API. Use when: autotask api, autotask query, autotask authentication, api filter, query builder, autotask pagination, api rate limit, autotask zone, api error, or autotask rest.
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
The Autotask REST API provides access to 215+ entities across the PSA. This skill covers authentication, query building, pagination, error handling, and performance optimization patterns.
The Autotask REST API provides access to 215+ entities across the PSA. This skill covers authentication, query building, pagination, error handling, and performance optimization patterns.
Autotask uses header-based authentication (NOT Basic Auth):
GET /v1.0/Tickets
ApiIntegrationCode: YOUR_INTEGRATION_CODE
UserName: [email protected]
Secret: YOUR_SECRET
Content-Type: application/json
Required Headers:
| Header | Description |
|---|---|
ApiIntegrationCode | Your Autotask integration code |
UserName | API username (email address) |
Secret | API secret/password |
Content-Type | application/json |
export AUTOTASK_USERNAME="[email protected]"
export AUTOTASK_INTEGRATION_CODE="YOUR_INTEGRATION_CODE"
export AUTOTASK_SECRET="YOUR_SECRET"
Autotask operates in multiple zones. The API can automatically detect your zone:
GET https://webservices.autotask.net/atservicesrest/v1.0/ZoneInformation
UserName: [email protected]
Response:
{
"url": "https://webservices5.autotask.net/atservicesrest",
"webUrl": "https://ww5.autotask.net"
}
Common Zones:
| Zone | API URL |
|---|---|
| webservices | https://webservices.autotask.net/atservicesrest |
| webservices1 | https://webservices1.autotask.net/atservicesrest |
| webservices2 | https://webservices2.autotask.net/atservicesrest |
| webservices5 | https://webservices5.autotask.net/atservicesrest |
| webservices6 | https://webservices6.autotask.net/atservicesrest |
The Autotask API supports 14 query operators:
| Operator | Description | Example |
|---|---|---|
eq | Equals | {"field": "status", "op": "eq", "value": 1} |
ne / noteq | Not equals | {"field": "status", "op": "noteq", "value": 5} |
gt | Greater than | {"field": "priority", "op": "gt", "value": 2} |
gte | Greater than or equal | {"field": "createDate", "op": "gte", "value": "2024-01-01"} |
lt | Less than | {"field": "priority", "op": "lt", "value": 3} |
lte | Less than or equal | {"field": "dueDateTime", "op": "lte", "value": "2024-02-15T17:00:00Z"} |
contains | Contains substring | {"field": "title", "op": "contains", "value": "email"} |
startsWith | Starts with | {"field": "companyName", "op": "startsWith", "value": "Acme"} |
endsWith | Ends with | {"field": "email", "op": "endsWith", "value": "@acme.com"} |
in | In array | {"field": "status", "op": "in", "value": [1, 2, 5]} |
notIn | Not in array | {"field": "status", "op": "notIn", "value": [5, 10]} |
isNull | Is null | {"field": "assignedResourceId", "op": "isNull"} |
isNotNull | Is not null | {"field": "dueDateTime", "op": "isNotNull"} |
between | Between range | {"field": "createDate", "op": "between", "value": ["2024-01-01", "2024-01-31"]} |
CRITICAL: To filter for records from "today", you must use a range — gte today's date AND lt tomorrow's date. Using only today's date (e.g. eq or a single filter on today) returns zero results. "Today" in Autotask means: created on or after today's midnight, but before tomorrow's midnight.
Correct — "tickets created today":
{
"filter": [
{"field": "createDate", "op": "gte", "value": "2026-04-13T00:00:00Z"},
{"field": "createDate", "op": "lt", "value": "2026-04-14T00:00:00Z"}
]
}
Wrong — returns NO results:
{
"filter": [
{"field": "createDate", "op": "eq", "value": "2026-04-13"}
]
}
Always compute "tomorrow" dynamically. For example:
const today = new Date();
today.setHours(0, 0, 0, 0);
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const todayISO = today.toISOString(); // "2026-04-13T00:00:00.000Z"
const tomorrowISO = tomorrow.toISOString(); // "2026-04-14T00:00:00.000Z"
This pattern applies to all datetime fields (createDate, lastActivityDate, dueDateTime, startDateTime, endDateTime, dateWorked, etc.) across all entities — not just tickets.
POST /v1.0/Tickets/query
Content-Type: application/json
{
"filter": [
{"field": "companyID", "op": "eq", "value": 12345},
{"field": "status", "op": "noteq", "value": 5}
],
"maxRecords": 50,
"includeFields": ["Company.companyName", "AssignedResource.firstName"]
}
AND conditions (default):
{
"filter": [
{"field": "companyID", "op": "eq", "value": 12345},
{"field": "priority", "op": "lte", "value": 2},
{"field": "status", "op": "in", "value": [1, 2, 5]}
]
}
OR conditions with items array:
{
"filter": [
{
"op": "or",
"items": [
{"field": "priority", "op": "eq", "value": 1},
{"field": "status", "op": "eq", "value": 14}
]
}
]
}
Nested AND/OR:
{
"filter": [
{"field": "companyID", "op": "eq", "value": 12345},
{
"op": "or",
"items": [
{"field": "priority", "op": "in", "value": [3, 4]},
{
"op": "and",
"items": [
{"field": "status", "op": "eq", "value": 1},
{"field": "estimatedHours", "op": "gt", "value": 10}
]
}
]
}
]
}
Retrieve related entity fields in a single request:
{
"filter": [{"field": "id", "op": "gt", "value": 0}],
"includeFields": [
"Company.companyName",
"Company.phone",
"AssignedResource.firstName",
"AssignedResource.lastName",
"Contact.emailAddress"
]
}
Response with includes:
{
"items": [
{
"id": 54321,
"title": "Email issue",
"companyID": 12345,
"companyName": "Acme Corporation",
"companyPhone": "555-123-4567",
"assignedResourceFirstName": "Jane",
"assignedResourceLastName": "Tech"
}
]
}
{
"filter": [{"field": "id", "op": "gt", "value": 0}],
"maxRecords": 100,
"pageNumber": 1
}
Pagination Fields:
| Field | Description | Max |
|---|---|---|
maxRecords | Records per page | 500 |
pageNumber | Current page (1-based) | - |
{
"items": [...],
"pageDetails": {
"count": 100,
"nextPageUrl": "/v1.0/Tickets/query?pageNumber=2",
"prevPageUrl": null,
"requestCount": 2847
}
}
async function fetchAllTickets(filter) {
const allItems = [];
let pageNumber = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch('/v1.0/Tickets/query', {
method: 'POST',
body: JSON.stringify({
filter,
maxRecords: 500,
pageNumber
})
});
const data = await response.json();
allItems.push(...data.items);
hasMore = data.pageDetails.nextPageUrl !== null;
pageNumber++;
}
return allItems;
}
Autotask enforces two hard limits:
| Limit | Value | Scope |
|---|---|---|
| Concurrent threads per endpoint | 3 | Per API tracking identifier (your integrationCode) |
| Total requests per hour | 10,000 | Per Autotask tenant database (all integrations combined) |
Concurrent thread limit is the most common cause of slowdowns in LLM-driven integrations. When Claude issues several tool calls in parallel (e.g., tickets search + companies search + contacts search), all three may target the Tickets endpoint simultaneously and hit the 3-thread cap.
When using the MCP server or autotask-node SDK, this is handled automatically — excess requests are queued and released as slots free up, so you won't see hard failures, but responses may be slower under load.
Multi-user / shared key risk: The 3-thread limit applies per integrationCode. If multiple users or teams share the same credentials, they compete for the same 3 slots. In a team deployment, give each team their own API user:
Support Team → integrationCode: SUPPORT_TEAM_CODE (3 threads, independent)
Projects Team → integrationCode: PROJECTS_TEAM_CODE (3 threads, independent)
When the concurrent thread limit or hourly request limit is exceeded (HTTP 429):
{
"errors": [
{
"message": "Rate limit exceeded. Try again in 30 seconds."
}
]
}
async function requestWithRetry(url, options, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 30;
const jitter = Math.random() * 1000;
await sleep(retryAfter * 1000 + jitter);
continue;
}
return response;
} catch (error) {
if (attempt === maxRetries - 1) throw error;
// Exponential backoff with jitter
const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
await sleep(delay);
}
}
}
To maximize throughput without hitting the per-endpoint thread limit, query different endpoints in parallel rather than the same endpoint multiple times:
// Good: parallel requests to different endpoints — each has its own 3-thread budget
const [tickets, companies, contacts] = await Promise.all([
client.tickets.query().where('status', 'in', [1, 5]).execute(),
client.companies.query().where('companyType', 'eq', 1).execute(),
client.contacts.query().where('isActive', 'eq', true).execute(),
]);
// Avoid: parallel requests to the SAME endpoint — they share 3 threads
// (will queue automatically, but adds latency)
const [page1, page2, page3] = await Promise.all([
client.tickets.query().pageNumber(1).execute(), // ← same endpoint
client.tickets.query().pageNumber(2).execute(), // ← same endpoint
client.tickets.query().pageNumber(3).execute(), // ← same endpoint
]);
For bulk operations, batch requests to avoid the hourly limit:
async function batchProcess(items, batchSize = 50, delayMs = 1000) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(item => processItem(item))
);
results.push(...batchResults);
if (i + batchSize < items.length) {
await sleep(delayMs);
}
}
return results;
}
| Code | Meaning | Action |
|---|---|---|
| 200 | Success | Process response |
| 201 | Created | Entity created successfully |
| 400 | Bad Request | Check request format/values |
| 401 | Unauthorized | Verify credentials |
| 403 | Forbidden | Check permissions |
| 404 | Not Found | Entity doesn't exist |
| 409 | Conflict | Resource locked/modified |
| 429 | Rate Limited | Implement backoff |
| 500 | Server Error | Retry with backoff |
{
"errors": [
{
"message": "The value '999' is not valid for field 'status'.",
"field": "status",
"value": 999
}
]
}
function handleApiError(response) {
if (!response.errors) return;
response.errors.forEach(error => {
console.log(`Error: ${error.message}`);
if (error.field) {
console.log(` Field: ${error.field}`);
console.log(` Invalid Value: ${error.value}`);
// Suggest fix based on field
if (error.field === 'status') {
console.log(' Suggestion: Query /v1.0/Tickets/entityInformation/fields for valid status IDs');
} else if (error.field === 'queueID') {
console.log(' Suggestion: Query /v1.0/Queues for valid queue IDs');
}
}
});
}
GET /v1.0/Tickets/entityInformation/fields
Response:
{
"fields": [
{
"name": "status",
"dataType": "Integer",
"isRequired": true,
"isPickList": true,
"picklistValues": [
{"value": 1, "label": "New"},
{"value": 2, "label": "In Progress"},
{"value": 5, "label": "Complete"}
]
}
]
}
GET /v1.0/Tickets/entityInformation/userDefinedFields
POST /v1.0/Tickets
Content-Type: application/json
{
"companyID": 12345,
"title": "New ticket",
"status": 1,
"priority": 2,
"queueID": 8
}
Single entity:
GET /v1.0/Tickets/54321
Query:
POST /v1.0/Tickets/query
PATCH /v1.0/Tickets
Content-Type: application/json
{
"id": 54321,
"status": 2,
"assignedResourceID": 29744150
}
PUT /v1.0/Tickets/54321
Content-Type: application/json
{
"id": 54321,
"companyID": 12345,
"title": "Updated ticket",
"status": 2,
"priority": 2,
"queueID": 8
}
DELETE /v1.0/Tickets/54321
Note: Not all entities support DELETE. Check entity documentation.
{
"filter": [{"field": "id", "op": "gt", "value": 0}],
"fields": ["id", "title", "status", "priority"]
}
Good - Uses indexed field:
{"field": "companyID", "op": "eq", "value": 12345}
Avoid - Full text search:
{"field": "description", "op": "contains", "value": "error"}
Cache slowly-changing data:
const cache = new Map();
async function getQueues() {
if (!cache.has('queues') || cache.get('queues').expires < Date.now()) {
const queues = await fetchQueues();
cache.set('queues', {
data: queues,
expires: Date.now() + 5 * 60 * 1000 // 5 minutes
});
}
return cache.get('queues').data;
}
integrationCode. Each team using the integration should have their own API user so they don't compete for the same thread budgetnpx claudepluginhub wyre-technology/msp-claude-plugins --plugin autotaskProvides patterns for ConnectWise PSA REST API authentication, pagination, query syntax, rate limiting, and error handling.
Authenticates, paginates, filters (OData), and handles errors for the ConnectWise Automate REST API. Covers token management and best practices for integrations.
Covers authentication, pagination, rate limiting, and error handling for the Atera REST API using X-API-KEY, OData-style pagination, and 700 req/min rate limits.