ConnectWise PSA skills and tools under ops/connectwise (hub-sanitized 2026-11-01: client intel, internal workflow/credential/member/company ids, archived-page apiToken/sessionId stripped to placeholders)
This commit is contained in:
commit
9c96092034
36 files changed
+310875
No files matched your search
@@ -0,0 +1,68 @@
|
||||
# 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:
|
||||
1. Fetch open tickets using `conditions=id >= [RecentID] AND closedFlag=false` with a large `pageSize`.
|
||||
2. Filter JSON **locally in Python** for the target company by `company.id`.
|
||||
3. (Optional) Filter for specific owner, board, or other criteria locally.
|
||||
4. 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
|
||||
```python
|
||||
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:
|
||||
- `closedFlag` is `false`
|
||||
- Status name does NOT start with `>` (e.g., `>Closed`, `>Completed`)
|
||||
- `dateResolved` is `None` or 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):
|
||||
1. Resolve the company: `GET /company/companies?conditions=name contains 'Partial'` → cache `company.id`.
|
||||
2. Size the corpus: `GET /service/tickets/count?conditions=company/id={id}` (the count endpoint honors the condition, unlike the list endpoint's bare `companyId` param). Bound it with `AND 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.
|
||||
3. 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,dateResolved` until a short page. Persist the array to a scratch JSON file between calls — kernel state does not survive a timeout.
|
||||
4. Per ticket: `GET /service/tickets/{id}/notes?pageSize=1000` and regex the `text` client-side. pageSize=1000 on a single ticket's notes is safe; the corruption concern applies to large LIST fetches only.
|
||||
5. 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=true` notes written by members. Email/contact notes carry `member: null`; read `contact` or 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.
|
||||
|
||||
Reference in new issue
Block a user