npm package: @masonlandcattle/servicetitan-sdk
Description: ServiceTitan API SDK for Node.js and TypeScript with retries, rate limiting, pagination, and broad tenant API coverage.
Author: Mason Land & Cattle
Latest version: 0.9.2

## ServiceTitan SDK (Node.js / TypeScript)

A pragmatic ServiceTitan API client with:

- Robust auth (Client Credentials)
- **Retries** with exponential backoff + jitter
- Honors **Retry-After**, handles 429/5xx
- **Built-in rate limiting** with queued waiting in the client transport
- URL builder and **pagination** helpers (`getAll`)
- Broad tenant API namespace coverage across the current ServiceTitan developer portal
- Exported TypeScript helper types for better IntelliSense when using the SDK

The package supports both:

- a namespaced client via `createClient(...)`
- direct functional imports per namespace when preferred

### Release status

Current release focus:

- Synced the SDK to the current ServiceTitan tenant API namespace set
- Corrected stale routes, verbs, and path segments across multiple resources
- Added missing documented resource modules and helpers
- Expanded exported TypeScript helper types to improve IntelliSense

Recommended release posture for the current line:

- `0.9.x` is the stabilization phase after the broad OpenAPI alignment pass
- the current public surface is intended to be much closer to the local OpenAPI specs than earlier releases
- future breaking changes should mostly be reserved for upstream ServiceTitan API changes or clear correctness fixes

### Repo notes

If you are editing this SDK with Codex or another coding agent, use [`AGENTS.md`](./AGENTS.md) as the repo-specific guide.

The short version:

- local OpenAPI JSON files are the source of truth
- breaking changes are acceptable when the schema and SDK disagree
- prefer namespace-specific types over generic placeholders
- keep [`src/types/resources.ts`](./src/types/resources.ts) for shared base helpers only
- run `npm run build` after changes

### Install

```bash
npm install @masonlandcattle/servicetitan-sdk
```

### Configure

Set env vars or pass options:

- `TENANT_ID`
- `APP_KEY`
- `CLIENT_ID`
- `SECRET_KEY`
- Optional: `ENVIRONMENT` = `production` | `development`

### Usage (namespaced client)

```ts
import { createClient } from "@masonlandcattle/servicetitan-sdk";

const st = createClient({
  tenantId: process.env.TENANT_ID!,
  appKey: process.env.APP_KEY!,
  clientId: process.env.CLIENT_ID!,
  clientSecret: process.env.SECRET_KEY!,
  environment: (process.env.ENVIRONMENT as any) || "production",
  retries: 3,
  apiRateLimitPerSecond: 45,
  maxConcurrent: 20,
});

// List jobs (single page)
const page = await st.jpm.listJobs({ page: 1, pageSize: 100 });

// List all jobs server-side (aggregates pages)
const allJobs = await st.jpm.listJobs({ status: "Open" }, { all: true, pageSize: 500 });

// Create a job note
await st.jpm.createJobNote(123456, { text: "Hello from SDK", pinToTop: true });

// Materials (with get-all)
const materials = await st.pricebook.listMaterials({}, { all: true, pageSize: 500 });
```

### Important behaviors

- Local OpenAPI JSON files in [`ServiceTitanOpenAPIJson`](./ServiceTitanOpenAPIJson) are the source of truth for this SDK.
- Breaking changes are allowed when the documented ServiceTitan contract and the previous SDK surface disagree.
- Requests are throttled inside [`src/client.ts`](./src/client.ts), so callers do not need to add their own queueing to use helpers like `client.request(...)` or `client.getAll(...)`.
- Normal tenant API traffic is queued at `45` requests per second per tenant by default. You can lower it or raise it during client creation, but the SDK clamps `apiRateLimitPerSecond` to `55` max.
- Reporting runs made through `st.reporting.getReportData(...)` are additionally queued at `5` runs per minute for the same `tenant + report category + report id`.
- When a limit is exceeded, the SDK waits in an in-memory queue instead of failing immediately. This means large `getAll()` calls or bursts of individual requests may take longer to complete under load.
- If ServiceTitan still responds with `429`, the client honors `Retry-After` when present and retries with exponential backoff as a fallback.
- Export feeds generally use `{ from, includeRecentChanges }` continuation params and return `{ data, hasMore, continueFrom }`.
- Binary telecom media endpoints return `ArrayBuffer`.
- `options.all` is only used on endpoints with safe paginated `data` plus `hasMore` behavior.

### Known limits

- Webhook verification helpers are not included yet. The public docs available in this repo workflow are not specific enough to safely implement the exact signature contract.
- Some nested model fields are still intentionally broad where the upstream schema is weak or highly open-ended.
- This package is an API SDK, not an application framework. Queueing, persistence, workflow orchestration, and webhook hosting should stay in the consuming app.
- ServiceTitan may change upstream schemas over time. When that happens, the local OpenAPI JSON files should be updated first, then the SDK should be realigned to match.

### Docs map

- [`README.md`](./README.md): install, usage, examples, and package behavior
- [`AGENTS.md`](./AGENTS.md): repo-specific engineering rules for Codex and contributors
- [`docs/architecture.md`](./docs/architecture.md): structure, boundaries, and extension patterns

### Resources and quick examples

Below is a brief tour of the main resource namespaces. Each list function supports `{ all?: boolean; pageSize?: number }` to fetch all pages server‑side.

Initialize once and reuse:
```ts
import { createClient } from "@masonlandcattle/servicetitan-sdk";
const st = createClient({
  tenantId: process.env.TENANT_ID!,
  appKey: process.env.APP_KEY!,
  clientId: process.env.CLIENT_ID!,
  clientSecret: process.env.SECRET_KEY!,
});
```

#### Accounting (`st.accounting`)
- Invoices, Payments, GL Accounts, Inventory Bills, AP Credits/Payments, Tax Zones, etc.
```ts
const invoices = await st.accounting.listInvoices({ customerId: 1234 }, { all: true });
const invoicesById = await st.accounting.listInvoicesByIds([111, 222, 333]);
await st.accounting.markInvoicesAsExported([111, 222]);
const gl = await st.accounting.listGlAccounts({ type: "Asset" }, { all: true });
```

#### CRM (`st.crm`)
- Customers, Contacts, Locations, Leads, Bookings.
```ts
const customers = await st.crm.listCustomers({ search: "Acme" }, { all: true });
const contacts = await st.crm.listContacts({ updatedAfter: "2024-01-01" }, { all: true });
const locations = await st.crm.listLocations({ customerId: 1234 }, { all: true });
```

#### Dispatch (`st.dispatch`)
- Teams, Zones, Arrival Windows, Technician Shifts, Appointment Assignments.
```ts
const teams = await st.dispatch.listTeams({}, { all: true });
const windows = await st.dispatch.listArrivalWindows({ date: "2025-01-01" }, { all: true });
```

#### Equipment Systems (`st.equipmentSystems`)
- Installed Equipment and metadata.
```ts
const eq = await st.equipmentSystems.listInstalledEquipment({ search: "Carrier" }, { all: true });
```

#### Findings (`st.findings`)
- Findings, finding assets, and finding attachments.
```ts
import type {
  FindingCreatePayload,
  FindingListResponse,
} from "@masonlandcattle/servicetitan-sdk";

const findings = await st.findings.listFindings({ page: 1, pageSize: 50 });
const assets = await st.findings.listFindingAssets({ page: 1, pageSize: 50 });

const findingPayload: FindingCreatePayload = {
  name: "Loose wire",
  summary: "Panel inspection issue",
  locationId: 1234,
};

const created = await st.findings.createFinding(findingPayload);
const attachment = await st.findings.createFindingAttachment(created.id!, {
  fileName: "inspection-photo.jpg",
  contentType: "image/jpeg",
  url: "https://example.com/inspection-photo.jpg",
});
```

Useful Findings exports for IntelliSense:
- `FindingCreatePayload`
- `FindingUpdatePayload`
- `FindingListParams`
- `FindingListResponse`
- `FindingAssetListParams`
- `FindingAssetListResponse`
- `FindingSummary`
- `FindingAsset`
- `FindingAttachment`

#### Forms (`st.forms`)
- Forms, Jobs + Forms, Submissions.
```ts
const forms = await st.forms.listForms({}, { all: true });
```

#### Inventory (`st.inventory`)
- Purchase Orders (+ types/markups/requests), Receipts, Transfers, Returns, Trucks, Vendors, Warehouses.
```ts
const pos = await st.inventory.listPurchaseOrders({ status: "Open" }, { all: true });
const receipts = await st.inventory.listReceipts({ dateFrom: "2025-01-01" }, { all: true });
```

#### JPM (Jobs/Projects) (`st.jpm`)
- Jobs, Appointments, Projects, WBS, types/statuses.
```ts
const jobs = await st.jpm.listJobs({ status: "Open" }, { all: true });
const appts = await st.jpm.listAppointments({ scheduledOnOrAfter: "2025-01-01" }, { all: true });
```

#### Marketing (`st.marketing`) and Marketing Ads (`st.marketingAds`)
- Campaigns/Categories/Costs; Ads Attributions, Performance, and Capacity Warnings.
```ts
const campaigns = await st.marketing.listCampaigns({}, { all: true });
await st.marketingAds.createWebLeadFormAttribution({
  leadId: 42,
  webSessionData: {
    landingPageUrl: "https://example.com/landing",
    referrerUrl: "https://google.com",
    utmSource: "google",
    utmMedium: "cpc",
    utmCampaign: "spring-promo",
  },
});
```

#### Marketing Reputation (`st.marketingReputation`)
- Reviews.
```ts
const reviews = await st.marketingReputation.listReviews({ createdOnOrAfter: "2025-01-01" }, { all: true });
```

#### Memberships (`st.memberships`)
- Memberships, Types, Recurring Service Types/Events/Services, Invoice Templates.
```ts
const memberships = await st.memberships.listMemberships({ customerId: 1234 }, { all: true });
```

#### Payroll (`st.payroll`)
- Timesheets (+ non-job, per-job), Timesheet Codes, Payrolls, Adjustments, Job Splits, Location Labor Rates.
```ts
const codes = await st.payroll.listTimesheetCodes({}, { all: true });
const payrolls = await st.payroll.listPayrolls({ createdOnOrAfter: "2025-01-01" }, { all: true });
```

#### Pricebook (`st.pricebook`)
- Materials, Services, Equipment, Images, Discounts & Fees, Categories, Client-Specific Pricing.
```ts
const materials = await st.pricebook.listMaterials({ updatedAfter: "2025-01-01" }, { all: true });
```

#### Reporting (`st.reporting`)
- Report Categories and mappings, Dynamic Value Sets.
```ts
const cats = await st.reporting.listReportCategories({}, { all: true });
```

#### Sales Estimates (`st.salesEstimates`)
- Estimates and Estimate Items.
```ts
const ests = await st.salesEstimates.listEstimates({ jobNumber: "131179" }, { all: true });
```

#### Scheduling Pro (`st.schedulingPro`)
- Routers, Schedulers.
```ts
const schedulers = await st.schedulingPro.listSchedulers({}, { all: true });
```

#### Service Agreements (`st.serviceAgreements`)
- Agreements and export endpoints.
```ts
const agreements = await st.serviceAgreements.listServiceAgreements({}, { all: true });
```

#### Settings (`st.settings`)
- Employees, Technicians, Business Units, Tag Types, User Roles.
```ts
const techs = await st.settings.listTechnicians({}, { all: true });
```

#### Task Management (`st.taskManagement`)
- Tasks and Client-Side Data.
```ts
const tasks = await st.taskManagement.listTasks({ status: "Open" }, { all: true });
```

#### Telecom (`st.telecom`)
- Calls + media.
```ts
const calls = await st.telecom.listCalls({ createdOnOrAfter: "2025-01-01" }, { all: true });
```

#### Timesheets V2 (`st.timesheetsV2`)
- Activities, Types, Categories.
```ts
const activities = await st.timesheetsV2.listActivities({}, { all: true });
```

#### Job Bookings (`st.jbce`)
- Call Reasons.
```ts
const reasons = await st.jbce.listCallReasons({}, { all: true });
```

#### Customer Interactions (`st.customerInteractions`)
- Technician Ratings.
```ts
const ratings = await st.customerInteractions.listTechnicianRatings({ createdOnOrAfter: "2025-01-01" }, { all: true });
```

### Type helpers

The package exports helper types from the root for stronger autocomplete and payload guidance. Examples:

```ts
import type {
  CrmExportCustomer,
  CreateEstimateRequest,
  CreateWebLeadFormAttributionRequest,
  Contact,
  ContactMethodCreatePayload,
  CustomerInteractionsExportResponse,
  FindingCreatePayload,
  Invoice,
  JpmExportJob,
  MarketingAdsPerformanceRecord,
  SalesEstimatesExportResponse,
  TelecomExportCall,
} from "@masonlandcattle/servicetitan-sdk";
```

Many resource methods now use typed params and payload helpers directly, so editor IntelliSense is much better than a generic `Record<string, unknown>` workflow.

### Alternate import style (functional)

```ts
import { ServiceTitanClient, CRM } from "@masonlandcattle/servicetitan-sdk";
const st = new ServiceTitanClient({ /* creds */ });
const customers = await CRM.listCustomers(st, { createdOnOrAfter: "2024-01-01" }, { all: true });
```

### Local development / testing without publishing

Run local example scripts directly against the source using `tsx`.

1. Set environment variables:
   - `TENANT_ID`
   - `APP_KEY`
   - `CLIENT_ID`
   - `SECRET_KEY`

2. Execute examples:

```bash
npm run ex:crm
npm run ex:dispatch
npm run ex:equip
npm run ex:namespaced
npm run ex:crm-export
npm run ex:telecom-media
```

These scripts import from `../src`, so there's no need to publish/install.

### API Shape

`/{category}/v2/tenant/{TENANT_ID}/{subject}`

Use `client.buildPath({ category, subject, idOrSubpath })` and `client.request(method, path, { params, data })`. To fetch every page server-side, pass `{ all: true, pageSize?: number }` to supported list functions.

### Contributing / Codex

If you are updating the SDK with Codex or another coding agent, read [`AGENTS.md`](./AGENTS.md) first. It captures the repo conventions used for the OpenAPI alignment work.

### Publish

```bash
npm run build
npm publish --access public
```

> Not affiliated with ServiceTitan. Respect rate limits and terms.