HaloPSA Clients — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited HaloPSA Clients (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.
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": "[email protected]",
"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": "[email protected]",
"phonenumber": "555-123-4567",
"inactive": false
}
]
}Search by name:
GET /api/Client?search=acmeActive clients only:
GET /api/Client?inactive=falseBy account manager:
GET /api/Client?accountmanager_id=101Paginated with sorting:
GET /api/Client?page_no=1&page_size=50&order=name&orderdesc=falseGET /api/Client/123With additional details:
GET /api/Client/123?includesites=true&includeusers=truePOST /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": "[email protected]",
"phonenumber": "555-123-4568",
"mobilenumber": "555-987-6543",
"jobtitle": "IT Director",
"isimportantcontact": true
}
]Contacts for a client:
GET /api/Users?client_id=123Search by email:
GET /api/[email protected]Active contacts only:
GET /api/Users?inactive=false&client_id=123HaloPSA supports parent-child client relationships:
[
{
"id": 124,
"name": "Acme West Branch",
"client_to_invoice": 123,
"toplevel_id": 123
}
]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:
[{ "id": 123, "inactive": true }]| 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=falseGET /api/Users?emailaddress=null&inactive=falseasync 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;
}~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.