QuickBooks Online Expenses — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited QuickBooks Online Expenses (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.
Expenses in QuickBooks Online are tracked through two primary entities: Purchase (for direct expenses like checks, credit card charges, and cash payments) and Bill (for accounts payable -- vendor invoices you owe). For MSPs, expense tracking is critical for per-client profitability analysis: tracking software licenses, hardware costs, subcontractor fees, and third-party service costs against the revenue each client generates.
cash receipts are qbo-invoices and qbo-payments. "Bill" is the trap: a QBO Bill is something the MSP owes, while a PSA "bill" is usually something a client owes.
xero-accounts andxero-invoices.
line-level cost of cloud licences before it is entered as a bill here. Use pax8-invoices or sherweb-billing.
time originates in the PSA (autotask-time-entries, connectwise-psa-time-entries) and lands here only as payroll.
these records, not a query against them; use qbo-reports.
The Purchase entity covers direct expenses paid immediately or via credit card:
| PaymentType | Description | MSP Example |
|---|---|---|
Cash | Cash/bank payment | Petty cash for supplies |
Check | Check payment | Vendor payment by check |
CreditCard | Credit card charge | Software subscription charge |
| Entity | When to Use | Payment Timing |
|---|---|---|
| Purchase | Expense already paid | Immediate (check, cash, credit card) |
| Bill | Vendor invoice received | Deferred (pay later via BillPayment) |
| Category | Description | Examples |
|---|---|---|
| Software Licenses | Per-seat or per-client licenses | Microsoft 365, antivirus, RMM seats |
| Hardware | Equipment for clients | Servers, firewalls, workstations |
| Subcontractors | Outsourced labor | Cabling, specialized consulting |
| Cloud Services | Hosted services | Azure, AWS, backup storage |
| Telecom | Communication services | Internet, VoIP, SIP trunks |
| Training | Staff certifications | Vendor certs, training courses |
QBO supports assigning expenses to customers, enabling per-client profitability analysis. Use the CustomerRef field on line items to allocate costs to specific MSP clients.
A Purchase requires PaymentType, AccountRef.value (the bank or credit card account it was paid from), and at least one Line. A Bill requires VendorRef.value and Line. Per-client allocation lives on the line detail, not the header: AccountBasedExpenseLineDetail.CustomerRef.value plus BillableStatus ("Billable", "NotBillable", "HasBeenBilled"). TotalAmt and Balance are read-only; SyncToken is required on updates.
See references/fields.md for the complete Purchase, Bill, and Vendor field reference.
GET /v3/company/{realmId}/query?query=SELECT * FROM Purchase WHERE PaymentType = 'CreditCard' AND TxnDate >= '2026-01-01'&minorversion=73
Authorization: Bearer {access_token}
Accept: application/jsonCommon Queries:
-- All credit card expenses in a date range
SELECT * FROM Purchase WHERE PaymentType = 'CreditCard' AND TxnDate >= '2026-01-01' AND TxnDate <= '2026-01-31'
-- Expenses for a specific vendor
SELECT * FROM Purchase WHERE EntityRef = '42'
-- All expenses in a month
SELECT * FROM Purchase WHERE TxnDate >= '2026-02-01' AND TxnDate <= '2026-02-28' ORDERBY TxnDate DESC
-- All bills (accounts payable)
SELECT * FROM Bill WHERE Balance > '0' ORDERBY DueDate ASC
-- Bills for a specific vendor
SELECT * FROM Bill WHERE VendorRef = '42' AND Balance > '0'
-- All vendors
SELECT * FROM Vendor WHERE Active = true ORDERBY DisplayNamePOST /v3/company/{realmId}/purchase?minorversion=73
Content-Type: application/json
Authorization: Bearer {access_token}Software license expense allocated to a client — note AccountRef on the header is the funding account (credit card), while AccountRef inside the line detail is the expense account:
{
"PaymentType": "CreditCard",
"AccountRef": { "value": "41" },
"EntityRef": { "value": "42", "type": "Vendor" },
"TxnDate": "2026-02-15",
"Line": [{
"Amount": 450.00,
"Description": "Microsoft 365 Business Premium - 30 seats - Acme Corp - February 2026",
"DetailType": "AccountBasedExpenseLineDetail",
"AccountBasedExpenseLineDetail": {
"AccountRef": { "value": "60" },
"CustomerRef": { "value": "123" },
"BillableStatus": "Billable"
}
}],
"PrivateNote": "Monthly software licenses for Acme Corp"
}See references/examples.md for the multi-line request body, the curl equivalent, and a Create Vendor payload.
{
"VendorRef": {
"value": "42"
},
"TxnDate": "2026-02-10",
"DueDate": "2026-03-12",
"Line": [
{
"Amount": 2400.00,
"Description": "Cabling project - Acme Corp new office build-out",
"DetailType": "AccountBasedExpenseLineDetail",
"AccountBasedExpenseLineDetail": {
"AccountRef": { "value": "62" },
"CustomerRef": { "value": "123" },
"BillableStatus": "Billable"
}
}
],
"PrivateNote": "Subcontractor cabling for Acme Corp office expansion"
}{
"VendorRef": {
"value": "42"
},
"PayType": "Check",
"CheckPayment": {
"BankAccountRef": { "value": "35" }
},
"TotalAmt": 2400.00,
"Line": [
{
"Amount": 2400.00,
"LinkedTxn": [
{
"TxnId": "789",
"TxnType": "Bill"
}
]
}
]
}async function getClientExpenses(customerId, startDate, endDate) {
// Get all purchases with line items allocated to this customer
const purchases = await queryAll('Purchase',
`TxnDate >= '${startDate}' AND TxnDate <= '${endDate}'`
);
const clientExpenses = [];
for (const purchase of purchases) {
for (const line of purchase.Line || []) {
const detail = line.AccountBasedExpenseLineDetail || line.ItemBasedExpenseLineDetail;
if (detail?.CustomerRef?.value === customerId) {
clientExpenses.push({
date: purchase.TxnDate,
vendor: purchase.EntityRef?.name || 'Unknown',
description: line.Description,
amount: line.Amount,
billable: detail.BillableStatus === 'Billable',
category: detail.AccountRef?.name
});
}
}
}
return {
customer: customerId,
period: `${startDate} to ${endDate}`,
expenses: clientExpenses,
total: clientExpenses.reduce((sum, e) => sum + e.Amount, 0)
};
}async function recordMonthlyLicenses(month, licenseData) {
const results = [];
for (const license of licenseData) {
const purchase = await createPurchase({
PaymentType: 'CreditCard',
AccountRef: { value: license.creditCardAccountId },
EntityRef: { value: license.vendorId, type: 'Vendor' },
TxnDate: `${month}-15`,
Line: [{
Amount: license.seats * license.perSeatCost,
Description: `${license.productName} - ${license.seats} seats - ${license.customerName} - ${month}`,
DetailType: 'AccountBasedExpenseLineDetail',
AccountBasedExpenseLineDetail: {
AccountRef: { value: license.expenseAccountId },
CustomerRef: { value: license.customerId },
BillableStatus: license.billable ? 'Billable' : 'NotBillable'
}
}],
PrivateNote: `Auto-recorded: ${license.productName} for ${license.customerName}`
});
results.push({
customer: license.customerName,
product: license.productName,
amount: license.seats * license.perSeatCost,
purchaseId: purchase.Id
});
}
return results;
}async function clientProfitability(customerId, startDate, endDate) {
// Revenue: sum of invoice line items for this customer
const invoices = await qboQuery(
`SELECT * FROM Invoice WHERE CustomerRef = '${customerId}' AND TxnDate >= '${startDate}' AND TxnDate <= '${endDate}'`
);
const revenue = (invoices.QueryResponse.Invoice || [])
.reduce((sum, inv) => sum + inv.TotalAmt, 0);
// Costs: sum of expense line items allocated to this customer
const expenses = await getClientExpenses(customerId, startDate, endDate);
return {
customerId,
period: `${startDate} to ${endDate}`,
revenue,
costs: expenses.total,
profit: revenue - expenses.total,
margin: revenue > 0 ? ((revenue - expenses.total) / revenue * 100).toFixed(1) + '%' : 'N/A'
};
}async function getOutstandingBills() {
const result = await qboQuery(
"SELECT * FROM Bill WHERE Balance > '0' ORDERBY DueDate ASC"
);
const bills = result.QueryResponse.Bill || [];
const today = new Date().toISOString().split('T')[0];
return bills.map(bill => ({
vendor: bill.VendorRef.name,
amount: bill.TotalAmt,
balance: bill.Balance,
dueDate: bill.DueDate,
overdue: bill.DueDate < today
}));
}header-level CustomerRef on Purchase or Bill — a report that reads only the header will show zero client cost.
AccountRef/EntityRef; the fault Detail names the field, the message does not.
account leaves existing transactions intact but rejects new ones referencing it.
DisplayName, which shares anamespace with Customer and Employee names in QBO.
See references/errors.md for the full error-code and validation tables plus a recovery pattern.
See references/api.md for the complete endpoint reference.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.