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 halopsaThis skill uses the workspace's default tool permissions.
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.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Compresses source documents into lossless, LLM-optimized distillates preserving all facts and relationships. Use for 'distill documents' or 'create distillate' requests.
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;
}