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.
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.
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
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)
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
ConnectWise Manage uses HTTP Basic authentication with a compound username. The format is specific and must be exact.
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_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 stringx-cw-client-id — your clientId UUIDThe proxy strips these headers, adds the correct
Authorization and clientId headers, and forwards to:https://{cwHost}/v4_6_release/apis/3.0{path}
| Aspect | Demo Mode | Live Mode |
|---|---|---|
| Data source | DEMO_CLIENTS array — 8 realistic MSP clients | CW Manage GET /company/companies via proxy |
| Mode indicator | Yellow dot, label: DEMO | Green dot, label: LIVE |
| Edit company | Updates local DEMO_CLIENTS in memory, shows [DEMO] toast | PATCH /company/companies/{id} via proxy |
| Edit contact | Updates local contact object in memory | PATCH /company/contacts/{id} via proxy |
| Bulk ops | Loops with [DEMO] toast per contact | Real PATCH calls per contact with rate limiting |
| Onboarding wizard | Simulates all 5 API calls, returns fake IDs | Real POST calls in sequence, stops on first failure |
| Config persistence | Config cleared on clearConfig() | Config stored in localStorage (base64-encoded) |
UI Panels
#ktc-barPlatform 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-barHost —
#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.
#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
#panel-detailfetchClientDetail(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.
GET /company/contacts?conditions=company%2Fid%3D{id}&pageSize=100GET /company/companies/{id}/sitesGET /finance/agreements?conditions=company%2Fid%3D{id}&pageSize=50All three fire in parallel via
Promise.all() on row click in live mode.
#panel-bulkSelected 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 toggleValue 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.
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.#panel-onboardSix-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.
| Step | Panel | API Call | Required Fields |
|---|---|---|---|
| 1. Company | #wz-1 | POST /company/companies | name, identifier (alphanumeric, unique), status |
| 2. Site | #wz-2 | POST /company/companies/{id}/sites | name (defaults to "Main Office") |
| 3. Contacts | #wz-3 | POST /company/contacts × N | firstName, lastName, company.id (from Step 1) |
| 4. Agreement | #wz-4 | POST /finance/agreements (skippable) | name, type, startDate, company.id |
| 5. Ticket | #wz-5 | POST /service/tickets (skippable) | summary, board.name, company.id |
| 6. Review | #wz-6 | Summary of all steps — confirm before submit | N/A |
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.
| Endpoint | Method | Purpose | Key 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. |
| Endpoint | Method | Purpose | Key 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. |
| Endpoint | Method | Purpose | Key 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"). |
| Endpoint | Method | Purpose | Key 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. |
| Endpoint | Method | Purpose | Key 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. |
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.
/parent/child format (e.g. /status/name). Boolean values are JSON booleans, not strings.Config Fields
CW_CONFIG| Field | Input ID | Description |
|---|---|---|
host | #cfg-host | CW Manage hostname. Cloud-hosted: na.myconnectwise.net (NA), eu.myconnectwise.net (EU), au.myconnectwise.net (AU). On-premise: your own domain. |
company | #cfg-company | Company identifier used in the auth string. Not the display name — the short login ID (visible in the CW URL). |
publicKey | #cfg-pub | Public API key from System → Members → API Members → {member} → API Keys. |
privateKey | #cfg-priv | Private API key. Only shown once at creation. Store securely. Sent as part of the base64 auth string. |
clientId | #cfg-cid | UUID from developer.connectwise.com. Required header on all requests since 2019.1. |
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_BASE| Constant | Default | Description |
|---|---|---|
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_CLIENTSEight 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.
id, name, identifier, status.name, type.namephoneNumber, city, state, numberOfEmployeescontacts[] — array of contact objects with defaultContactFlag, inactiveFlagagreements[] — array of agreement objects with billAmountDemo 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.
• 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
var PROXY_BASE = 'http://localhost:3001' to your deployed proxy URL. No trailing slash.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.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.Limitations
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.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.page=2, page=3, etc.) would need to be implemented to load the full dataset. Currently not implemented — the console assumes <1000 active companies.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.POST /finance/agreements call will return a 400 error.GET /service/boards — that would require an additional lookup call.Troubleshooting
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.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.
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.Also check: The CW API member's security role has write access to Company → Contacts. Read-only roles will return 403 on PATCH calls.
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.