kyslan

Kyslan / Library

HaloPSA

HaloPSA contracts, and where the money actually lives

Updated 27 August 2026 · 9 min read · By Kyslan, a Northbeams product

The short answer

In HaloPSA the recurring money is not on the contract record. The real per-cycle charge lives on the recurring invoice, joined to the contract by contract_id, and a contract with no live recurring invoice has no readable value at all. A recurring invoice created through the API comes back disabled until a schedule is set in the interface, which means a service can look like revenue while nothing is billing, or bill while showing zero. That single split causes most HaloPSA billing surprises.

Kyslan finds the unbilled work in your own PSA, then writes the change order that bills it. First report free, read-only. Claim it back →

Where the money actually lives

The intuitive place to look for a HaloPSA contract's value is the contract. That is not where it is, and reading it there produces numbers that are wrong in both directions.

The recurring charge lives on the recurring invoice, which joins to the contract by contract_id. The parent record's total is the charge per cycle. A contract with no live recurring invoice attached has no readable recurring value, whatever any field on the contract itself says.

Observed on a live tenant

The same contract carried a period charge amount of 2,400 while its recurring invoice was disabled, and zero once that invoice was active. Reading the contract field would have reported $4,800 a year of revenue for a service that was switched off, and nothing for the one that was running. The contract-level amount is not a source, a fallback, or a cross-check. It is noise.

There is a second trap one level down. Recurring invoice lines carry monthly and annual converted price fields, and on a live tenant those read zero on every line while the parent total is correct. Summing the lines gives a confident total of nothing.

The disabled recurring invoice trap

A recurring invoice created through the API arrives disabled. That state is not something you can set: it is derived from whether the invoice has a schedule, and the schedule can only be set in the Halo interface, under Invoices, Recurring Invoices, Set Schedule.

The operational consequence is the one that costs money. A contract can be signed, a recurring invoice can exist, everything can look complete on the contract screen, and no invoice ever goes out, because a schedule was never set. Nothing in the normal workflow raises a flag, because from every other angle the contract is live.

The check

  1. List every active contract.
  2. Join recurring invoices by contract_id.
  3. Flag any active contract with no recurring invoice, and any recurring invoice that is disabled.
  4. For each one, work out how many cycles have passed since the contract started.

That last step is the one that hurts. A contract live for seven months with no schedule set is seven cycles of revenue that never existed, and it is fully recoverable in the sense that the client agreed to pay it and simply was never asked.

Billing periods are codes, not words

A HaloPSA contract stores its billing period as a numeric code. On the tenants we have worked against, 2 is monthly and 3 is yearly, read from how the Client Contracts list renders those codes in the interface.

Two practical warnings. First, Halo's lookup endpoint does not contain cadence names, so the codes cannot be resolved from data alone: they have to be confirmed against your own tenant's interface. Second, the code set is per tenant. If you are pulling contract values into a report or a spreadsheet, confirm what each code means in your own instance before multiplying anything by twelve.

The five leaks to check

1. Active contracts with no live recurring invoice

The largest and most specific HaloPSA leak, for the reason above. Priced as the contract value times the number of cycles since it should have started billing.

2. Contract value read from the wrong place

Not a leak in itself but the cause of several. Any internal report, spreadsheet or dashboard built on the contract record's own amount is reporting numbers that do not match what is being invoiced, in both directions. Rebuild it on recurring invoice totals.

3. Time logged against expired contracts

List contracts whose end date has passed, then check for actions recorded against them afterwards. Halo does not block it.

4. Non-billable actions that should have billed

Group non-billable hours over 90 days by ticket type and by agent. As in every PSA, the pattern is usually a default set the wrong way on one type rather than a spread of individual decisions.

5. Contracts with no client seat basis

Halo does not hold a seat count on a client record, so a per-user contract has no in-system truth to reconcile against. The comparison has to come from outside, from the client's directory or licence portal, which means it is a check nobody runs unless somebody schedules it.

The time entry field that reads zero

Halo records work as actions, and an action carries the time taken. There are two fields, and they behave differently on a live tenant from how they behave in any test fixture:

FieldOn a live tenant
timetakenadjustedPresent and zero on real actions
timetakenHolds the real figure

Anything that prefers the adjusted field, or that falls back only when a value is null rather than when it is zero, reads every action as zero hours. The failure is total and silent: no error, no warning, just a report saying no billable time exists. If a HaloPSA time report has ever come back suspiciously empty, this is the first thing to check.

What HaloPSA will not tell you

None of this makes HaloPSA a poor system. It is a good one, and every PSA has a shape its reporting fits and a shape it does not. It does mean that a billing reconciliation in Halo is a join across contracts, recurring invoices, actions and invoices that no single screen performs, which is why the gaps persist in shops with careful people.

Kyslan connects to HaloPSA read-only and runs those joins for you, weekly, with each finding priced. First audit free, or see the sample report.

Common questions

Where is the contract value stored in HaloPSA?

On the recurring invoice, joined to the contract by contract_id, where the parent record total is the charge per cycle. The amount held on the contract record itself is unreliable: the same contract has been observed showing a value while its recurring invoice was disabled and showing zero once that invoice was active, so it is wrong in both directions and should not be used even as a cross-check.

Why is my HaloPSA recurring invoice disabled?

Because it has no schedule. The disabled state is derived rather than set directly, and a recurring invoice created through the API arrives disabled until a schedule is added in the interface under Invoices, Recurring Invoices, Set Schedule. Until then the contract looks live from every other screen while nothing is actually billing.

What do HaloPSA billing period codes mean?

They are numeric codes stored on the contract. On the tenants we have worked against, 2 is monthly and 3 is yearly, confirmed by how the Client Contracts list renders them in the interface. Halo's lookup data does not contain cadence names, so the codes cannot be resolved from the API alone and should be confirmed against your own instance before any figure is annualised.

Why does my HaloPSA time report show zero hours?

Most likely because it is reading timetakenadjusted rather than timetaken. On a live tenant the adjusted field is present and zero on real actions while timetaken holds the actual figure, so any report preferring the adjusted value, or falling back only when a value is null rather than zero, returns nothing at all with no error raised.

Can you list contract attachments in HaloPSA?

Not reliably. The attachment listing returns no rows for contracts with or without a filter, although fetching a known attachment by its id does serve the file. In practice that means scope wording held in an attached document is not discoverable programmatically, so scope checks have to work from the contract's own fields.

Keep reading

See your own number, then send the invoice for it

Kyslan reads 90 days of your tickets, time entries and contracts, finds the work you delivered and never billed, then writes the change order that puts it back on the invoice. Read only, nothing installed, first report free.

Claim it back

Free · read-only key · no card · HaloPSA, ConnectWise Manage, Autotask PSA or a CSV export