Autotask API Patterns — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Autotask API Patterns (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
lazy-loaded MCP connection exposes only four meta-tools. That is a discovery problem, not an auth or query problem; use autotask-tool-discovery.
filter DSL will happily match an ID that does not exist in your tenant and return nothing. Fetch the real values with autotask-picklists.
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/jsonRequired 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"]
}Filters combine with implicit AND. Use "op": "or" / "op": "and" with an items array to group or nest conditions:
{
"filter": [
{"field": "companyID", "op": "eq", "value": 12345},
{
"op": "or",
"items": [
{"field": "priority", "op": "eq", "value": 1},
{"field": "status", "op": "eq", "value": 14}
]
}
]
}See references/api.md for an AND-only example and a nested AND/OR example.
Retrieve related entity fields in a single request:
{
"filter": [{"field": "id", "op": "gt", "value": 0}],
"includeFields": [
"Company.companyName",
"AssignedResource.firstName",
"AssignedResource.lastName"
]
}See references/api.md for the response shape returned when includes are used.
{
"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
}
}Page through results by looping while pageDetails.nextPageUrl is non-null. See references/examples.md for a full pagination loop implementation.
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."
}
]
}Implement exponential backoff with jitter on 429 responses, honoring Retry-After when present. See references/examples.md for a retry-with-backoff implementation.
To maximize throughput without hitting the per-endpoint thread limit, query different endpoints in parallel rather than the same endpoint multiple times — parallel requests to Tickets + Companies + Contacts each get their own 3-thread budget, while parallel requests to multiple pages of the same endpoint share one budget and queue.
For bulk write operations, batch requests in groups (e.g. 50 at a time) with a short delay between batches to avoid the hourly limit. See references/examples.md for parallel-query and batch-processing code.
| 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
}
]
}When a field-level error is returned, cross-reference the field against /v1.0/<Entity>/entityInformation/fields (or /v1.0/Queues for queue IDs) to find valid values. See references/examples.md for a full validation-error handler.
Query field definitions and picklist values before writing to an unfamiliar entity:
GET /v1.0/Tickets/entityInformation/fields
GET /v1.0/Tickets/entityInformation/userDefinedFieldsSee references/api.md for the full response shape and CRUD (create/read/update/replace/delete) request examples.
{
"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 — Queues, Resources, Issue Types, Priorities, Company lists — rather than re-fetching per request. See references/examples.md for a TTL-based cache pattern.
integrationCode. Each team using the integration should have their own API user so they don't compete for the same thread budget~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.