ConnectWise PSA API Patterns
Overview
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.
Base URLs
| 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).
Legacy URLs
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/
Authentication
Public/Private Key + Client ID
ConnectWise PSA uses Basic Authentication with a combined credential string plus a Client ID header.
Credential Format
Authorization: Basic base64({companyId}+{publicKey}:{privateKey})
clientId: {your-client-id}
Step-by-Step Authentication
- •
Combine credentials:
codecompanyId + "+" + publicKey + ":" + privateKey Example: company+publickey:privatekey
- •
Base64 encode:
codebase64("company+publickey:privatekey") = "Y29tcGFueStwdWJsaWNrZXk6cHJpdmF0ZWtleQ==" - •
Set headers:
httpAuthorization: Basic Y29tcGFueStwdWJsaWNrZXk6cHJpdmF0ZWtleQ== clientId: your-registered-client-id Content-Type: application/json
Example Request
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
JavaScript Authentication Example
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'
};
Obtaining Credentials
- •API Member: Create in System > Members > API Members
- •Public/Private Keys: Generate for API member
- •Client ID: Register at ConnectWise Developer Portal
Conditions Query Syntax
Basic Syntax
conditions=field operator value
Supported Operators
| 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) |
Field References
Use / to reference nested fields:
company/id=12345 status/name="New" contact/firstName contains "John"
Combining Conditions
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 Conditions
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]
String Conditions
Exact match:
conditions=summary="Email not working"
Contains:
conditions=summary contains "email"
Like (wildcards):
conditions=summary like "%email%" conditions=company/identifier like "AC%"
Null Checks
conditions=contact=null conditions=assignedResource!=null
URL Encoding
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
Pagination
Request Parameters
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
page | int | 1 | - | Page number (1-based) |
pageSize | int | 25 | 1000 | Records per page |
Example Request
GET /service/tickets?page=1&pageSize=100
Response Headers
| Header | Description |
|---|---|
Link | Contains next/prev page URLs |
X-Total-Count | Total record count (if requested) |
Pagination Example
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;
}
Getting Total Count
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
Rate Limiting
Limits
| Limit | Value |
|---|---|
| Requests per minute | 60 |
| Per API member | Yes |
Rate Limit Headers
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per minute |
X-RateLimit-Remaining | Requests remaining in window |
X-RateLimit-Reset | Seconds until limit resets |
429 Response
When rate limited, you receive HTTP 429:
{
"code": "RateLimitExceeded",
"message": "Rate limit exceeded. Try again in 30 seconds."
}
Retry Strategy
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');
}
Best Practices for Rate Limits
- •Implement exponential backoff - Don't hammer the API
- •Check headers - Monitor remaining requests
- •Batch operations - Reduce total requests
- •Cache reference data - Queues, statuses, members
- •Use webhooks - Instead of polling for changes
Error Handling
HTTP Status Codes
| 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 |
Error Response Format
{
"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"
}
]
}
Common Errors
| 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 |
Common API Patterns
Field Selection
Request specific fields only:
GET /service/tickets?fields=id,summary,status/name,company/name
Ordering
GET /service/tickets?orderBy=priority/id asc, dateEntered desc
Child Collections
Include child records:
GET /service/tickets?childconditions=notes/text contains "update"
Custom Fields
GET /service/tickets?customFieldConditions=customField1 contains "value"
Webhook Configuration
Webhook Callback
ConnectWise can POST to your endpoint on entity changes:
{
"Action": "updated",
"ID": 54321,
"Type": "ticket",
"MemberID": 123,
"Callback": {
"ID": 54321,
"Type": "ticket"
}
}
Registering Callbacks
POST /system/callbacks
Content-Type: application/json
{
"url": "https://your-server.com/webhook",
"objectId": 0,
"type": "ticket",
"level": "owner",
"description": "Ticket updates webhook"
}
Environment Configuration
Recommended Environment Variables
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"
Configuration Object
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'
};
Best Practices
- •Store credentials securely - Never commit to source control
- •Use environment variables - For configuration
- •Implement rate limit handling - Don't get blocked
- •Cache reference data - Reduce API calls
- •Handle errors gracefully - Retry transient failures
- •Use pagination - Don't fetch unbounded results
- •Select needed fields - Reduce payload size
- •Log API calls - For debugging and audit
- •Test in sandbox - Before production changes
- •Monitor usage - Track API call patterns
API Documentation
Related Skills
- •ConnectWise Tickets - Ticket management
- •ConnectWise Companies - Company management
- •ConnectWise Contacts - Contact management
- •ConnectWise Projects - Project management
- •ConnectWise Time Entries - Time tracking