# 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 = '' COMPANY_ID = 'VNI' PUBLIC_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.