autotask-rest-api-types
v0.1.1
Published
Comprehensive, generated TypeScript types for the Datto/Kaseya Autotask PSA REST API. Strongly-typed entities, query filters, responses, and a typed client surface for the apigrate/autotask-restapi connector. Built for Next.js and TS projects.
Downloads
267
Maintainers
Readme
autotask-rest-api-types
TypeScript types for the Datto/Kaseya Autotask PSA REST API, generated from Autotask's official Swagger 2.0 spec. Entity interfaces, typed query filters and response envelopes, a typed client surface, and a machine-readable catalog of every collection — for Next.js/TypeScript integrations, tooling, and codegen.
- 223 entity interfaces (Companies, Tickets, Contacts, Contracts, Configuration Items, Projects, Time Entries, attachments, …) with per-field JSDoc.
- 221 REST collections mapped to their entity in
AutotaskEntities. - Query filters — all 13 query operators plus grouped
and/or,includeFields, a fluent filter DSL, and cursor pagination helpers. - Response envelopes —
{ items, pageDetails },{ item },{ itemId }, plus entity-information / live picklist metadata shapes. - Two client surfaces — typing for the
@apigrate/autotask-restapiconnector, and a provider-agnosticAutotaskTypedClientyou implement overfetch. - Runtime catalog —
AUTOTASK_COLLECTIONSdescribes every collection (operations, UDF support, parent collections). - Webhook helpers — typed delivery parsing, HMAC verification, registration plan builders, and an optional router.
- Instance enrichment —
npx autotask-enrich(read-only) captures your instance's live picklist values, required/read-only flags, and reference targets into your project, plus strict picklist type aliases (TicketPriorityValue = 1 | 2 | 3 | 4). Instance data is never bundled. - Type-only by default — no runtime dependencies; tree-shakeable; ESM with full
.d.ts.
Unofficial / community-maintained. "Autotask" is a trademark of Datto/Kaseya. Generated from the public API spec; not affiliated with or endorsed by Datto or Kaseya.
Install
pnpm add autotask-rest-api-types
# or: npm i autotask-rest-api-types / yarn add autotask-rest-api-typesInstall it as a normal dependency — the package ships a few runtime helpers (filters, collectAll/iterateAll, getUdf/toUdfArray, writtenId, requireField, isPresent, isAutotaskError, and the AUTOTASK_COLLECTIONS catalog). If you import only types (import type …), a dev dependency (-D) is fine.
The package has no runtime dependencies of its own. If you also want a transport, the examples below use the official connector:
pnpm add @apigrate/autotask-restapiQuick start — with the apigrate connector
Cast the connector instance to AutotaskApi for end-to-end typing:
import { AutotaskRestApi } from "@apigrate/autotask-restapi";
import type { AutotaskApi, Company } from "autotask-rest-api-types";
import { filters } from "autotask-rest-api-types";
const autotask = new AutotaskRestApi(
process.env.AUTOTASK_USER!, // API user (UserName header)
process.env.AUTOTASK_SECRET!, // secret (Secret header)
process.env.AUTOTASK_INTEGRATION_CODE!, // tracking id (ApiIntegrationCode header)
) as unknown as AutotaskApi;
const f = filters<Company>();
const { items, pageDetails } = await autotask.Companies.query({
maxRecords: 50,
includeFields: ["id", "companyName", "isActive"],
filter: [f.eq("isActive", true), f.beginsWith("companyName", "A")],
});
items.forEach((c: Company) => console.log(c.id, c.companyName));filter fields and includeFields autocomplete from the entity; items is Company[].
Quick start — provider-agnostic (fetch, Next.js route handler, etc.)
Implement the small AutotaskTypedClient interface over any transport. The entity type is inferred from the collection name on every call:
import type { AutotaskTypedClient } from "autotask-rest-api-types";
// (sketch) your own thin wrapper around fetch + zone detection
const client: AutotaskTypedClient = createMyClient();
// `tickets` is typed as Ticket[] — inferred purely from the string "Tickets".
const { items: tickets } = await client.query("Tickets", {
filter: [{ op: "eq", field: "status", value: 1 }],
});See skills/autotask-rest-api for the AI-assistant guide and a complete fetch-based client (header auth + zone detection), and examples/nextjs for a runnable Next.js Ticket Console (simulated without credentials, live when present).
Error handling
Failed requests return an { errors: string[] } envelope (HTTP 400/500) — invalid picklist values, maxRecords > 500, missing required fields, etc. Type and narrow it with AutotaskErrorResponse / isAutotaskError:
import { isAutotaskError } from "autotask-rest-api-types";
const res = await fetch(url, { method: "POST", headers, body });
if (!res.ok) {
const body = await res.json();
if (isAutotaskError(body)) console.error(body.errors); // string[]
}Connector vs
fetch. The apigrate connector throws on a failed request instead of returning the envelope — the call rejects with anAutotaskApiErrorcarrying.statusand.details(the{ errors }body). Wrap those calls intry/catch; theisAutotaskErrorcheck above is for the rawfetch/AutotaskTypedClientpath, which returns the body.
Webhooks
Webhook helpers are available from the root package and from the tree-shakeable subpath:
import {
AUTOTASK_SIGNATURE_HEADER,
isDeliveryFor,
isUpdateDelivery,
parseWebhookDelivery,
verifyAutotaskSignature,
} from "autotask-rest-api-types/webhooks";Receive and verify a delivery
Read the raw body before parsing JSON. Autotask signs the raw request body with the webhook secret:
export async function POST(req: Request) {
const secret = process.env.AUTOTASK_WEBHOOK_SECRET;
if (!secret) return new Response("Missing AUTOTASK_WEBHOOK_SECRET", { status: 500 });
const raw = await req.text();
const signature = req.headers.get(AUTOTASK_SIGNATURE_HEADER);
// If real callouts return 401, retry with `{ escapeBody: true }`.
// That escape pass is reverse-engineered and undocumented, so keep it opt-in.
const ok = await verifyAutotaskSignature(raw, secret, signature);
if (!ok) return new Response("Invalid signature", { status: 401 });
const delivery = parseWebhookDelivery(JSON.parse(raw));
if (isDeliveryFor(delivery, "Tickets") && isUpdateDelivery(delivery)) {
const title = delivery.Fields.title; // string | null | undefined
// Update callouts contain subscribed/changed fields only.
void title;
}
return Response.json({ ok: true });
}parseWebhookDelivery validates Action, accepts Autotask's legacy EntityType names such as Account, and normalizes them to package collection names such as Companies. The field payload is deliberately permissive because a raw live callout still needs to confirm exact Fields casing, UDF nesting, and whether delete/deactivation deliveries omit Fields or send {}.
Plan webhook registration
buildWebhookRegistrationPlan creates a parent-first plan with child field, UDF-field, and excluded-resource rows. If you already have numeric field ids, the executor creates the parent, reads the id with writtenId, and injects webhookID into child bodies:
import {
buildWebhookRegistrationPlan,
executeWebhookRegistrationPlan,
} from "autotask-rest-api-types/webhooks";
const plan = buildWebhookRegistrationPlan("Ticket", {
webhook: {
name: "Ticket updates",
webhookUrl: "https://example.com/api/autotask",
isSubscribedToUpdateEvents: true,
},
fields: [{ fieldID: 12, subscribed: true }],
excludedResourceIDs: [apiUserResourceId],
});
await executeWebhookRegistrationPlan(plan, async (step, body, parentId) => {
// Call your client here. Child steps receive `parentId` and a body containing `webhookID`.
return createWebhookRow(step.collection, body, parentId);
});If you start from field names, resolve them from the live webhook child collection metadata, not from Tickets.fieldInfo() or the parent webhook route. The flow is:
- Create or find the parent webhook and normalize its id with
writtenId(res). - Query
TicketWebhookFields/entityInformation/fieldsand read thefieldIDpicklist. For UDFs, queryTicketWebhookUdfFields/entityInformation/fieldsand readudfFieldID. - Build
nameToId, callresolveFieldIds(["title", "status"], nameToId), then create the child rows with{ ...step.body, webhookID: webhookId }.
Webhook endpoints should acknowledge quickly with a 2xx response, dedupe on Guid, and keep retry/queue/persistence concerns in your app. A successful registration can still fail to fire if isReady is false or the owner resource lacks permission; sustained failures can deactivate a webhook.
Entities & the collection registry
Import any entity interface directly:
import type { Ticket, Contact, ConfigurationItem, ContractService } from "autotask-rest-api-types";Map a collection name to its entity type generically:
import type { AutotaskEntities, EntityName, EntityOf } from "autotask-rest-api-types";
type T = AutotaskEntities["Tickets"]; // Ticket
type C = EntityOf<"Companies">; // Company
function load<K extends EntityName>(name: K): Promise<EntityOf<K>[]> { /* ... */ }Field typing conventions
Generated from the spec; the type mapping is:
| Swagger | TypeScript | Notes |
|---|---|---|
| integer (int32 / int64) | number | Autotask ids stay within JS safe-integer range |
| number (double) | number | |
| string | string | |
| string (date-time) | string | UTC, no Z/offset, e.g. 2024-01-31T15:04:05.307; append "Z" to parse |
| string (byte) | string | base64 (attachment data) |
| boolean | boolean | |
| userDefinedFields | UserDefinedField[] | name/value pairs (see below) |
Every field is optional and nullable (field?: T | null). This is deliberate and reflects the API's real behavior: Autotask returns null for empty fields and omits any field you leave out of includeFields. id is readonly id?: number (server-assigned, never null on a fetched record). Read-only fields (server-managed, e.g. createDate) carry the readonly modifier and JSDoc.
Working with nullable fields
Helpers cut down on null-narrowing boilerplate:
import { requireField, isPresent } from "autotask-rest-api-types";
import type { Loaded, WithId, Company } from "autotask-rest-api-types";
// get() resolves to bare `null` on a 404 (the apigrate connector returns the
// API body verbatim), so null-check the result before destructuring `item`.
const res = await autotask.Companies.get(1);
if (res?.item) {
const name = requireField(res.item, "companyName"); // string — throws if null/absent
const loaded = res.item as Loaded<Company>; // every field non-null (assertion of intent)
loaded.companyName.trim();
}
const active = companies.filter((c) => isPresent(c.companyName)); // typed narrowing in filter
const withId: WithId<Company> = { id: 1, companyName: "Acme" }; // id guaranteed presentLegacy business-object names
Autotask's REST collection names differ from its older SOAP/business-object names: Company = Account, ConfigurationItem = InstalledProduct, BillingCode = AllocationCode, etc. Each entity's legacy name is in AUTOTASK_COLLECTIONS[name].entity, and ENTITY_TO_COLLECTION resolves the reverse:
import { ENTITY_TO_COLLECTION } from "autotask-rest-api-types";
ENTITY_TO_COLLECTION["Account"]; // "Companies"
ENTITY_TO_COLLECTION["InstalledProduct"]; // "ConfigurationItems"Querying
Queries follow the Swagger QueryModel: { maxRecords?, includeFields?, filter[] }. Top-level filter entries are combined with AND; use explicit and / or groups for anything else.
Operators
| Kind | Operators |
|---|---|
| Comparison | eq, noteq, gt, gte, lt, lte, beginsWith, endsWith, contains |
| Existence (no value) | exist, notExist |
| Set (array value) | in, notIn |
| Grouping (nested items) | and, or |
As plain objects
import type { AutotaskQuery, Ticket } from "autotask-rest-api-types";
const q: AutotaskQuery<Ticket> = {
maxRecords: 100,
includeFields: ["id", "ticketNumber", "title", "status"],
filter: [
{ op: "eq", field: "companyID", value: 123 },
{ op: "in", field: "status", value: [1, 5, 8] },
{ op: "or", items: [
{ op: "gte", field: "createDate", value: "2024-01-01T00:00:00Z" },
{ op: "exist", field: "lastActivityDate" },
] },
],
};With the fluent DSL
import { filters } from "autotask-rest-api-types";
import type { Ticket } from "autotask-rest-api-types";
const f = filters<Ticket>();
const q = {
filter: [
f.eq("companyID", 123),
f.in("status", [1, 5, 8]),
f.or(f.gte("createDate", "2024-01-01T00:00:00Z"), f.exist("lastActivityDate")),
f.udf("CustomerRef", "eq", "ACME-001"), // user-defined field
],
} satisfies AutotaskQuery<Ticket>;Pagination
Autotask returns up to 500 records per page (MAX_PAGE_SIZE) with a pageDetails block. To pull an entire result set, use the built-in id-cursor helpers (no reliance on nextPageUrl):
import { collectAll, iterateAll } from "autotask-rest-api-types";
// Buffer everything:
const all = await collectAll((q) => autotask.Tickets.query(q), {
filter: [{ op: "eq", field: "companyID", value: 123 }],
});
// Or stream page-by-page:
for await (const ticket of iterateAll((q) => autotask.Tickets.query(q), baseQuery)) {
// ...
}Create / Update / Delete
import type { CreateModel, UpdateModel, Ticket } from "autotask-rest-api-types";
import { writtenId } from "autotask-rest-api-types";
// Create — omit `id` (server-assigned). Returns HTTP 200 with { itemId }.
const create: CreateModel<Ticket> = { companyID: 1, title: "Server down", status: 1, priority: 2 };
const res = await autotask.Tickets.create(create);
const id = writtenId(res); // normalize itemId to a number
// Update (PATCH, partial) — only the fields you send change. `id` is required.
const patch: UpdateModel<Ticket> = { id, status: 5 };
await autotask.Tickets.update(patch);
// Delete — only on entities that allow it. Tickets are NOT deletable via the API
// (AUTOTASK_COLLECTIONS.Tickets.canDelete === false); a TimeEntry, for example, is:
await autotask.TimeEntries.delete(someTimeEntryId);CreateInput<"Tickets"> / UpdateInput<"Tickets"> are convenience aliases keyed by collection name.
PATCH vs PUT.
update()is a PATCH (sparse): only the fields you send change.replace()is a PUT (full replace): every writable field you omit is set tonull/default. The types enforce this —replace()takes aReplaceModel<T>that requires every writable field (read-only excluded), so a partial like{ id, title }won't compile; build it from a fully-loaded record (replace({ ...loaded, title })). Preferupdate()unless you deliberately want a full replace. Create, update, and replace all return HTTP 200 with{ itemId }(Autotask does not use201).
Many entities can't be deleted via the API (e.g.
Companies,Tickets), and child entities like*Notesare created/updated under their parent (POST /Companies/{parentId}/Notes). The runtime catalog records this — checkAUTOTASK_COLLECTIONS[name].canDeleteand.parentWriteOnly/.parentsbefore assuming an operation exists.
User-defined fields (UDFs)
UDFs arrive as an array of { name, value } pairs (values are always strings on the wire):
import { getUdf, toUdfArray } from "autotask-rest-api-types";
const ref = getUdf(ticket.userDefinedFields, "CustomerRef"); // string | null
await autotask.Tickets.update({
id: 123,
userDefinedFields: toUdfArray({ CustomerRef: "ACME-001", SlaTier: 2 }),
});Live metadata & picklists
Picklist option values are instance-specific and therefore not in the static spec — fetch them at runtime via the entity-information endpoints, which are fully typed here:
import type { FieldInformationResult, PickListValue } from "autotask-rest-api-types";
const { fields } = await autotask.Tickets.fieldInfo();
const status = fields.find((x) => x.name === "status");
status?.picklistValues?.forEach((v: PickListValue) => console.log(v.value, "→", v.label));info(), fieldInfo(), and udfInfo() are typed to EntityInformationResult, FieldInformationResult, and UserDefinedFieldInformationResult.
Captured field metadata (generate your own — never bundled)
Picklist values, required/read-only flags, and reference targets are instance-specific, so they are not bundled. Generate a snapshot of your instance into your project:
# read-only against your instance; reads creds from ./.env in your project
npx autotask-enrich ./autotask-field-metadata.ts.env keys recognized (case-insensitive): user (APIUSER/AUTOTASK_USER), secret (APISECRET/AUTOTASK_SECRET), tracking code (TRACKINGID/AUTOTASK_INTEGRATION_CODE), optional full zone URL (AUTOTASK_BASE_URL). The tool only issues GET requests — it can't create, modify, or delete anything.
The generated file exports lookup helpers and opt-in strict picklist types, which you import from your own path:
import {
getFieldMeta, getPicklist, picklistLabel, picklistValue, FIELD_METADATA,
type TicketPriorityValue,
} from "./autotask-field-metadata";
getFieldMeta("Tickets", "status")?.isRequired; // true
getFieldMeta("Tickets", "assignedResourceID")?.referenceEntityType; // "Resource"
getPicklist("Companies", "companyType"); // [{ value: 1, label: "Customer" }, …]
picklistLabel("Tickets", "priority", 1); // "High"
picklistValue("Tickets", "priority", "Critical"); // 4
const priority: TicketPriorityValue = 2; // 1 | 2 | 3 | 4 — `99` would be a compile errorThese values mirror the instance they were captured from and change with your Autotask configuration — re-run the command to refresh. Add the generated file to
.gitignoreif your instance config is sensitive.
Runtime collection catalog
AUTOTASK_COLLECTIONS is a typed as const map of every REST collection:
import { AUTOTASK_COLLECTIONS, COLLECTION_NAMES } from "autotask-rest-api-types";
AUTOTASK_COLLECTIONS.Tickets;
// {
// name: "Tickets", entity: "Ticket", model: "TicketModel", typed: true,
// canQuery: true, canCount: true, canGet: true, canCreate: true,
// canUpdate: true, canDelete: false, hasUserDefinedFields: true,
// parentWriteOnly: false, deleteRequiresParent: false, parents: []
// }
const updatable = COLLECTION_NAMES.filter((n) => AUTOTASK_COLLECTIONS[n].canUpdate);Design notes & limitations
- Everything is
| nulland optional. Intentional — it forces you to handle Autotask's empty-as-null reality and partial responses. Narrow with a guard (if (c.companyName != null) …) when you need a concrete value. - Filter
valueis not correlated to the field's type.valueisstring | number | boolean. This is required so UDF names and dotted child-field paths (arbitrary strings) remain validfields — TypeScript can't subtract known keys fromstring, so correlation would break those legitimate queries. - Picklist fields are
numberin the base interfaces (their valid codes are instance-specific). For real values + strict types, use your own generated./autotask-field-metadatafile (getPicklist,picklistLabel, and the*Valuealiases — see Captured field metadata) or callfieldInfo()live. - Reference (foreign-key) fields are
numberin the base interfaces; your generated./autotask-field-metadata(referenceEntityType) and livefieldInfo()both report the target entity. CollectionName⊋EntityName.AUTOTASK_COLLECTIONS/CollectionNamecover every REST path including action-only ones (Version,ZoneInformation,Authenticate, …). The client surfaces useEntityName/TypedCollectionName— only collections with a generated entity interface (typed: true).exactOptionalPropertyTypescompatible. Types are authored as?: T | null; the| nullmirrors the wire. Works with the flag on or off.
Rate limits
Autotask allows 10,000 external requests per hour per database (across all integrations), and adds latency as you approach it (0.5s at 50%, 1s at 75%+). Check current usage via GET /ThresholdInformation (typed as ThresholdInformation). Combine with the collectAll / iterateAll helpers, which page at the 500-record server cap.
Regenerating from a newer spec
The committed swagger.json is the single source of truth. To refresh against a newer API version:
curl -s https://webservices.autotask.net/ATServicesRest/swagger/docs/v1 -o swagger.json
npm run generate # rewrites src/generated/*
npm run buildscripts/generate.mjs is the only generator; hand-written code in src/core and src/client.ts is never touched.
To capture instance-specific picklist/field metadata for local development (read-only API calls, credentials from .env):
npm run enrich # writes src/generated/field-metadata.ts (git-ignored, NOT published)
npm run buildThe output is instance-specific, git-ignored, and excluded from the published tarball. Consumers of the published package generate their own with npx autotask-enrich (see Captured field metadata).
AI coding assistants
This package ships an agent skill at skills/autotask-rest-api/SKILL.md — the rules for generating correct Autotask code (the null-everywhere field shape, instance-specific picklists, PATCH vs PUT, the exact operator set) and a complete fetch client. Point your assistant at it, or have it read the runtime AUTOTASK_COLLECTIONS catalog for what each collection supports.
License
MIT.
