How to reconcile PSA time entries to invoices (ConnectWise, HaloPSA, QuickBooks, Xero)
Updated 2026-10-18. Written by the maker of MSP Billing Sentinel.
Unbilled work usually hides between two systems: the PSA, where time is logged, and the accounting system, where invoices are issued. Neither one shows the gap. This method finds it with two exports and a spreadsheet. It works the same for ConnectWise Manage or HaloPSA on the PSA side, and QuickBooks Online or Xero on the invoice side.
What you need
- Closed tickets for the last 90 days with ticket number, client, summary, technician, closed date, billable flag, hours and rate. Export from your PSA's reporting or ticket list; field names vary by version.
- Invoice lines for the same period plus 30 days with invoice number, customer, date, line description, quantity and amount. Export from QuickBooks or Xero with line descriptions included.
Step 1: keep only billable, out-of-scope work
Filter the tickets to billable ones. Then remove work covered by a flat-fee agreement, because that time is not billed per hour. What remains is the work someone should have invoiced.
Step 2: match by ticket number
If your invoice descriptions contain the ticket number (for example "Core switch reboot, #20401"), extract it with a pattern such as #(\d+) and look each ticket up. A ticket with a matching invoice line is billed. This is the cleanest test, and it is the best reason to put the ticket number on every invoice line.
Step 3: match what is left by client, date and hours
For tickets with no number on any invoice, look for an invoice line to the same client, dated shortly after the ticket closed, with about the same quantity of hours. A reasonable window is a week before to three weeks after the close date, and hours within a quarter hour. Treat these as likely matches that a person should confirm.
Step 4: review the unmatched list
Whatever still has no invoice is your candidate list of unbilled work. Multiply hours by the rate to get a dollar figure. Then check each one against the client's agreement before you bill: some of it will turn out to be covered, written off on purpose, or already billed in a bundle.
A worked example (sample data)
In Sentinel's synthetic benchmark, 142 tickets were compared with 19 invoices. Nine tickets, 21.5 hours in total, had no invoice. At $150 an hour that came to $3,225. These figures are made-up benchmark data, shown only to illustrate the method; your own result will differ.
How Sentinel does this
Sentinel runs steps 1 to 4 on your two CSV exports and lists each ticket it believes was not billed, with a dollar figure. It matches by ticket number first, then by client, close date and hours as described above. It only reads the files you upload and never writes to your PSA or accounting system. Matching can be wrong, so every finding needs your review.