ConnectWise Manage Contacts — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ConnectWise Manage Contacts (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Contacts in ConnectWise PSA represent individuals at your client companies. Contacts can be ticket requestors, agreement signers, project stakeholders, and portal users. This skill covers contact CRUD operations, communication items, contact types, and portal access management.
defaults hang off the company record, not its contacts; use connectwise-psa-companies.
/system/members, aseparate entity that contact searches never return; query it through connectwise-psa-api-patterns.
Base: /company/contactsStandard contact types in ConnectWise PSA:
| Type ID | Name | Description |
|---|---|---|
| 1 | Admin | Administrative contact |
| 2 | Primary | Main point of contact |
| 3 | Billing | Billing/accounts payable |
| 4 | Technical | Technical contact |
| 5 | Sales | Sales contact |
Note: Contact types are configurable. Query /company/contacts/types for your instance's types.
| Field | Type | Required | Description |
|---|---|---|---|
id | int | System | Auto-generated unique identifier |
firstName | string(30) | Yes | Contact first name |
lastName | string(30) | No | Contact last name |
company | object | Yes | {id: companyId} - Parent company |
site | object | No | {id: siteId} - Company site |
type | object | No | {id: typeId} - Contact type |
| Field | Type | Required | Description |
|---|---|---|---|
title | string(50) | No | Job title |
department | object | No | {id: departmentId} |
relationship | object | No | {id: relationshipId} |
nickName | string(30) | No | Nickname/alias |
school | string(50) | No | School/university |
marriedFlag | boolean | No | Marital status |
childrenFlag | boolean | No | Has children |
significantOther | string(30) | No | Spouse/partner name |
anniversary | date | No | Anniversary date |
birthDay | date | No | Birth date |
| Field | Type | Required | Description |
|---|---|---|---|
addressLine1 | string(50) | No | Street address |
addressLine2 | string(50) | No | Suite/unit |
city | string(50) | No | City |
state | string(50) | No | State/province |
zip | string(12) | No | Postal code |
country | object | No | {id: countryId} |
| Field | Type | Required | Description |
|---|---|---|---|
portalSecurityLevel | int | No | Portal access level (1-6) |
disablePortalLoginFlag | boolean | No | Disable portal access |
unsubscribeFlag | boolean | No | Opt out of emails |
| Field | Type | Required | Description |
|---|---|---|---|
inactiveFlag | boolean | No | Contact is inactive |
defaultMergeContactId | int | No | ID for merge operations |
managerContactId | int | No | Manager contact ID |
assistantContactId | int | No | Assistant contact ID |
_info | object | System | Metadata |
Communication items store contact methods (email, phone, fax, etc.) for a contact.
/company/contacts/{contactId}/communications| Field | Type | Required | Description |
|---|---|---|---|
id | int | System | Communication ID |
type | object | Yes | {id: communicationTypeId} - a tenant communication type (see below) |
value | string(250) | Yes | The email/phone/etc. value |
extension | string(15) | No | Phone extension |
defaultFlag | boolean | No | Is primary for this type |
communicationType | string | No | Category: Email, Phone, or Fax. Not the type name. |
Communication type ids and names are per tenant. Never assume {"id": 1} is Email. Your tenant may have "Email", "Work Email", "Direct", "Mobile", and so on, with different ids.
Each type is a row from GET /company/communicationTypes. These fields say what it is for:
| Field | Meaning |
|---|---|
description | The type name shown in ConnectWise (e.g. "Direct") |
emailFlag | Type is for email addresses |
phoneFlag | Type is for phone numbers |
faxFlag | Type is for fax numbers |
defaultFlag | Tenant default for its kind |
Call cw_list_communication_types (read-only) to see the types and their flags before building communication items or patching them. In the examples below, 12345 (email) and 12346 (phone) are placeholders: replace them with ids returned by cw_list_communication_types.
communicationType on an item is the category (Email, Phone, Fax). It is not the type name. A type named "Direct" still has communicationType: "Phone".
POST /company/contacts/{contactId}/communications
Content-Type: application/json
{
"type": {"id": 12345},
"value": "[email protected]",
"defaultFlag": true,
"communicationType": "Email"
}POST /company/contacts/{contactId}/communications
Content-Type: application/json
{
"type": {"id": 12346},
"value": "555-123-4567",
"extension": "101",
"defaultFlag": true,
"communicationType": "Phone"
}cw_list_communication_typesRead-only. Lists the tenant's communication types with their flags. No required params.
cw_create_contactCreates a contact, optionally with an email and phone. The MCP resolves the communication types itself from /company/communicationTypes and caches them per connection:
emailFlag=true; prefers defaultFlag, then the name "Email".phoneFlag=true; prefers the name "Direct", then defaultFlag.If no type resolves, it returns an error listing the available types. Override with optional params (an id wins over a name; an unknown name errors):
| Param | Description |
|---|---|
emailTypeId | Communication type id for the email |
emailTypeName | Communication type name for the email |
phoneTypeId | Communication type id for the phone |
phoneTypeName | Communication type name for the phone |
Example (override params only; add the usual contact fields as normal):
{
"emailTypeId": 12345,
"phoneTypeName": "Mobile"
}cw_update_contactWhen changing communication items, use type: { "id": <typeId> } with an id from cw_list_communication_types.
| Level | Name | Access |
|---|---|---|
| 1 | Admin | Full access to all company tickets/data |
| 2 | Manager | Access to department tickets |
| 3 | User | Access to own tickets only |
| 4 | Limited | View only |
| 5 | Read Only | Read only, no create |
| 6 | Restricted | Minimal access |
PATCH /company/contacts/{id}
Content-Type: application/json
{
"portalSecurityLevel": 2,
"disablePortalLoginFlag": false
}Portal passwords are managed through the ConnectWise portal. The API does not expose password fields.
To invite a contact to the portal:
portalSecurityLevel > 0disablePortalLoginFlag = falsePOST /company/contacts
Content-Type: application/json
{
"firstName": "John",
"lastName": "Smith",
"company": {"id": 12345},
"title": "IT Director",
"type": {"id": 4}
}POST /company/contacts
Content-Type: application/json
{
"firstName": "John",
"lastName": "Smith",
"company": {"id": 12345},
"title": "IT Director",
"type": {"id": 4},
"communicationItems": [
{
"type": {"id": 12345},
"value": "[email protected]",
"defaultFlag": true
},
{
"type": {"id": 12346},
"value": "555-123-4567",
"extension": "101",
"defaultFlag": true
}
]
}GET /company/contacts/{id}PATCH /company/contacts/{id}
Content-Type: application/json
{
"title": "CTO",
"type": {"id": 2}
}GET /company/contacts?conditions=company/id=12345 and inactiveFlag=falseDELETE /company/contacts/{id}Note: Contacts with related records (tickets, etc.) cannot be deleted. Mark as inactive instead.
All contacts for a company:
conditions=company/id=12345Active contacts only:
conditions=inactiveFlag=falseContacts by type:
conditions=type/id=2Contacts with portal access:
conditions=portalSecurityLevel>0 and disablePortalLoginFlag=falseSearch by name:
conditions=firstName contains "John" or lastName contains "Smith"Contacts by email:
conditions=communicationItems/value="[email protected]"Primary contacts only:
conditions=type/id=2 and inactiveFlag=false| Entity | Relationship |
|---|---|
| Company | /company/companies/{companyId} |
| Tickets | /service/tickets?conditions=contact/id={id} |
| Notes | /company/contacts/{id}/notes |
| Communications | /company/contacts/{id}/communications |
| Groups | /company/contacts/{id}/groups |
GET /company/contacts/{id}/notes
POST /company/contacts/{id}/notesNote Fields:
| Field | Type | Description |
|---|---|---|
text | string | Note content |
type | object | {id: noteTypeId} |
flagged | boolean | Flagged for attention |
| Error | Cause | Resolution |
|---|---|---|
| Company required | Missing company reference | Include company: {id: x} |
| firstName required | Missing first name | Provide firstName field |
| Invalid company | Company doesn't exist | Verify company ID |
| Cannot delete | Has related records | Mark as inactive instead |
| Email exists | Duplicate email | Use existing contact |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.