2.6 KiB
2.6 KiB
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=+<PUBLIC_KEY>
CW_PRIVATE_KEY=<PRIVATE_KEY>
WRONG: Base64-encoding the entire file contents as the Authorization header. RIGHT: Parse the key=value pairs first.
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": "<CLIENT_ID>",
"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
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 |
|---|---|
<Client A> |
<companyId> |
<Client B> |
<companyId> |
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>[...] |