HaloPSA Contracts — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited HaloPSA Contracts (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.
Contracts in HaloPSA define the service relationship with clients - what services you provide, how you bill for them, and what service levels apply. Contracts control how time, expenses, and recurring charges flow to invoices and are critical for MSP financial management.
| Type | Description | Billing Method |
|---|---|---|
| Recurring | Monthly/annual managed services | Fixed recurring fee |
| Prepaid Hours | Block hours/time bank | Deduct from balance |
| Ad-Hoc | Pay as you go (T&M) | Bill actual time |
| Project | Fixed-price project | Milestone billing |
| Warranty | Coverage period | No direct billing |
| Field | Type | Required | Description |
|---|---|---|---|
id | int | System | Unique identifier |
ref | string | Yes | Contract reference/name |
client_id | int | Yes | Associated client |
startdate | date | Yes | Contract start |
enddate | date | No | Contract end |
status | string | Yes | Contract status |
type | string | Yes | Contract type |
| Field | Type | Required | Description |
|---|---|---|---|
billingfrequency | string | No | Monthly, Quarterly, Annual |
invoiceday | int | No | Day of month to invoice |
taxcode | string | No | Tax code for invoicing |
currency_code | string | No | Billing currency |
poref | string | No | Purchase order reference |
| Field | Type | Required | Description |
|---|---|---|---|
sla_id | int | No | Associated SLA |
priority_id | int | No | Default ticket priority |
includesallsites | bool | No | Covers all client sites |
includesallassets | bool | No | Covers all assets |
| Field | Type | Required | Description |
|---|---|---|---|
value | decimal | No | Contract value |
setupfee | decimal | No | One-time setup fee |
renewalvalue | decimal | No | Renewal amount |
marginpercent | decimal | No | Target margin |
| Status | Description | Billing |
|---|---|---|
| Active | In effect | Billable |
| Pending | Not yet started | Not billable |
| Expired | Past end date | Not billable |
| Cancelled | Terminated early | Not billable |
| On Hold | Temporarily paused | Not billable |
POST /api/ClientContract
Authorization: Bearer {token}
Content-Type: application/json[
{
"ref": "Acme Corp - Managed Services 2024",
"client_id": 123,
"type": "Recurring",
"status": "Active",
"startdate": "2024-01-01",
"enddate": "2024-12-31",
"billingfrequency": "Monthly",
"invoiceday": 1,
"value": 2500.00,
"sla_id": 1,
"includesallsites": true,
"includesallassets": true,
"notes": "Premium support tier, includes unlimited remote support"
}
]{
"contracts": [
{
"id": 5001,
"ref": "Acme Corp - Managed Services 2024",
"client_id": 123,
"client_name": "Acme Corporation",
"status": "Active",
"startdate": "2024-01-01",
"enddate": "2024-12-31"
}
]
}By client:
GET /api/ClientContract?client_id=123Active contracts:
GET /api/ClientContract?status=ActiveExpiring soon:
GET /api/ClientContract?enddate_before=2024-03-31&enddate_after=2024-01-01&status=ActiveBy type:
GET /api/ClientContract?type=RecurringGET /api/ClientContract/5001With recurring items:
GET /api/ClientContract/5001?includerecurringinvoiceitems=truePOST /api/ClientContract
Authorization: Bearer {token}
Content-Type: application/json[
{
"id": 5001,
"status": "On Hold",
"notes": "Client requested temporary pause - resume Feb 2024"
}
]Recurring items are the line items that generate recurring invoices.
| Field | Type | Required | Description |
|---|---|---|---|
id | int | System | Unique identifier |
contract_id | int | Yes | Parent contract |
description | string | Yes | Line item description |
quantity | decimal | Yes | Quantity |
unitprice | decimal | Yes | Price per unit |
billingfrequency | string | No | Override contract frequency |
startdate | date | No | Item start date |
enddate | date | No | Item end date |
POST /api/RecurringInvoiceItem
Authorization: Bearer {token}
Content-Type: application/json[
{
"contract_id": 5001,
"description": "Managed Workstation Support",
"quantity": 25,
"unitprice": 50.00,
"startdate": "2024-01-01"
},
{
"contract_id": 5001,
"description": "Server Management",
"quantity": 3,
"unitprice": 200.00,
"startdate": "2024-01-01"
},
{
"contract_id": 5001,
"description": "M365 Business Premium Licenses",
"quantity": 25,
"unitprice": 25.00,
"startdate": "2024-01-01"
}
][
{
"id": 10001,
"quantity": 30,
"notes": "Added 5 workstations in March"
}
]| Field | Type | Description |
|---|---|---|
prepaid_hours | decimal | Total hours purchased |
prepaid_hours_used | decimal | Hours consumed |
prepaid_hours_remaining | decimal | Available balance |
hourlyrate | decimal | Rate per hour |
[
{
"ref": "Acme Corp - Prepaid Hours Q1 2024",
"client_id": 123,
"type": "Prepaid Hours",
"status": "Active",
"startdate": "2024-01-01",
"enddate": "2024-03-31",
"prepaid_hours": 40,
"hourlyrate": 150.00,
"value": 6000.00
}
]GET /api/ClientContract/5002?includehoursummary=trueResponse includes:
{
"prepaid_hours": 40,
"prepaid_hours_used": 12.5,
"prepaid_hours_remaining": 27.5
}Time entries against tickets linked to prepaid contracts automatically deduct hours:
{
"ticket_id": 54321,
"timetaken": 60,
"contract_id": 5002
}Link contracts to Service Level Agreements:
[
{
"id": 5001,
"sla_id": 1
}
]Tickets under contract inherit SLA settings:
GET /api/ClientContract?enddate_before=2024-03-31&status=Active async function reviewContractPerformance(contractId) {
const contract = await getContract(contractId);
const tickets = await getContractTickets(contractId);
return {
totalTickets: tickets.length,
slaCompliance: calculateSLACompliance(tickets),
hoursUsed: calculateHoursUsed(tickets),
profitability: calculateProfitability(contract, tickets)
};
} [
{
"ref": "Acme Corp - Managed Services 2025",
"client_id": 123,
"type": "Recurring",
"status": "Pending",
"startdate": "2025-01-01",
"enddate": "2025-12-31",
"renewalvalue": 2750.00,
"notes": "Renewal from contract 5001"
}
] [{ "id": 5001, "status": "Expired" }]async function checkPrepaidBalance(contractId) {
const contract = await getContract(contractId);
const threshold = 10; // hours
if (contract.prepaid_hours_remaining <= threshold) {
return {
alert: true,
message: `Only ${contract.prepaid_hours_remaining} hours remaining`,
suggestedAction: 'Create replenishment quote'
};
}
return { alert: false };
}async function reconcileContractBilling(contractId, period) {
const contract = await getContract(contractId);
const invoices = await getContractInvoices(contractId, period);
const timeEntries = await getContractTime(contractId, period);
const expectedRecurring = calculateRecurringTotal(contract);
const actualBilled = invoices.reduce((sum, i) => sum + i.total, 0);
const unbilledTime = timeEntries.filter(t => !t.invoiced);
return {
contract_id: contractId,
period,
expected_recurring: expectedRecurring,
actual_billed: actualBilled,
unbilled_time_entries: unbilledTime.length,
unbilled_amount: unbilledTime.reduce((sum, t) => sum + (t.amount || 0), 0)
};
}| Code | Message | Resolution |
|---|---|---|
| 400 | ref required | Contract needs a reference name |
| 400 | client_id required | Must associate with a client |
| 400 | Invalid type | Use valid contract type |
| 400 | enddate before startdate | Fix date sequence |
| 404 | Contract not found | Verify contract ID |
| 409 | Cannot delete - has invoices | Cancel instead of delete |
function validateContract(contract) {
const errors = [];
if (!contract.ref || contract.ref.trim() === '') {
errors.push('Contract reference is required');
}
if (!contract.client_id) {
errors.push('Client ID is required');
}
if (!contract.startdate) {
errors.push('Start date is required');
}
if (contract.enddate && contract.startdate > contract.enddate) {
errors.push('End date must be after start date');
}
if (contract.type === 'Prepaid Hours' && !contract.prepaid_hours) {
errors.push('Prepaid hours contracts require hours allocation');
}
return {
isValid: errors.length === 0,
errors
};
}GET /api/ClientContract?groupby=status&count=trueasync function getExpiringContracts(days = 90) {
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + days);
const contracts = await searchContracts({
status: 'Active',
enddate_before: futureDate.toISOString().split('T')[0]
});
return contracts.map(c => ({
id: c.id,
ref: c.ref,
client_name: c.client_name,
enddate: c.enddate,
value: c.value,
days_remaining: Math.ceil(
(new Date(c.enddate) - new Date()) / (1000 * 60 * 60 * 24)
)
}));
}async function getContractValueByClient() {
const clients = await fetchAllClients();
const results = [];
for (const client of clients) {
const contracts = await getClientContracts(client.id, { status: 'Active' });
const totalValue = contracts.reduce((sum, c) => sum + (c.value || 0), 0);
results.push({
client_id: client.id,
client_name: client.name,
active_contracts: contracts.length,
annual_value: totalValue
});
}
return results.sort((a, b) => b.annual_value - a.annual_value);
}~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.