4.3 KiB
ConnectWise Ticket Fetching Implementation
Implementation details for fetching tickets from the VistaNet ConnectWise instance.
API Configuration
- Base URL:
https://cw.vistanetinc.com/v4_6_release/apis/3.0 - Authentication: Basic Auth using
companyid+publickey:privatekey. - Required Header:
clientId(Crucial: 401 Unauthorized is returned if this is missing or incorrect).
Implementation Pattern
Since server-side filtering is unreliable, the reliable pattern is:
- Fetch open tickets using
conditions=id >= [RecentID] AND closedFlag=falsewith a largepageSize. - Filter JSON locally in Python for the target company by
company.id. - (Optional) Filter for specific owner, board, or other criteria locally.
- Filter out statuses starting with
>(e.g.,>Completed) to identify truly open tickets.
Why local filtering is required
The companyId query parameter on /service/tickets is silently ignored by the API and returns tickets from all companies. Server-side conditions on identifiers also often return empty lists or incorrect results. Always fetch broad and filter client-side.
Example: All open tickets for a specific company
import base64, requests, json
BASE_URL = 'https://cw.vistanetinc.com/v4_6_release/apis/3.0'
CLIENT_ID = '<CLIENT_ID>'
COMPANY_ID = 'VNI'
PUBLIC_KEY = '<PUBLIC_KEY>'
PRIVATE_KEY = '<PRIVATE_KEY>'
auth_b64 = base64.b64encode(f'{COMPANY_ID}+{PUBLIC_KEY}:{PRIVATE_KEY}'.encode()).decode()
headers = {
'Authorization': f'Basic {auth_b64}',
'clientId': CLIENT_ID,
'Content-Type': 'application/json'
}
# 1. Fetch broad — companyId is ignored, so we use id range to avoid legacy mirroring
url = f'{BASE_URL}/service/tickets?pageSize=1000&conditions=id%20%3E%3D%2080000%20AND%20closedFlag%3Dfalse'
resp = requests.get(url, headers=headers, timeout=45)
resp.raise_for_status()
tickets = resp.json()
# 2. Filter client-side for Miller Glass (company.id == 19502)
miller_tickets = [
t for t in tickets
if t.get('company', {}).get('id') == 19502
and not t.get('status', {}).get('name', '').startswith('>')
and t.get('dateResolved') is None
]
"Open" ticket definition
A ticket is considered open when ALL of the following are true:
closedFlagisfalse- Status name does NOT start with
>(e.g.,>Closed,>Completed) dateResolvedisNoneor absent
Searching ticket NOTES for text across a company's tickets
Note text is NOT searchable server-side on this instance: POST /service/tickets/search accepts childconditions in its FilterValues body but silently ignores it — verified that six different childconditions values (notes/text contains '...') all returned identical result sets, with no error to warn you. Never conclude a note doesn't exist based on a childconditions query.
Working pattern (validated end-to-end, ~0.25 s/ticket):
- Resolve the company:
GET /company/companies?conditions=name contains 'Partial'→ cachecompany.id. - Size the corpus:
GET /service/tickets/count?conditions=company/id={id}(the count endpoint honors the condition, unlike the list endpoint's barecompanyIdparam). Bound it withAND dateEntered >= [YYYY-01-01T00:00:00Z]when the ask is recency-scoped — the date condition is also honored and shrank a 4,301-ticket corpus to 1,086. - Page-fetch ticket ids:
GET /service/tickets?conditions=company/id={id} AND dateEntered >= [...]&pageSize=200&page={n}&orderBy=id asc&fields=id,summary,status/name,closedFlag,dateResolveduntil a short page. Persist the array to a scratch JSON file between calls — kernel state does not survive a timeout. - Per ticket:
GET /service/tickets/{id}/notes?pageSize=1000and regex thetextclient-side. pageSize=1000 on a single ticket's notes is safe; the corruption concern applies to large LIST fetches only. - Triage matches by snippet before reporting: generic keywords ('office', 'field') drown in "out of the office" autoresponder noise from email-logged notes — the signal is usually in
internalFlag=truenotes written by members. Email/contact notes carrymember: null; readcontactor the text for attribution.
Budget runtime: 1,000+ tickets of note fetches takes ~4–5 minutes — chunk the scan (~300 tickets per execute_code call) and append matches to a scratch JSON so a timeout loses nothing.