From connectwise-psa
Provides ConnectWise Manage PSA REST API patterns: authentication with public/private keys and clientId, pagination via page/pageSize, conditions query syntax, 60/min rate limiting, and error handling.
npx claudepluginhub wyre-technology/msp-claude-plugins --plugin connectwise-psaThis skill uses the workspace's default tool permissions.
The ConnectWise PSA REST API provides access to all PSA entities including tickets, companies, contacts, projects, and time entries. This skill covers authentication, query syntax, pagination, rate limiting, and best practices for API integration.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Reviews prose for communication issues impeding comprehension, outputs minimal fixes in a three-column table per Microsoft Writing Style Guide. Useful for 'review prose' or 'improve prose' requests.
The ConnectWise PSA REST API provides access to all PSA entities including tickets, companies, contacts, projects, and time entries. This skill covers authentication, query syntax, pagination, rate limiting, and best practices for API integration.
| Region | Base URL |
|---|---|
| North America | https://api-na.myconnectwise.net/{codebase}/apis/3.0/ |
| Europe | https://api-eu.myconnectwise.net/{codebase}/apis/3.0/ |
| Australia | https://api-au.myconnectwise.net/{codebase}/apis/3.0/ |
Replace {codebase} with your company identifier (e.g., v4_6_release or custom).
Some instances may use legacy URLs:
https://api-na.myconnectwise.net/v4_6_release/apis/3.0/
https://api-staging.connectwisedev.com/v4_6_release/apis/3.0/
ConnectWise PSA uses Basic Authentication with a combined credential string plus a Client ID header.
Authorization: Basic base64({companyId}+{publicKey}:{privateKey})
clientId: {your-client-id}
Combine credentials:
companyId + "+" + publicKey + ":" + privateKey
Example: company+publickey:privatekey
Base64 encode:
base64("company+publickey:privatekey") = "Y29tcGFueStwdWJsaWNrZXk6cHJpdmF0ZWtleQ=="
Set headers:
Authorization: Basic Y29tcGFueStwdWJsaWNrZXk6cHJpdmF0ZWtleQ==
clientId: your-registered-client-id
Content-Type: application/json
GET /v4_6_release/apis/3.0/service/tickets
Host: api-na.myconnectwise.net
Authorization: Basic Y29tcGFueStwdWJsaWNrZXk6cHJpdmF0ZWtleQ==
clientId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Content-Type: application/json
const companyId = process.env.CONNECTWISE_COMPANY_ID;
const publicKey = process.env.CONNECTWISE_PUBLIC_KEY;
const privateKey = process.env.CONNECTWISE_PRIVATE_KEY;
const clientId = process.env.CONNECTWISE_CLIENT_ID;
const credentials = `${companyId}+${publicKey}:${privateKey}`;
const base64Credentials = Buffer.from(credentials).toString('base64');
const headers = {
'Authorization': `Basic ${base64Credentials}`,
'clientId': clientId,
'Content-Type': 'application/json'
};
conditions=field operator value
| Operator | Description | Example |
|---|---|---|
= | Equals | status/id=1 |
!= | Not equals | status/id!=5 |
< | Less than | priority/id<3 |
<= | Less than or equal | priority/id<=2 |
> | Greater than | dateEntered>2024-01-01 |
>= | Greater than or equal | dateEntered>=2024-01-01 |
contains | Contains substring | summary contains "email" |
like | Pattern match | summary like "%email%" |
in | In list | status/id in (1,2,3) |
not in | Not in list | status/id not in (5) |
Use / to reference nested fields:
company/id=12345
status/name="New"
contact/firstName contains "John"
AND (default):
conditions=company/id=12345 and status/id!=5 and priority/id<=2
OR:
conditions=status/id=1 or status/id=2
Complex:
conditions=(status/id=1 or status/id=2) and company/id=12345
Date format: YYYY-MM-DD or ISO 8601
conditions=dateEntered>=[2024-01-01]
conditions=dateEntered>=[2024-01-01T00:00:00Z] and dateEntered<[2024-02-01T00:00:00Z]
Exact match:
conditions=summary="Email not working"
Contains:
conditions=summary contains "email"
Like (wildcards):
conditions=summary like "%email%"
conditions=company/identifier like "AC%"
conditions=contact=null
conditions=assignedResource!=null
Special characters must be URL-encoded:
| Character | Encoded |
|---|---|
| Space | %20 |
= | %3D |
< | %3C |
> | %3E |
" | %22 |
Example:
GET /service/tickets?conditions=company/id%3D12345%20and%20status/id!%3D5
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
page | int | 1 | - | Page number (1-based) |
pageSize | int | 25 | 1000 | Records per page |
GET /service/tickets?page=1&pageSize=100
| Header | Description |
|---|---|
Link | Contains next/prev page URLs |
X-Total-Count | Total record count (if requested) |
async function fetchAllTickets(conditions) {
const allTickets = [];
let page = 1;
const pageSize = 250;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`${baseUrl}/service/tickets?conditions=${conditions}&page=${page}&pageSize=${pageSize}`,
{ headers }
);
const tickets = await response.json();
allTickets.push(...tickets);
hasMore = tickets.length === pageSize;
page++;
}
return allTickets;
}
GET /service/tickets?conditions=status/id!=5&pageSize=1&fields=id
Check X-Total-Count header or use /count endpoint:
GET /service/tickets/count?conditions=status/id!=5
| Limit | Value |
|---|---|
| Requests per minute | 60 |
| Per API member | Yes |
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per minute |
X-RateLimit-Remaining | Requests remaining in window |
X-RateLimit-Reset | Seconds until limit resets |
When rate limited, you receive HTTP 429:
{
"code": "RateLimitExceeded",
"message": "Rate limit exceeded. Try again in 30 seconds."
}
async function requestWithRetry(url, options, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
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;
}
throw new Error('Max retries exceeded');
}
| Code | Meaning | Action |
|---|---|---|
| 200 | Success | Process response |
| 201 | Created | Entity created |
| 204 | No Content | Delete successful |
| 400 | Bad Request | Check request format |
| 401 | Unauthorized | Verify credentials |
| 403 | Forbidden | Check permissions |
| 404 | Not Found | Entity doesn't exist |
| 409 | Conflict | Record locked/modified |
| 429 | Rate Limited | Implement backoff |
| 500 | Server Error | Retry with backoff |
{
"code": "InvalidArgument",
"message": "The value 'invalid' is not valid for field 'status/id'.",
"errors": [
{
"code": "InvalidArgument",
"message": "status/id must be a valid integer",
"field": "status/id"
}
]
}
| Error | Cause | Resolution |
|---|---|---|
InvalidCredentials | Bad auth | Verify company ID, keys |
MissingClientId | No clientId header | Add clientId header |
InvalidArgument | Bad field value | Check field type/values |
RequiredFieldMissing | Missing required field | Add required fields |
RecordNotFound | Entity doesn't exist | Verify ID exists |
RecordLocked | Being edited | Retry after delay |
Request specific fields only:
GET /service/tickets?fields=id,summary,status/name,company/name
GET /service/tickets?orderBy=priority/id asc, dateEntered desc
Include child records:
GET /service/tickets?childconditions=notes/text contains "update"
GET /service/tickets?customFieldConditions=customField1 contains "value"
ConnectWise can POST to your endpoint on entity changes:
{
"Action": "updated",
"ID": 54321,
"Type": "ticket",
"MemberID": 123,
"Callback": {
"ID": 54321,
"Type": "ticket"
}
}
POST /system/callbacks
Content-Type: application/json
{
"url": "https://your-server.com/webhook",
"objectId": 0,
"type": "ticket",
"level": "owner",
"description": "Ticket updates webhook"
}
export CONNECTWISE_COMPANY_ID="your-company-id"
export CONNECTWISE_PUBLIC_KEY="your-public-key"
export CONNECTWISE_PRIVATE_KEY="your-private-key"
export CONNECTWISE_CLIENT_ID="your-client-id"
export CONNECTWISE_SITE="api-na.myconnectwise.net"
const config = {
companyId: process.env.CONNECTWISE_COMPANY_ID,
publicKey: process.env.CONNECTWISE_PUBLIC_KEY,
privateKey: process.env.CONNECTWISE_PRIVATE_KEY,
clientId: process.env.CONNECTWISE_CLIENT_ID,
site: process.env.CONNECTWISE_SITE || 'api-na.myconnectwise.net',
apiPath: '/apis/3.0'
};