npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@panoptic-it-solutions/autotask-client

v0.11.0

Published

A Node.js SDK for interacting with the Autotask API.

Readme

@panoptic-it-solutions/autotask-client

A Node.js SDK for interacting with the Autotask REST API, featuring built-in rate limiting, TypeScript support, automatic pagination, and relationship embedding.

Table of Contents

Installation

npm install @panoptic-it-solutions/autotask-client
# or
yarn add @panoptic-it-solutions/autotask-client
# or
pnpm add @panoptic-it-solutions/autotask-client

Full Usage Guide

For comprehensive examples covering setup, querying, nested create endpoints (Notes/Attachments), updates, deletes, field discovery, and error handling, see the usage guide in the repository docs: autotask-client/docs.

Configuration

This SDK requires the following environment variables to be set for basic Autotask API interaction:

  • AUTOTASK_USERNAME: Your Autotask API User's username (Access Key in Autotask).
  • AUTOTASK_SECRET: Your Autotask API User's secret (Secret Key in Autotask).
  • AUTOTASK_API_INTEGRATION_CODE: Your Autotask API Tracking Identifier.
  • AUTOTASK_API_URL: (Optional) The base URL for the Autotask REST API specific to your zone (e.g., https://webservices[XX].autotask.net/ATServicesRest/v1.0/). If not provided, the client will use a default.

Upstash Redis for Rate Limiting (Recommended)

For robust, distributed rate limiting (essential for serverless environments or horizontally scaled applications), the client integrates with Upstash Redis.

Required Environment Variables for Upstash Rate Limiting:

  • UPSTASH_REDIS_REST_URL: The REST URL for your Upstash Redis instance.
  • UPSTASH_REDIS_REST_TOKEN: The read/write token for your Upstash Redis instance.

You can find these credentials in your Upstash console after creating a Redis database.

Optional Upstash Rate Limit Configuration (via AutotaskClientConfig):

Default rate limits (e.g., 5 requests per second, 5000 per hour) can be overridden in the AutotaskClientConfig (see Client Instantiation). Environment variables UPSTASH_HOURLY_LIMIT and UPSTASH_SECONDLY_LIMIT are no longer directly used for overriding these; use the config object instead.

If Upstash credentials are not provided in the config, the client will operate without proactive distributed rate limiting, relying solely on reactive backoff for Autotask API 429 responses. This is not recommended for production or distributed environments.

Example .env File

It's recommended to use a library like dotenv to manage these variables. Create a .env file in the root of your project:

# Autotask API Credentials
AUTOTASK_USERNAME="YOUR_API_USERNAME"
AUTOTASK_SECRET="YOUR_API_SECRET"
AUTOTASK_API_INTEGRATION_CODE="YOUR_API_INTEGRATION_CODE"
AUTOTASK_API_URL="https://your-zone.autotask.net/ATServicesRest/v1.0/"

# Upstash Redis Credentials (Recommended for Rate Limiting)
UPSTASH_REDIS_REST_URL="your_upstash_redis_url_here"
UPSTASH_REDIS_REST_TOKEN="your_upstash_redis_token_here"

Load these variables at the beginning of your application:

import * as dotenv from "dotenv";
dotenv.config();

Usage

Client Instantiation

import {
  AutotaskClient,
  AutotaskClientConfig,
  LogLevel, // For setting log levels
} from "@panoptic-it-solutions/autotask-client";
import * as dotenv from "dotenv";

// Load environment variables
dotenv.config();

const clientConfig: AutotaskClientConfig = {
  userName: process.env.AUTOTASK_USERNAME!,
  secret: process.env.AUTOTASK_SECRET!,
  integrationCode: process.env.AUTOTASK_API_INTEGRATION_CODE!,
  baseUrl: process.env.AUTOTASK_API_URL!, // Optional: Base URL for the Autotask API
  // Optional Upstash configuration for rate limiting
  upstash: {
    upstashRedisUrl: process.env.UPSTASH_REDIS_REST_URL, // Can be undefined if not using Upstash
    upstashRedisToken: process.env.UPSTASH_REDIS_REST_TOKEN, // Can be undefined
    // hourlyLimit: 4500, // Example: Override default hourly limit
    // secondlyLimit: 4,    // Example: Override default per-second limit
  },
  // Optional retry configuration (client has sensible defaults)
  retryConfig: {
    maxRetries: 5,
    initialBackoffMs: 1000,
  },
  // Optional logging configuration
  logLevel: LogLevel.INFO, // Supported levels: DEBUG, INFO, WARN, ERROR, NONE
  // logger: new MyCustomLogger(), // Optionally provide a custom logger instance
};

const client = new AutotaskClient(clientConfig);

Creating Entities (createEntity)

Writes are entity-dependent. For child entities (e.g., Notes/Attachments), the client auto-routes to nested create endpoints when the relevant parent ID is present in the payload.

// Generic create
const company = await client.createEntity('Companies', {
  companyName: 'Example Corp',
  companyType: 1,
});

// Nested create (auto-routed): ProjectNotes → POST Projects/{projectID}/Notes
const note = await client.createEntity('ProjectNotes', {
  projectID: 64,
  title: 'Kickoff summary',
  description: 'Plain text',
  noteType: 5,
  publish: 1,
  isAnnouncement: false,
});

Updating Entities

Important: Update support is entity-dependent. The SDK uses PATCH /{entity} with the id included in the body. If the entity doesn't support PATCH, the API will return an error which the SDK surfaces.

// Standard entity update
const updated = await client.updateEntity('Companies', company.id!, {
  phone: '555-9999'
});

// Child entity updates (TicketChecklistItems, TicketNotes, Tasks)
// The client automatically routes these to the correct parent-scoped endpoints
const updatedChecklistItem = await client.updateEntity('TicketChecklistItems', itemId, {
  isCompleted: true,
  itemName: 'Updated item'
  // ticketID is optional - automatically looked up if not provided
});

const updatedNote = await client.updateEntity('TicketNotes', noteId, {
  title: 'Updated Title',
  description: 'Updated content'
  // ticketID is optional - automatically looked up if not provided
});

Automatic Parent ID Lookup: For child entities like TicketChecklistItems and TicketNotes, the client automatically determines the parent ID (e.g., ticketID) if not provided, and routes the request to the correct nested endpoint (e.g., /Tickets/{ticketID}/ChecklistItems). This eliminates 404 errors when updating these entities.

Querying a Single Page of Entities (queryEntities)

Use queryEntities to fetch a single page of entities.

import {
  AutotaskQueryOptions,
  AllAutotaskEntityNames,
  AutotaskEntityTypeMap,
  SdkClientPagedResponse,
} from "@panoptic-it-solutions/autotask-client";

// Assume 'client' is an instantiated AutotaskClient

async function getFirstPageOfActiveCompanies() {
  try {
    const entityName: AllAutotaskEntityNames = "Companies";
    const options: AutotaskQueryOptions = {
      fields: ["id", "companyName", "companyType", "isActive"],
      filter: [
        // Refer to Autotask API docs for correct field name (e.g., 'status' or 'companyStatus') and value
        { field: "isActive", op: "eq", value: true },
      ],
      sdkPageSize: 10,
      includeTotalCount: true, // Get total count for pagination info on the first request
    };

    const response: SdkClientPagedResponse<AutotaskEntityTypeMap["Companies"]> =
      await client.queryEntities(entityName, options);

    console.log(
      `Fetched page ${response.currentPage} of ${
        response.totalPages ?? "unknown"
      }, containing ${response.items.length} companies.`
    );
    if (response.totalItemsAvailable !== undefined) {
      console.log(
        `Total active companies matching filter: ${response.totalItemsAvailable}`
      );
    }

    response.items.forEach((company) => {
      console.log(
        `- ID: ${company.id}, Name: ${company.companyName}, Type: ${company.companyType}, Active: ${company.isActive}`
      );
    });

    if (response.hasNextPage && response.continuationToken) {
      console.log(
        "\nTo fetch the next page, use this continuationToken:",
        response.continuationToken
      );
      // Example: Fetching next page
      // const nextPageOptions: AutotaskQueryOptions = {
      //   continuationToken: response.continuationToken,
      //   sdkPageSize: 10 // sdkPageSize can be repeated or omitted (uses original if omitted)
      // };
      // const nextPageResponse = await client.queryEntities(entityName, nextPageOptions);
    }
  } catch (error) {
    console.error("Error fetching companies:", error);
    // Handle specific errors, e.g., AutotaskApiError, RateLimitError
  }
}

getFirstPageOfActiveCompanies();

Fetching All Pages Automatically (queryAllEntities)

Use queryAllEntities to retrieve all items matching a query, with the SDK handling pagination transparently.

import {
  AutotaskQueryOptions,
  AllAutotaskEntityNames,
} from "@panoptic-it-solutions/autotask-client";
// Assume 'client' is an instantiated AutotaskClient

async function fetchAllActiveContacts() {
  try {
    const entityName: AllAutotaskEntityNames = "Contacts";
    const options: AutotaskQueryOptions = {
      filter: [{ field: "isActive", op: "eq", value: true }],
      fields: ["id", "firstName", "lastName", "emailAddress"],
      sdkPageSize: 50, // SDK fetches in chunks of 50 from the API
    };

    console.log("Fetching all active contacts...");
    const allContacts = await client.queryAllEntities(
      entityName,
      options,
      // Optional progress callback
      (progress) => {
        const total =
          progress.totalKnownItems !== undefined
            ? ` of ${progress.totalKnownItems}`
            : "";
        console.log(
          `Progress: Page ${progress.currentPageNumber} fetched. ` +
            `${progress.totalItemsFetchedSoFar}${total} contacts retrieved.`
        );
      },
      // Optional: fetch total count upfront for more accurate progress reporting
      { fetchTotalCountForProgress: true }
    );

    console.log(`Successfully fetched ${allContacts.length} active contacts.`);
    // Process allContacts array
  } catch (error) {
    console.error("Failed to fetch all contacts:", error);
  }
}

fetchAllActiveContacts();

Filtering

The SDK supports powerful filtering capabilities when querying entities.

Filter Structure (AutotaskApiFilter)

Filters are provided as an array of AutotaskApiFilter objects to the filter property in AutotaskQueryOptions. Each filter object has the following structure:

interface AutotaskApiFilter {
  field: string;
  op: AutotaskApiFilterOp; // Union of supported operator strings
  value: string | number | boolean | (string | number)[]; // Value depends on 'op'
  udf?: boolean; // Set to true if 'field' is a User-Defined Field
}
  • field: The camelCase name of the field to filter on (e.g., companyName, status, lastActivityDate). Always consult the official Autotask REST API documentation for the correct filterable field names for each entity.
  • op: The filter operator. See Available Filter Operators.
  • value: The value to compare against. For the 'in' or 'notin' operators, this should be an array (e.g., [1, 2, 3] or ['Val1', 'Val2']).
  • udf: (Optional) Boolean, set to true if the field is a User-Defined Field (UDF).

Multiple filters in the array are combined with AND logic.

Available Filter Operators (AutotaskApiFilterOp)

The AutotaskApiFilterOp type includes the following string literals. Refer to the Autotask REST API documentation for the definitive list of supported operators for specific fields and entities, as not all operators apply to all fields.

| Operator | Description | Example Value(s) | Notes | | ------------ | -------------------------- | ------------------------------- | ---------------------------- | | eq | Equals | 1, "Active", true | | | noteq | Not equals | 1, "Active", true | Matches noteq in type def | | gt | Greater than | 100, "2023-01-01T00:00:00Z" | For numbers, dates | | gte | Greater than or equal to | 100, "2023-01-01" | For numbers, dates | | lt | Less than | 100, "2023-12-31" | For numbers, dates | | lte | Less than or equal to | 100, "2023-12-31" | For numbers, dates | | beginsWith | Starts with | "Tech" | Case-insensitive for strings | | endsWith | Ends with | "Solutions" | Case-insensitive for strings | | contains | Contains (substring match) | "Service" | Case-insensitive for strings | | exist | Field is not null | (value not applicable) | | | notExist | Field is null | (value not applicable) | | | in | In a list of values | [1, 2, 3], ["Val1", "Val2"] | value is an array | | notIn | Not in a list of values | [4, 5] | value is an array |

Important: Always cross-reference with the Autotask API documentation for the most accurate and up-to-date list of supported operators and their behavior for specific entity fields. The SDK's AutotaskQueryOperator type provides type safety for the operators listed above; the API may support additional query capabilities.

Filter Examples

// Active companies created after a specific date
const recentActiveCompanies = await client.queryEntities("Companies", {
  filter: [
    { field: "isActive", op: "eq", value: true },
    { field: "createDate", op: "gte", value: "2024-01-01" },
  ],
  fields: ["id", "companyName", "createDate"],
});

// Tickets with high priority, not yet completed
const highPriorityOpenTickets = await client.queryEntities("Tickets", {
  filter: [
    { field: "priority", op: "eq", value: 1 }, // Assuming 1 is High Priority
    { field: "status", op: "notIn", value: [5, 6] }, // Assuming 5=Complete, 6=Cancelled
    { field: "assignedResourceID", op: "exist" }, // Must be assigned
  ],
  fields: ["id", "ticketNumber", "title", "priority", "status"],
});

Pagination

The queryEntities method uses a continuation token-based system.

Pagination Options

In AutotaskQueryOptions:

  • sdkPageSize?: number: Desired items per SDK page (default: 25). The SDK may make multiple API calls to fulfill this.
  • continuationToken?: string: Token from a previous response to fetch the next/previous page.
  • pageRequest?: 'next' | 'prev': Direction when using continuationToken. Default is 'next'.
  • includeTotalCount?: boolean: If true (and no continuationToken), makes an extra call for total count. Default: false.

When using continuationToken, the filter and fields from the original query are implicitly used. Re-specifying them might lead to unpredictable behavior.

Response Properties

SdkClientPagedResponse<T> contains:

  • items: T[]: Entities for the current page.
  • pageSize: number: sdkPageSize used.
  • currentPage: number: 1-indexed SDK page number.
  • continuationToken: string | null: Token for navigation. Null if no more pages in that direction.
  • hasNextPage: boolean: If a next page is available.
  • hasPreviousPage: boolean: If a previous page is available.
  • totalItemsAvailable?: number: Total items matching (if includeTotalCount was true on initial query).
  • totalPages?: number: Total SDK pages (if totalItemsAvailable known).
  • errors?: AutotaskErrorItem[]: API errors from the response body.

Example: Manual Pagination Loop

// Assume 'client' is an instantiated AutotaskClient
async function getAllActiveCompaniesManually() {
  let allCompanies: AutotaskEntityTypeMap["Companies"][] = [];
  let currentContinuationToken: string | null = null;
  const sdkPageSize = 10;
  let currentPageNumber = 0;
  let totalItems: number | undefined;
  let totalSdkPages: number | undefined;
  const entityName: AllAutotaskEntityNames = "Companies";

  console.log(`Fetching active companies, ${sdkPageSize} per SDK page...`);

  try {
    do {
      currentPageNumber++;
      const queryOptions: AutotaskQueryOptions = {
        ...(currentContinuationToken
          ? { continuationToken: currentContinuationToken } // Subsequent requests only need token
          : {
              // First request
              fields: ["id", "companyName", "isActive"],
              filter: [{ field: "isActive", op: "eq", value: true }],
              includeTotalCount: true,
            }),
        sdkPageSize: sdkPageSize,
      };

      console.log(
        `Requesting SDK page ${currentPageNumber}${
          totalSdkPages ? ` of ${totalSdkPages}` : ""
        }...`
      );
      const response = await client.queryEntities(entityName, queryOptions);

      if (response.errors && response.errors.length > 0) {
        console.error("Errors received:", response.errors);
        break;
      }

      if (currentPageNumber === 1) {
        // Capture totals from the first response
        totalItems = response.totalItemsAvailable;
        totalSdkPages = response.totalPages;
      }

      const pageInfo = totalSdkPages
        ? `SDK Page ${response.currentPage}/${totalSdkPages}`
        : `SDK Page ${response.currentPage}`;
      console.log(`  Received ${pageInfo}: ${response.items.length} items.`);

      response.items.forEach((company) => allCompanies.push(company));
      currentContinuationToken = response.continuationToken;
    } while (currentContinuationToken); // Continues as long as there's a token for the next page

    console.log(
      `\nSuccessfully fetched ${allCompanies.length} active companies.`
    );
  } catch (error) {
    console.error("Error fetching all active companies:", error);
  }
}

getAllActiveCompaniesManually();

Fetching Related Entities (include Clause)

Fetch primary entities and their related entities using the include option in AutotaskQueryOptions.

Structure of IncludeClause

interface IncludeClause {
  path: string; // Name of the relationship path (e.g., "Account" for a Ticket).
  // MUST correspond to a defined relationship for the primary entity.
  select?: string[]; // Optional: Fields to retrieve for the related entity.
}

Relationship definitions are internally managed by the SDK (see relationships/index.ts).

How it Works

  1. Client fetches primary entities.
  2. For each IncludeClause, it looks up the relationship configuration to find foreignEntity, localKey (on primary), and foreignKey (on related).
  3. Collects localKey values from primary entities.
  4. Makes additional API calls (using queryAllEntities internally) to fetch related entities, filtered by collected foreign key values.
  5. Embeds related entities into primary entity objects under a property matching path.
  6. If select is used, only specified fields are on the embedded entity.

Current Limitation: Supports one level of relationship.

Example: Tickets with Account and Contact

// Assume 'client' is an instantiated AutotaskClient
// Assume 'Tickets' entity has relationships 'Account' (to 'Companies') and 'Contact' (to 'Contacts')

async function getTicketsWithDetails() {
  try {
    const options: AutotaskQueryOptions = {
      filter: [{ field: "status", op: "eq", value: 1 }], // Example: Open tickets
      fields: ["id", "title", "companyID", "contactID"],
      include: [
        { path: "Account", select: ["id", "companyName"] },
        { path: "Contact", select: ["id", "firstName", "lastName"] },
      ],
      sdkPageSize: 5,
    };

    const response = await client.queryEntities("Tickets", options);

    response.items.forEach((ticket) => {
      // Type assertion for included properties
      const typedTicket = ticket as AutotaskEntityTypeMap["Tickets"] & {
        Account?: Partial<AutotaskEntityTypeMap["Companies"]>;
        Contact?: Partial<AutotaskEntityTypeMap["Contacts"]>;
      };
      console.log(`Ticket: ${typedTicket.title}`);
      if (typedTicket.Account)
        console.log(`  Account: ${typedTicket.Account.companyName}`);
      if (typedTicket.Contact)
        console.log(
          `  Contact: ${typedTicket.Contact.firstName} ${typedTicket.Contact.lastName}`
        );
    });

    if (response.errors)
      console.warn("\nErrors during fetch/embedding:", response.errors);
  } catch (error) {
    console.error("Error fetching tickets with details:", error);
  }
}
getTicketsWithDetails();

Fetching Entity Field Definitions and Picklists

The SDK provides a method to retrieve detailed information about the fields of any Autotask entity, including their data types and picklist options. This is particularly useful for dynamically building UIs, validating data, or understanding entity structures programmatically.

The getEntityFieldsDetailed method on the AutotaskClient instance allows you to fetch this information.

import {
  AutotaskClient,
  ProcessedFieldDetailedInfo, // Make sure this type is imported
  // ... other necessary imports
} from "@panoptic-it-solutions/autotask-client";

// Assume 'client' is an instantiated AutotaskClient

async function logTicketFieldDetails() {
  try {
    const entityName = "Tickets"; // Example: Get fields for the "Tickets" entity
    
    // Fetch all fields for the "Tickets" entity
    const allTicketFields: ProcessedFieldDetailedInfo[] = 
      await client.getEntityFieldsDetailed(entityName);

    console.log(`Field details for ${entityName}:`);
    allTicketFields.forEach(field => {
      console.log(`- Field Name: ${field.name}`);
      console.log(`  Data Type: ${field.dataType}`);
      console.log(`  Is Picklist: ${field.isPickList}`);
      if (field.isPickList && field.picklistMap) {
        console.log(`  Picklist Options (Value: Label):`);
        for (const [value, label] of Object.entries(field.picklistMap)) {
          console.log(`    '${value}': '${label}'`);
        }
      }
      // Example of accessing raw picklistValues if needed
      // if (field.isPickList && Array.isArray(field.picklistValues)) {
      //   console.log(`  Raw Picklist Values (first 2):`, field.picklistValues.slice(0, 2));
      // }
    });

    // Example: Fetch specific fields for the "Companies" entity
    const specificCompanyFields: ProcessedFieldDetailedInfo[] = 
      await client.getEntityFieldsDetailed("Companies", ["companyName", "companyType", "marketSegmentID"]);
    
    console.log(`\nSpecific field details for Companies:`);
    specificCompanyFields.forEach(field => {
      console.log(`- Field: ${field.name}, Type: ${field.dataType}${field.isPickList ? ', Picklist' : ''}`);
      if (field.name === "marketSegmentID" && field.picklistMap) {
        console.log(`  Market Segment Picklist:`, field.picklistMap);
      }
    });

  } catch (error) {
    console.error(`Error fetching field details:`, error);
  }
}

logTicketFieldDetails();

Understanding ProcessedFieldDetailedInfo

The getEntityFieldsDetailed method returns an array of ProcessedFieldDetailedInfo objects. Each object contains:

  • All properties from AutotaskRawField, which represents the raw data returned by the Autotask API for a field (e.g., name, dataType, isPickList, isReadOnly, isRequired, picklistValues array).
  • An optional picklistMap: If isPickList is true and picklistValues are available, this property provides a convenient key-value map where keys are the picklist item values (as strings) and values are their corresponding labels. This simplifies looking up display labels for picklist values.

For example, if a "Status" field (isPickList: true) has picklist values like { value: 1, label: "New" }, { value: 2, label: "In Progress" }, the picklistMap would be { "1": "New", "2": "In Progress" }.

Key Types and Interfaces

  • AllAutotaskEntityNames: Union type of all valid Autotask entity names (e.g., "Companies").
  • AutotaskEntityTypeMap: Maps entity names to their TypeScript interfaces (e.g., AutotaskEntityTypeMap["Tickets"] is TicketEntityType).
  • AutotaskQueryOptions: Options for queryEntities and queryAllEntities.
  • AutotaskApiFilter, AutotaskApiFilterOp: For filter conditions.
  • SdkClientPagedResponse<T>: Response from queryEntities.
  • SdkProgressData<T>: Progress object for queryAllEntities callback.
  • IncludeClause: For defining related entities to embed.
  • AutotaskClientConfig: Configuration for AutotaskClient.
  • AutotaskRawField: Interface representing the raw field definition as returned by Autotask's /entityInformation/fields endpoint.
  • ProcessedFieldDetailedInfo: Extends AutotaskRawField and includes an optional picklistMap for easier consumption of picklist options. This is the return type for client.getEntityFieldsDetailed().
  • AutotaskErrorItem: Structure of errors in API responses.
  • Custom Error Classes (AutotaskApiError, RateLimitError, NetworkError, TimeoutError, AutotaskClientError): Provide detailed error context.

These types are generally exported from @panoptic-it-solutions/autotask-client or accessible via its main exports. Refer to the SDK's types, autotask-types, and fields directories for detailed definitions.

Error Handling

  • Wrap SDK calls in try...catch blocks.
  • Check response.errors in SdkClientPagedResponse for non-fatal API errors.
  • Handle specific custom error types for better diagnostics.
try {
  const response = await client.queryEntities("Companies", {
    /* ... */
  });
  if (response.errors && response.errors.length > 0) {
    console.warn("API reported errors:", response.errors);
  }
  // Process response.items
} catch (error) {
  if (error instanceof AutotaskApiError) {
    console.error("Autotask API Error:", error.statusCode, error.errorsFromApi);
  } else if (error instanceof RateLimitError) {
    console.error("Rate limit hit (retries exhausted):", error.message);
  } else {
    console.error("SDK or Network Error:", error);
  }
}

Rate Limiting Details

  • Uses Upstash Redis (if configured) for distributed rate limiting. Essential for serverless/scaled environments.
  • Defaults (e.g., 5 req/sec, 5000 req/hr) are configurable in AutotaskClientConfig.upstash.
  • Automatic retries with backoff for RateLimitError or API 429s.
  • Without Upstash, falls back to reactive retries on 429s (less robust).

Source Code Structure Overview

  • src/client.ts: Main AutotaskClient class.
  • src/types/: Core type definitions (config, API responses, errors).
  • src/autotask-types/: Generated TypeScript interfaces for all Autotask entities.
  • src/relationships/: Defines entity relationships for the include feature.
  • src/client-internals/: Lower-level SDK mechanics (pagination, retry, request execution).
  • src/rate-limiting/: Rate limiting implementation.

Platform-Specific Configuration Examples

How you configure and use the AutotaskClient can vary. Using Upstash Redis for rate limiting is strongly recommended for all production and serverless/scaled environments.

1. Standard Node.js (e.g., Express.js Server)

Instantiate AutotaskClient once (singleton pattern) and reuse.

// src/services/autotaskService.ts
import {
  AutotaskClient,
  AutotaskClientConfig,
  LogLevel,
} from "@panoptic-it-solutions/autotask-client";
import * as dotenv from "dotenv";

dotenv.config();

const clientConfig: AutotaskClientConfig = {
  userName: process.env.AUTOTASK_USERNAME!,
  secret: process.env.AUTOTASK_SECRET!,
  integrationCode: process.env.AUTOTASK_API_INTEGRATION_CODE!,
  baseUrl: process.env.AUTOTASK_API_URL!,
  upstash: {
    upstashRedisUrl: process.env.UPSTASH_REDIS_REST_URL,
    upstashRedisToken: process.env.UPSTASH_REDIS_REST_TOKEN,
  },
  logLevel: LogLevel.INFO,
};

const autotaskClient = new AutotaskClient(clientConfig);
export default autotaskClient;

// Usage in an Express route
// import autotaskClient from '../services/autotaskService';
// router.get('/companies', async (req, res) => { /* ... use autotaskClient ... */ });

2. Next.js / Vercel (Serverless Functions)

Instantiate AutotaskClient inside your API route handler. Upstash Redis is critical here.

// pages/api/autotask/contacts.ts
import type { NextApiRequest, NextApiResponse } from "next";
import {
  AutotaskClient,
  AutotaskClientConfig /* other types */,
} from "@panoptic-it-solutions/autotask-client";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === "GET") {
    try {
      const clientConfig: AutotaskClientConfig = {
        userName: process.env.AUTOTASK_USERNAME!,
        secret: process.env.AUTOTASK_SECRET!,
        integrationCode: process.env.AUTOTASK_API_INTEGRATION_CODE!,
        baseUrl: process.env.AUTOTASK_API_URL!,
        upstash: {
          // Essential for serverless
          upstashRedisUrl: process.env.UPSTASH_REDIS_REST_URL,
          upstashRedisToken: process.env.UPSTASH_REDIS_REST_TOKEN,
        },
      };
      const client = new AutotaskClient(clientConfig);
      // ... use client to fetch data ...
      // res.status(200).json(contactData);
    } catch (error: any) {
      console.error("Autotask API Error:", error);
      res.status(500).json({ error: error.message || "Failed to fetch" });
    }
  } // ...
}

3. Other Serverless Environments (e.g., AWS Lambda, Google Cloud Functions)

Principles are similar: instantiate client in handler, use distributed rate limiting (Upstash).

// Example for AWS Lambda
import {
  AutotaskClient,
  AutotaskClientConfig /* other types */,
} from "@panoptic-it-solutions/autotask-client";

export const handler = async (event: any) => {
  try {
    const clientConfig: AutotaskClientConfig = {
      userName: process.env.AUTOTASK_USERNAME!,
      // ... other config from environment variables ...
      upstash: {
        upstashRedisUrl: process.env.UPSTASH_REDIS_REST_URL,
        upstashRedisToken: process.env.UPSTASH_REDIS_REST_TOKEN,
      },
    };
    const client = new AutotaskClient(clientConfig);
    // ... use client ...
    // return { statusCode: 200, body: JSON.stringify(data) };
  } catch (error: any) {
    // ... error handling ...
  }
};

AI Assistant Guidelines

Comprehensive, organized rules for AI coding assistants (Claude Code, Cursor, Cline, GitHub Copilot) are available in the .ai-rules/ directory.

📁 AI Rules Directory

.ai-rules/
├── README.md          # Index and quick reference
├── cursor.md          # Cursor IDE specific rules
├── claude.md          # Claude Code specific rules (also in .claude/rules.md)
├── cline.md           # Cline specific rules
├── copilot.md         # GitHub Copilot specific rules
└── shared/            # Shared rules for all AI assistants
    ├── core-patterns.md
    ├── crud-operations.md
    ├── queries.md
    ├── udfs.md
    ├── error-handling.md
    ├── security.md
    ├── performance.md
    └── testing.md

🤖 AI-Specific Rules

  • Claude Code - Optimized for multi-step planning, file operations, comprehensive code generation
  • Cursor IDE - Optimized for inline completion, quick fixes, type inference
  • Cline - Optimized for autonomous execution, CLI workflows
  • GitHub Copilot - Optimized for code completion, comment-driven development

📚 Shared Rules

All AI assistants should reference the shared rules:

For the complete rules index, see .ai-rules/README.md.

Quick Reference for AI

Key Principles:

  1. Always use TypeScript types - The SDK provides strong typing for all entities and operations
  2. Upstash Redis is critical for production - Especially in serverless/distributed environments
  3. Include parent IDs in child entity updates - Avoids extra API calls (TicketChecklistItems, TicketNotes, Tasks)
  4. Set udf: true for User-Defined Fields - Required for filtering on UDFs
  5. Validate filter values - Don't pass unvalidated user input to filters
  6. Use queryAllEntities for multi-page results - Let the SDK handle pagination
  7. Check response.errors - Even successful responses may contain warnings
  8. Specify only needed fields - Don't fetch all fields if you only need a few

Common Patterns:

// ✅ Good: Comprehensive setup
const client = new AutotaskClient({
  userName: process.env.AUTOTASK_USERNAME!,
  secret: process.env.AUTOTASK_SECRET!,
  integrationCode: process.env.AUTOTASK_API_INTEGRATION_CODE!,
  baseUrl: process.env.AUTOTASK_API_URL,
  upstash: {
    upstashRedisUrl: process.env.UPSTASH_REDIS_REST_URL,
    upstashRedisToken: process.env.UPSTASH_REDIS_REST_TOKEN,
  },
  logLevel: LogLevel.INFO,
});

// ✅ Good: Efficient query with specific fields
const companies = await client.queryAllEntities('Companies', {
  filter: [{ field: 'isActive', op: 'eq', value: true }],
  fields: ['id', 'companyName', 'phone'],
  sdkPageSize: 100,
});

// ✅ Good: Child entity update with parent ID
await client.updateEntity('TicketChecklistItems', itemId, {
  ticketID: ticketId, // Avoids extra lookup
  isCompleted: true,
});

// ✅ Good: UDF filtering
await client.queryEntities('Companies', {
  filter: [{
    field: 'customTier',
    op: 'eq',
    value: 'Enterprise',
    udf: true  // Required!
  }],
  fields: ['id', 'companyName', 'customTier']
});

Common Mistakes to Avoid:

// ❌ Bad: No Upstash in serverless
const client = new AutotaskClient({
  userName: process.env.AUTOTASK_USERNAME!,
  secret: process.env.AUTOTASK_SECRET!,
  integrationCode: process.env.AUTOTASK_API_INTEGRATION_CODE!,
  // Missing upstash config in serverless environment!
});

// ❌ Bad: Fetching all fields
await client.queryEntities('Companies', {
  // No fields specified - fetches everything
});

// ❌ Bad: Missing udf flag
await client.queryEntities('Companies', {
  filter: [{ field: 'customTier', op: 'eq', value: 'Enterprise' }]
  // Missing udf: true
});

// ❌ Bad: Re-specifying filter with continuation token
await client.queryEntities('Companies', {
  filter: [...], // Don't re-specify!
  continuationToken: token,
});

Platform-Specific Notes:

  • Serverless (Lambda/Vercel): Always instantiate client inside handler, always use Upstash
  • Traditional Node.js: Use singleton pattern, Upstash recommended for multiple instances
  • Next.js API Routes: Instantiate per request, Upstash required

For complete documentation including CRUD operations, filtering patterns, pagination, UDFs, error handling, and platform-specific examples, see docs/rules/autotask-client.mdc

Contributing

Contributions are welcome! Please open an issue or submit a pull request.

License

This project is licensed under the AGPLv3 License. See the LICENSE file for details.