Knowledge Base · ConnectWise PSA Console · REST API v4_6_release

ConnectWise PSA Console

A client management console wired directly to ConnectWise Manage's REST API. Manages companies, contacts, sites, agreements, and onboarding — every action maps to a documented CW endpoint. No fake data in live mode.

File dashboard-connectwise-psa-console.html
API Base /v4_6_release/apis/3.0
Status Demo Mode
Live Data Proxy Required

What This Tool Is

The ConnectWise PSA Console is a dedicated client management interface that replaces ad-hoc CW navigation for the most common MSP operational tasks. It provides a single screen for viewing all clients, editing company and contact records, running bulk contact updates, and onboarding new clients through a guided wizard.

What It Does
List and search all CW companies
View contacts, sites, agreements per client
Edit company and contact records inline
Set primary contacts with correct two-PATCH logic
Add new contacts directly from the detail view
Bulk update contact fields across multiple clients
Onboard new clients through a 6-step wizard
Create company → site → contacts → agreement → ticket in sequence
What It Does Not Do
Manage tickets beyond the onboarding ticket
Replace the full CW Manage interface
Handle time entries or invoicing
Manage service boards or workflows
Bulk-create or bulk-delete companies
Authenticate users (no login layer)
Call CW directly from the browser (CORS blocked)
No bulk contact endpoint exists in CW Manage REST API. The bulk operations panel loops individual PATCH /company/contacts/{id} calls per contact. This is not a limitation of the console — it is a documented gap in the CW Manage API. The console is transparent about this and tracks progress per record.

Architecture

Authentication

ConnectWise Manage uses HTTP Basic authentication with a compound username. The format is specific and must be exact.

// Auth header construction Authorization: Basic base64({companyId}+{publicKey}:{privateKey}) // Example (before base64) mycompany+abc123PUBLIC:xyz789PRIVATE // Required headers on every request (since CW 2019.1) clientId: {your-client-id-uuid} Content-Type: application/json
clientId Requirement
The clientId header became mandatory in ConnectWise 2019.1. Every request without it returns 401. A clientId is a UUID assigned to your integration — register at developer.connectwise.com to obtain one. Internal/private integrations use a private clientId and do not need marketplace listing. Never share your clientId publicly.
Proxy Model
Direct browser-to-CW API calls are blocked by CORS. ConnectWise Manage does not allow cross-origin requests from a browser. A proxy server is required. The proxy holds all credentials — no API keys are ever stored in the HTML file or sent directly from the browser to CW.
Proxy Contract
The console sends requests to PROXY_BASE + path with four custom headers the proxy uses to authenticate with CW:

x-cw-host — your CW hostname (e.g. na.myconnectwise.net)
x-cw-auth — base64-encoded auth string
x-cw-client-id — your clientId UUID

The proxy strips these headers, adds the correct Authorization and clientId headers, and forwards to:
https://{cwHost}/v4_6_release/apis/3.0{path}
// Proxy default URL (change to deployed URL) var PROXY_BASE = 'http://localhost:3001'; // Console sends (browser → proxy) GET http://localhost:3001/cw/company/companies x-cw-host: na.myconnectwise.net x-cw-auth: base64(company+pub:priv) x-cw-client-id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx // Proxy forwards (proxy → CW) GET https://na.myconnectwise.net/v4_6_release/apis/3.0/company/companies Authorization: Basic base64(company+pub:priv) clientId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Content-Type: application/json
Demo vs Live Mode
AspectDemo ModeLive Mode
Data sourceDEMO_CLIENTS array — 8 realistic MSP clientsCW Manage GET /company/companies via proxy
Mode indicatorYellow dot, label: DEMOGreen dot, label: LIVE
Edit companyUpdates local DEMO_CLIENTS in memory, shows [DEMO] toastPATCH /company/companies/{id} via proxy
Edit contactUpdates local contact object in memoryPATCH /company/contacts/{id} via proxy
Bulk opsLoops with [DEMO] toast per contactReal PATCH calls per contact with rate limiting
Onboarding wizardSimulates all 5 API calls, returns fake IDsReal POST calls in sequence, stops on first failure
Config persistenceConfig cleared on clearConfig()Config stored in localStorage (base64-encoded)

UI Panels

KTC Nav Bar — #ktc-bar
Fixed Navigation Strip — 36px
Platform-standard fixed nav bar. Contains:

Platform Hub link — returns to the main platform index.
CW PSA Console label — active page indicator.
API version tag — static label: ConnectWise Manage · REST API v4_6_release.
Mode dot + label — Yellow (DEMO) or green (LIVE). Set by saveConfig().
Live clock — HH:MM:SS, updated every second.
Config Bar — #config-bar
ConnectWise Credentials — 44px strip
Five input fields required to connect to a live CW instance. All five must be filled before the Connect button activates a live session.

Host#cfg-host — Your CW Manage hostname. Cloud-hosted instances use api-na.myconnectwise.net or similar regional variants. On-premise: your own domain.

Company ID#cfg-company — The company identifier used to log into CW (not the company name — the short ID used in the URL).

Public Key#cfg-pub — The public portion of your API member's API key pair. Created under System → Members → API Members.

Private Key#cfg-priv — The private portion. Only visible at creation time — store it securely. Displayed as •••• if auto-restored from localStorage.

Client ID#cfg-cid — UUID registered at developer.connectwise.com. Required on all requests since CW 2019.1.

Connect button — Calls saveConfig(). Stores config in localStorage (base64-encoded), sets MODE to 'live', triggers loadClients().

Clear button — Wipes config from localStorage, resets to demo mode.
Client List — Left Column
340px fixed-width sidebar
Search bar#search-in — Filters client rows in real time by company name, identifier, or city. Client-side filter on the loaded dataset — does not re-query CW.

Active / All filter — Switches between conditions=status/name="Active" and no status filter. Triggers a full reload via loadClients().

Client rows — Each row has a checkbox (for bulk selection) and displays company name, identifier, city/state, and status badge. Clicking a row calls selectClient(id) which opens the detail panel.

Count header — Shows filtered count vs total loaded (e.g. "12 of 124").

Bulk bar — Bottom strip. Shows selected count, a Select All toggle, and the Bulk Ops button. Bulk Ops button disabled when 0 clients selected.

Live source: GET /company/companies?pageSize=1000&orderBy=name+asc
Client Detail Panel — #panel-detail
Default right-panel workspace
Shown when a client row is clicked. In live mode, also triggers fetchClientDetail(id) which fetches fresh contacts, sites, and agreements in parallel.

Hero section — Company name, CW ID, identifier, plus Edit Company and Add Contact buttons. Eight summary fields in a 4-column grid: status (color-coded), type, phone, location, employee count, contact count, agreement count, total MRR.

Contacts section — Expandable. Lists all contacts for this company with avatar initials, name, title, email. Primary contact marked with a “PRIMARY” badge. Each row has Edit and Set as Primary buttons.

Agreements section — Expandable. Lists agreements with name, type, start date, and MRR.

Edit Company modal — Inline overlay. Fields: name, phone, status, website. Submits PATCH /company/companies/{id} as a JSON Patch array.

Edit Contact modal — Fields: first name, last name, title, inactive flag. Submits PATCH /company/contacts/{id}.

Set as Primary — Sends two PATCH calls: one to set the new contact's defaultContactFlag to true, one to clear the previous primary. CW does not enforce this automatically — both must be patched explicitly.

Add Contact modal — Creates a new contact via POST /company/contacts. Appends to the local contact list on success.
Live Sources for Detail Panel
GET /company/contacts?conditions=company%2Fid%3D{id}&pageSize=100
GET /company/companies/{id}/sites
GET /finance/agreements?conditions=company%2Fid%3D{id}&pageSize=50

All three fire in parallel via Promise.all() on row click in live mode.
Bulk Operations — #panel-bulk
Contact field update across multiple clients
Opened via the Bulk Ops button in the client list footer, or by clicking the Bulk Operations tab directly.

Selected client count — Live count from the checkbox selection in the client list. Must be >0 to execute.

Contact filter — Controls which contacts within each selected company are targeted: All contacts, Primary contacts only (defaultContactFlag=true), or Active contacts only (inactiveFlag=false).

Field selector — The CW contact field to update. Options:
  • inactiveFlag — Activate or deactivate contacts (value: true/false)
  • type/name — Contact type (value: CW lookup name)
  • relationship/name — Relationship type
  • department — Department string
  • disablePortalLoginFlag — Portal access toggle

Value field — The new value for the selected field. Field hint updates based on selected field.

Execute button — Disabled until clients are selected, a field is chosen, and a value is entered. Runs the loop.

Progress log — Real-time per-record progress: ✓ ok (green), ✕ error (red), ○ demo/skipped (dim). Counter shows N / Total. Progress bar fills as records complete.

Rate limiting — Pauses 3 seconds every 10 requests to respect CW's ~200 req/min guidance.
No bulk endpoint in CW Manage REST API. Each contact is updated with a separate PATCH /company/contacts/{id} call. This is a documented CW API limitation. For 500 contacts across 50 clients, this is 500 individual API calls taking approximately 2–3 minutes at safe rate. Do not close the browser during execution.
Onboarding Wizard — #panel-onboard

Six-step guided workflow that creates a complete new client record in CW Manage. Each step documents its API call before submission. Steps can be individually skipped (Agreement and Ticket are optional). On submit, all steps execute in sequence — if one fails, subsequent steps are skipped.

StepPanelAPI CallRequired Fields
1. Company#wz-1POST /company/companiesname, identifier (alphanumeric, unique), status
2. Site#wz-2POST /company/companies/{id}/sitesname (defaults to "Main Office")
3. Contacts#wz-3POST /company/contacts × NfirstName, lastName, company.id (from Step 1)
4. Agreement#wz-4POST /finance/agreements (skippable)name, type, startDate, company.id
5. Ticket#wz-5POST /service/tickets (skippable)summary, board.name, company.id
6. Review#wz-6Summary of all steps — confirm before submitN/A
Auto-Fill Behaviour
On Step 1 completion, the wizard pre-fills the Site step with the company's address fields.
The first contact added in Step 3 is set as primary (defaultContactFlag: true) by default.
The onboarding ticket summary is auto-set to "Onboarding — {Company Name}" on Step 1 completion.
Agreement start date defaults to today's date.
The company's country defaults to {id: 1} (US) — update if serving other regions.

API Reference

All endpoints are from the ConnectWise Manage REST API. Base URL: https://{cwHost}/v4_6_release/apis/3.0. All calls are proxied — none are made directly from the browser.

Company Endpoints
EndpointMethodPurposeKey Params / Body
/company/companies GET Load client list on startup and filter change conditions=status/name="Active", pageSize=1000, orderBy=name+asc. Max pageSize is 1000.
/company/companies/{id} PATCH Update company fields (Edit Company modal) JSON Patch array: [{op:"replace", path:"/name", value:"..."}]. One op per field changed. See JSON Patch section.
/company/companies POST Create new company (Onboarding Wizard Step 1) Body: {name, identifier, status:{name}, type:{name}, phoneNumber, website, addressLine1, city, state, zip, country:{id}}. identifier must be alphanumeric, unique across CW.
Contact Endpoints
EndpointMethodPurposeKey Params / Body
/company/contacts GET Load contacts for selected company conditions=company%2Fid%3D{id}, pageSize=100. URL-encode the equals sign as %3D and slash as %2F.
/company/contacts/{id} PATCH Edit contact, set primary, bulk updates JSON Patch array. Setting primary requires two calls: defaultContactFlag:true on new primary AND defaultContactFlag:false on old primary. CW does not auto-clear the previous primary.
/company/contacts POST Add contact (detail panel + onboarding wizard) Body: {firstName, lastName, title, email, company:{id}, defaultContactFlag, inactiveFlag}. Required: firstName, lastName, company.id.
Site Endpoints
EndpointMethodPurposeKey Params / Body
/company/companies/{id}/sites GET List sites for a company (detail panel) No additional params required.
/company/companies/{id}/sites POST Create default site (Onboarding Wizard Step 2) Body: {name, phoneNumber, addressLine1, city, state, zip, timeZone:{name}}. Required: name. The timeZone.name must match a CW lookup value (e.g. "Central Standard Time").
Agreement Endpoints
EndpointMethodPurposeKey Params / Body
/finance/agreements GET Load agreements for selected company (detail panel) conditions=company%2Fid%3D{id}, pageSize=50.
/finance/agreements POST Create managed service agreement (Onboarding Wizard Step 4) Body: {name, type:{name}, company:{id}, startDate, endDate, billAmount, billCycleType}. Required: name, type, startDate, company. The type.name must match an existing agreement type in CW.
Ticket Endpoints
EndpointMethodPurposeKey Params / Body
/service/tickets POST Create onboarding ticket (Onboarding Wizard Step 5) Body: {summary, company:{id}, board:{name}, status:{name}, priority:{name}, owner:{identifier}, initialDescription}. Required: summary, board, company. The board.name must exactly match a service board name in CW.
JSON Patch Format

All PATCH calls use the JSON Patch standard (RFC 6902). ConnectWise Manage requires an array of operation objects. Only replace is used in this console.

// PATCH /company/companies/{id} body [ { "op": "replace", "path": "/name", "value": "New Company Name" }, { "op": "replace", "path": "/status/name", "value": "Active" }, { "op": "replace", "path": "/phoneNumber","value": "312-555-0100" } ] // PATCH /company/contacts/{id} — set primary [ { "op": "replace", "path": "/defaultContactFlag", "value": true } ]
Paths use forward-slash notation matching the JSON field names. Nested objects use /parent/child format (e.g. /status/name). Boolean values are JSON booleans, not strings.

Config Fields

Auth Config — CW_CONFIG
FieldInput IDDescription
host#cfg-hostCW Manage hostname. Cloud-hosted: na.myconnectwise.net (NA), eu.myconnectwise.net (EU), au.myconnectwise.net (AU). On-premise: your own domain.
company#cfg-companyCompany identifier used in the auth string. Not the display name — the short login ID (visible in the CW URL).
publicKey#cfg-pubPublic API key from System → Members → API Members → {member} → API Keys.
privateKey#cfg-privPrivate API key. Only shown once at creation. Store securely. Sent as part of the base64 auth string.
clientId#cfg-cidUUID from developer.connectwise.com. Required header on all requests since 2019.1.
Config is stored in localStorage as cw_config (base64-encoded JSON) and auto-restored on page load. The private key is masked as •••••••• after restore. Clicking Clear removes the entry from localStorage completely.
Proxy Config — PROXY_BASE
ConstantDefaultDescription
PROXY_BASE'http://localhost:3001'Base URL of the proxy server. Change to your deployed proxy URL before going live. All apiFetch() calls prefix this to the CW path.
Demo Data — DEMO_CLIENTS

Eight realistic MSP client objects mirroring the CW Manage company response shape. Each client has embedded contacts[] and agreements[] arrays for full detail panel demonstration without API calls.

Demo Client Structure
id, name, identifier, status.name, type.name
phoneNumber, city, state, numberOfEmployees
contacts[] — array of contact objects with defaultContactFlag, inactiveFlag
agreements[] — array of agreement objects with billAmount

Demo clients include Active, Inactive, and Prospect status examples. Contacts range from 1 to 3 per client. Two clients have no agreements (prospect and inactive scenarios).

Proxy Activation Checklist

Complete in order. The console runs fully in demo mode before proxy deployment — all panels and wizards are testable with realistic data.

Step 1 — Register a Client ID
Register at developer.connectwise.com to obtain a clientId UUID. Select “Private” type for an internal integration that will not be listed on the marketplace. This is free and required since CW 2019.1. Store the UUID securely.
Step 2 — Create an API Member in CW Manage
System → Members → API Members → + → Create a new API member. Assign a Security Role with at minimum: Company (read/write), Contacts (read/write), Finance/Agreements (read/write), Service/Tickets (read/write). Click API Keys and create a new key pair — save the private key immediately, it will not be shown again.
Step 3 — Deploy Proxy Server
Deploy a Node.js or Cloudflare Worker proxy that:
• Accepts /cw/* routes
• Reads x-cw-host, x-cw-auth, x-cw-client-id headers from the browser request
• Forwards to https://{x-cw-host}/v4_6_release/apis/3.0/{path}
• Adds Authorization: Basic {x-cw-auth} and clientId: {x-cw-client-id} headers
• Returns CORS headers to allow browser requests
• Strips the custom x-cw-* headers before forwarding
Step 4 — Update PROXY_BASE
Change var PROXY_BASE = 'http://localhost:3001' to your deployed proxy URL. No trailing slash.
Step 5 — Update Demo Client Names (Optional)
The DEMO_CLIENTS array contains fictional company names. In production these are not shown once connected. Optionally update them to reflect real client names for training purposes. The demo data is never sent to CW.
Step 6 — Update Wizard Defaults
In the onboarding wizard, update the default service board name (document.getElementById('wz-ticket-board').value = 'Service Desk') to match your actual CW board name. The country default is {id: 1} (US) — update if serving other regions. Verify agreement type names match your CW agreement type lookup table.
Step 7 — Enter Credentials and Verify
Enter all five config bar fields and click Connect. Mode dot turns green. Client list loads real companies. Click a client and verify contacts and agreements load. Run a test onboarding with a dummy company name.
Activation confirmed when: Mode dot green, label LIVE. Client list shows real CW companies. Clicking a client loads real contacts. Edit company PATCH returns 200. Onboarding wizard creates a real company record visible in CW Manage.

Limitations

API Limitation No Bulk Contact Endpoint in CW Manage
ConnectWise Manage REST API does not have a bulk PATCH endpoint for contacts. There is no POST /company/contacts/bulk or equivalent. The console loops individual PATCH /company/contacts/{id} calls. For 500 contacts this is 500 separate API calls. This is a CW API design decision, not a console limitation.
API Limitation Setting Primary Contact Requires Two Calls
CW Manage does not automatically clear defaultContactFlag on other contacts when one is set to primary. The console sends two PATCH calls: one to set the new primary to true, one to set the previous primary to false. If the second call fails, CW will have two primary contacts.
API Limitation pageSize Maximum is 1000
CW Manage enforces a maximum pageSize of 1000 per request. If you have more than 1000 active companies, the client list will be incomplete. Pagination (page=2, page=3, etc.) would need to be implemented to load the full dataset. Currently not implemented — the console assumes <1000 active companies.
Not Yet Live Contact Search is Client-Side Only
The search bar filters the already-loaded client list in memory. It does not issue a new GET /company/companies?conditions=name like "{query}%" call to CW. For large MSPs with many companies, this is sufficient. For very large datasets, server-side search via the conditions parameter would be more performant.
Not Yet Live Agreement Types Must Exist in CW
The onboarding wizard hardcodes agreement type names (Managed Service, Block Hours, Per Device, etc.). These must exactly match agreement types configured in your CW Manage instance under Finance → Agreement Types. If the name does not match, the POST /finance/agreements call will return a 400 error.
Not Yet Live Service Board Name Must Match CW
The onboarding ticket step uses a free-text board name input defaulting to “Service Desk”. If this does not match a real board name in CW, the ticket creation will fail. The console does not populate the board input from GET /service/boards — that would require an additional lookup call.
Limitation No Authentication Layer
No login, session management, or RBAC. Anyone with the URL and valid CW credentials can use the console. Restrict access via VPN, internal network, or reverse proxy auth. The CW API key itself provides access control — use an API member with appropriately scoped security roles.

Troubleshooting

Connect button pressed but client list does not load
Cause 1: Proxy is not running at PROXY_BASE. Open the browser console — you will see a network error on /cw/company/companies.
Cause 2: CORS not configured on proxy — check the proxy adds Access-Control-Allow-Origin: * to all responses.
Cause 3: Wrong hostname — verify the host field matches your CW URL exactly, no https:// prefix, no trailing slash.
401 Unauthorized on all requests
Cause 1: Missing or wrong clientId. Verify the UUID is correct and registered at developer.connectwise.com.
Cause 2: Auth string format wrong. Must be base64({companyId}+{publicKey}:{privateKey}) — note the + between companyId and publicKey, and the : between public and private.
Cause 3: API member is inactive or the security role has been changed.
400 Bad Request on onboarding POST calls
Company POST: Identifier is not unique, contains invalid characters (must be alphanumeric), or required fields are missing.
Agreement POST: type.name does not match an agreement type in CW. Check Finance → Agreement Types.
Ticket POST: board.name does not match a service board. Check Service → Service Boards. Status and priority names must also match CW lookup values exactly.
Bulk operation completes but CW records not updated
Cause: Demo mode is still active — all bulk PATCH calls return [DEMO] toast instead of calling CW. Verify the mode dot is green and the config bar shows “Connected”.
Also check: The CW API member's security role has write access to Company → Contacts. Read-only roles will return 403 on PATCH calls.
Contacts section shows empty after clicking a client
Cause 1: In demo mode, contacts are embedded in DEMO_CLIENTS. If the client row exists but has no contacts array, the section shows empty.
Cause 2: In live mode, the GET /company/contacts?conditions=company%2Fid%3D{id} call failed. Check the URL-encoding — the / in company/id must be %2F and the = must be %3D. Some proxies double-decode these.
Cause 3: The company genuinely has no contacts in CW.
Setting primary contact fails or results in two primary contacts
Cause: The second PATCH call (to clear the old primary) succeeded but the first (to set the new) failed, or vice versa. CW does not do this atomically. Check browser console for which call errored. Manually patch the incorrect contact via the Edit Contact modal if needed.
Client list shows only 1000 companies
Expected behaviour. CW Manage enforces a maximum pageSize of 1000. If you have more than 1000 active companies, implement pagination by tracking the total count from the response header and making sequential page requests. Not yet implemented in the console.
KRAWCZYK.CITY · KB · CONNECTWISE PSA CONSOLE · COPILOT PLATFORM