Help us improve
Share bugs, ideas, or general feedback.
From halopsa
Manages HaloPSA clients: create, update, search customer relationships; handle fields, sites, locations, contacts, types, and onboarding workflows for MSP CRM.
npx claudepluginhub wyre-technology/msp-claude-plugins --plugin halopsaHow this skill is triggered — by the user, by Claude, or both
Slash command
/halopsa:clientsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Clients (customers) are the foundation of HaloPSA. All tickets, contracts, assets, and invoices are associated with clients. Proper client data management is critical for accurate service delivery and billing.
Manages SuperOps.ai client accounts via GraphQL: create, update, search, delete; handle sites, contacts, custom fields, stages, and statuses. For MSP PSA workflows.
Manages ConnectWise Automate client organizations: create, read, update, delete. Covers locations, settings, groups, EDPs, identifiers, and hierarchy.
Manages Syncro MSP customer records: create, update, search; handle contacts, sites/locations via REST API. For MSP client onboarding and management.
Share bugs, ideas, or general feedback.
Clients (customers) are the foundation of HaloPSA. All tickets, contracts, assets, and invoices are associated with clients. Proper client data management is critical for accurate service delivery and billing.
The primary entity representing a customer organization.
| Field | Type | Required | Description |
|---|---|---|---|
id | int | System | Unique identifier |
name | string(255) | Yes | Official company name |
client_to_invoice | int | No | Parent company for billing |
toplevel_id | int | No | Top-level parent in hierarchy |
inactive | bool | No | Active/inactive status |
main_site_id | int | No | Primary site reference |
| Field | Type | Required | Description |
|---|---|---|---|
emailaddress | string | No | Primary email |
phonenumber | string | No | Main phone |
website | string | No | Website URL |
accountmanager_id | int | No | Assigned account manager |
| Field | Type | Required | Description |
|---|---|---|---|
colour | string | No | Client color code (hex) |
notes | text | No | Internal notes |
taxcode | string | No | Tax identifier |
currency_code | string | No | Billing currency |
payment_terms | int | No | Payment terms (days) |
HaloPSA supports client classification through custom fields and categories. Common patterns:
| Type | Description | Use Case |
|---|---|---|
| Customer | Active paying client | Full service |
| Prospect | Potential customer | Sales pipeline |
| Lead | Marketing qualified | Pre-sales |
| Partner | Strategic partner | Collaboration |
| Vendor | Supplier | Procurement |
Sites represent physical locations for a client.
| Field | Type | Required | Description |
|---|---|---|---|
id | int | System | Unique identifier |
client_id | int | Yes | Parent client |
name | string(255) | Yes | Site name |
line1 | string | No | Address line 1 |
line2 | string | No | Address line 2 |
line3 | string | No | Address line 3 |
line4 | string | No | City |
postcode | string | No | Postal/ZIP code |
country | string | No | Country |
phonenumber | string | No | Site phone |
main_site | bool | No | Is primary site |
inactive | bool | No | Active status |
Contacts (also called Users in HaloPSA) are individuals at a client organization.
| Field | Type | Required | Description |
|---|---|---|---|
id | int | System | Unique identifier |
client_id | int | Yes | Associated client |
site_id | int | No | Associated site |
name | string(255) | Yes | Full name |
firstname | string | No | First name |
surname | string | No | Last name |
emailaddress | string | No | Email address |
phonenumber | string | No | Direct phone |
mobilenumber | string | No | Mobile phone |
jobtitle | string | No | Job title |
inactive | bool | No | Active status |
isimportantcontact | bool | No | VIP flag |
POST /api/Client
Authorization: Bearer {token}
Content-Type: application/json
[
{
"name": "Acme Corporation",
"emailaddress": "info@acme.example.com",
"phonenumber": "555-123-4567",
"website": "https://acme.example.com",
"notes": "Enterprise client, premium support tier",
"accountmanager_id": 101
}
]
{
"clients": [
{
"id": 123,
"name": "Acme Corporation",
"emailaddress": "info@acme.example.com",
"phonenumber": "555-123-4567",
"inactive": false
}
]
}
Search by name:
GET /api/Client?search=acme
Active clients only:
GET /api/Client?inactive=false
By account manager:
GET /api/Client?accountmanager_id=101
Paginated with sorting:
GET /api/Client?page_no=1&page_size=50&order=name&orderdesc=false
GET /api/Client/123
With additional details:
GET /api/Client/123?includesites=true&includeusers=true
POST /api/Client
Authorization: Bearer {token}
Content-Type: application/json
[
{
"id": 123,
"phonenumber": "555-987-6543",
"website": "https://newsite.acme.example.com",
"accountmanager_id": 102
}
]
POST /api/Site
Authorization: Bearer {token}
Content-Type: application/json
[
{
"client_id": 123,
"name": "Acme HQ",
"line1": "123 Main Street",
"line2": "Suite 500",
"line4": "Springfield",
"postcode": "62701",
"country": "United States",
"phonenumber": "555-123-4567",
"main_site": true
}
]
POST /api/Users
Authorization: Bearer {token}
Content-Type: application/json
[
{
"client_id": 123,
"site_id": 456,
"name": "John Smith",
"firstname": "John",
"surname": "Smith",
"emailaddress": "john.smith@acme.example.com",
"phonenumber": "555-123-4568",
"mobilenumber": "555-987-6543",
"jobtitle": "IT Director",
"isimportantcontact": true
}
]
Contacts for a client:
GET /api/Users?client_id=123
Search by email:
GET /api/Users?search=john.smith@acme.example.com
Active contacts only:
GET /api/Users?inactive=false&client_id=123
HaloPSA supports parent-child client relationships:
[
{
"id": 124,
"name": "Acme West Branch",
"client_to_invoice": 123,
"toplevel_id": 123
}
]
Create client record
Create primary site
Create primary contact
Set up contract
Deploy assets (if applicable)
async function onboardContact(clientId, contactData) {
// 1. Check for existing contact by email
const existing = await searchUsers({
client_id: clientId,
search: contactData.emailaddress
});
if (existing.length > 0) {
console.log('Contact already exists:', existing[0].id);
return existing[0];
}
// 2. Create new contact
const contact = await createUser({
client_id: clientId,
...contactData
});
// 3. Send welcome email (if configured)
if (contactData.send_welcome) {
await sendWelcomeEmail(contact.id);
}
return contact;
}
When a client churns:
Mark client inactive
[{ "id": 123, "inactive": true }]
Close open tickets
End contracts
Update assets
| Code | Message | Resolution |
|---|---|---|
| 400 | Name is required | Client must have a name |
| 400 | Invalid client_id | Parent client doesn't exist |
| 400 | Duplicate email | Contact email already in use |
| 404 | Client not found | Verify client ID |
| 409 | Cannot delete - has related records | Deactivate instead |
function validateClient(client) {
const errors = [];
if (!client.name || client.name.trim() === '') {
errors.push('Client name is required');
}
if (client.emailaddress && !isValidEmail(client.emailaddress)) {
errors.push('Invalid email format');
}
if (client.website && !isValidUrl(client.website)) {
errors.push('Invalid website URL');
}
return {
isValid: errors.length === 0,
errors
};
}
GET /api/Client?hasusers=false&inactive=false
GET /api/Users?emailaddress=null&inactive=false
async function findDuplicateClients() {
const clients = await fetchAllClients();
const names = {};
const duplicates = [];
clients.forEach(client => {
const normalized = client.name.toLowerCase().trim();
if (names[normalized]) {
duplicates.push({
name: client.name,
ids: [names[normalized], client.id]
});
} else {
names[normalized] = client.id;
}
});
return duplicates;
}