# ConnectWise Raw API Authentication & Time Tracking ## Correct Auth Construction The credential file at `~/.secrets/connectwise` is a multi-line `.env` file, NOT a raw string. ``` CW_COMPANY_ID=VNI CW_PUBLIC_KEY=+ CW_PRIVATE_KEY= ``` **WRONG:** Base64-encoding the entire file contents as the Authorization header. **RIGHT:** Parse the key=value pairs first. ```python import base64 import os with open(os.path.expanduser("~/.secrets/connectwise"), "r") as f: keys = {} for line in f.read().strip().splitlines(): if '=' in line and not line.startswith('#'): k, v = line.split('=', 1) keys[k.strip()] = v.strip() company_id = keys['CW_COMPANY_ID'] public_key = keys['CW_PUBLIC_KEY'] private_key = keys['CW_PRIVATE_KEY'] # CRITICAL: public_key already starts with '+', so do NOT add another auth_string = f"{company_id}+{public_key}:{private_key}" if '++' in auth_string: auth_string = auth_string.replace('++', '+') encoded_auth = base64.b64encode(auth_string.encode()).decode() headers = { "Authorization": f"Basic {encoded_auth}", "clientId": "", "Content-Type": "application/json" } ``` ## Time Entries (Not `/service/time`) | Wrong | Right | |-------|-------| | `/service/time` | `/time/entries` | | `hours` field | `actualHours` field | | `dateEntered>'...'` (quotes) | `dateEntered>[...]` (square brackets) | ### Working Example ```python from datetime import datetime, timedelta twelve_months_ago = (datetime.utcnow() - timedelta(days=365)).strftime('%Y-%m-%dT%H:%M:%SZ') url = ( "https://cw.vistanetinc.com/v4_6_release/apis/3.0" f"/time/entries?conditions=company/id={company_id} and dateEntered>[{twelve_months_ago}]" "&pageSize=1000" ) resp = requests.get(url, headers=headers, timeout=30) entries = resp.json() total_hours = sum(e.get('actualHours', 0) or 0 for e in entries) ``` ## Base URL - **VistaNet internal:** `https://cw.vistanetinc.com/v4_6_release/apis/3.0` - **Public endpoints** (`api-na.connectwise.com`) are **not reachable** from this environment. ## Known Company IDs | Company | ID | |---------|----| | `` | `` | | `` | `` | ## Summary of Errors This Fixes | Symptom | Root Cause | Fix | |---------|-----------|-----| | `500 Length cannot be less than zero` | Auth header was garbage (base64 of entire .env file) | Parse key=value pairs | | `403 Forbidden` on `/service/time` | Wrong endpoint | Use `/time/entries` | | `None` or `0` for hours | Wrong field name | Use `actualHours` | | `400 ApiFindCondition` for datetime | Quoted string instead of bracket syntax | Use `dateEntered>[...]` |