QuickBooks Online Invoices — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited QuickBooks Online Invoices (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.
Invoices are the primary billing mechanism in QuickBooks Online. For MSPs, invoices typically represent monthly managed services fees, project work, hardware procurement, or ad-hoc support. QBO supports line items with service/product references, automatic tax calculation, email delivery, payment links, and integration with online payment processing. Invoices track due dates via payment terms and automatically contribute to the customer's outstanding balance.
system. Use xero-invoices. "Invoice", "customer", "line item", and "payment terms" mean the same thing in both, so the vendor name is the only signal; confirm it before constructing a call.
a different entity with different tools; use qbo-expenses. A distributor invoice from Pax8 or Sherweb is not in QBO at all until it is entered as a bill (pax8-invoices, sherweb-billing).
qbo-payments.Creating an invoice and applying cash to it are separate entities; the invoice is never edited to mark it paid.
originates in the PSA; use autotask-billing, connectwise-psa-time-entries, or halopsa-invoices.
payments platform; use alternative-payments-invoicing.
| Status | Description | Balance Impact |
|---|---|---|
| Draft | Not yet sent | Adds to balance |
| Sent | Emailed to customer | Adds to balance |
| Partially Paid | Some payment applied | Reduced balance |
| Paid | Fully paid | Zero balance |
| Voided | Cancelled | Removed from balance |
| Overdue | Past due date | Adds to balance (flagged) |
Each invoice contains one or more line items. Line items reference Items (products/services) from the QBO Items list:
| Line Type | Description | MSP Example |
|---|---|---|
SalesItemLineDetail | Standard product/service line | Monthly IT services |
GroupLineDetail | Grouped bundle of items | Managed services bundle |
DescriptionOnlyLine | Text-only description line | Section headers |
DiscountLineDetail | Discount applied | Multi-year contract discount |
SubTotalLineDetail | Running subtotal | Subtotal before tax |
| Type | Frequency | Description |
|---|---|---|
| Recurring Monthly | Monthly | Managed services, monitoring, backup |
| Project-Based | One-time | Network upgrade, migration, deployment |
| Time & Materials | As incurred | Break-fix support, consulting hours |
| Hardware | One-time | Equipment procurement and markup |
CustomerRef.value and Line are the only required fields on create. TotalAmt and Balance are read-only. SyncToken is required on every update, void, and delete. EmailStatus ("NotSet", "NeedToSend", "EmailSent") drives email queuing, and SalesTermRef.value drives automatic DueDate calculation.
See references/fields.md for the complete field reference.
GET /v3/company/{realmId}/query?query=SELECT * FROM Invoice WHERE CustomerRef = '123' AND Balance > '0'&minorversion=73
Authorization: Bearer {access_token}
Accept: application/jsonCommon Queries:
-- Unpaid invoices for a customer
SELECT * FROM Invoice WHERE CustomerRef = '123' AND Balance > '0'
-- All invoices in a date range
SELECT * FROM Invoice WHERE TxnDate >= '2026-01-01' AND TxnDate <= '2026-01-31' ORDERBY TxnDate DESC
-- Overdue invoices (past due date with balance)
SELECT * FROM Invoice WHERE DueDate < '2026-02-23' AND Balance > '0' ORDERBY DueDate ASC
-- Recent invoices
SELECT * FROM Invoice ORDERBY TxnDate DESC MAXRESULTS 25
-- Invoices by doc number
SELECT * FROM Invoice WHERE DocNumber = 'INV-1042'GET /v3/company/{realmId}/invoice/456?minorversion=73
Authorization: Bearer {access_token}POST /v3/company/{realmId}/invoice?minorversion=73
Content-Type: application/json
Authorization: Bearer {access_token}Minimal body — one line item, QBO assigns DocNumber and computes TotalAmt:
{
"CustomerRef": { "value": "123" },
"Line": [{
"Amount": 2500.00,
"Description": "Monthly Managed IT Services - February 2026",
"DetailType": "SalesItemLineDetail",
"SalesItemLineDetail": {
"ItemRef": { "value": "1" },
"Qty": 1,
"UnitPrice": 2500.00,
"ServiceDate": "2026-02-01"
}
}]
}See references/examples.md for the full multi-line managed-services request body and curl equivalents.
POST /v3/company/{realmId}/invoice/456/[email protected]&minorversion=73
Content-Type: application/octet-stream
Authorization: Bearer {access_token}{
"Id": "456",
"SyncToken": "3",
"sparse": true,
"CustomerMemo": {
"value": "Payment is now overdue. Please remit immediately."
}
}POST /v3/company/{realmId}/invoice?operation=void&minorversion=73
Content-Type: application/json
Authorization: Bearer {access_token}{
"Id": "456",
"SyncToken": "3"
}POST /v3/company/{realmId}/invoice?operation=delete&minorversion=73
Content-Type: application/json
Authorization: Bearer {access_token}{
"Id": "456",
"SyncToken": "3"
}GET /v3/company/{realmId}/invoice/456/pdf?minorversion=73
Authorization: Bearer {access_token}
Accept: application/pdfasync function generateMonthlyInvoices(billingMonth) {
// Fetch all active MSP clients
const customers = await qboQuery(
"SELECT * FROM Customer WHERE Active = true AND Balance >= '0'"
);
const clients = customers.QueryResponse.Customer || [];
const results = { created: [], errors: [] };
for (const client of clients) {
try {
// Build line items from client's service agreement
const lines = await buildServiceLines(client.Id, billingMonth);
if (lines.length === 0) continue;
const invoice = await createInvoice({
CustomerRef: { value: client.Id },
TxnDate: billingMonth + '-01',
BillEmail: { Address: client.PrimaryEmailAddr?.Address },
EmailStatus: 'NeedToSend',
Line: lines,
CustomerMemo: {
value: `Services for ${billingMonth}. Thank you for your business.`
}
});
results.created.push({
customer: client.DisplayName,
invoiceId: invoice.Id,
total: invoice.TotalAmt
});
} catch (error) {
results.errors.push({
customer: client.DisplayName,
error: error.message
});
}
}
return results;
}async function createProjectInvoice(customerId, projectDetails) {
const lines = projectDetails.tasks.map(task => ({
Amount: task.hours * task.rate,
Description: `${task.description}\n${task.hours} hours @ $${task.rate}/hr`,
DetailType: 'SalesItemLineDetail',
SalesItemLineDetail: {
ItemRef: { value: task.itemId },
Qty: task.hours,
UnitPrice: task.rate,
ServiceDate: task.date
}
}));
return await createInvoice({
CustomerRef: { value: customerId },
Line: lines,
CustomerMemo: {
value: `Project: ${projectDetails.name}\nWork completed ${projectDetails.startDate} - ${projectDetails.endDate}`
},
PrivateNote: `PSA Ticket: ${projectDetails.ticketNumber}`
});
}async function sendUnsentInvoices() {
const unsent = await qboQuery(
"SELECT * FROM Invoice WHERE EmailStatus = 'NeedToSend' AND Balance > '0'"
);
const invoices = unsent.QueryResponse.Invoice || [];
for (const invoice of invoices) {
const email = invoice.BillEmail?.Address;
if (email) {
await sendInvoice(invoice.Id, email);
}
}
return { sent: invoices.length };
}async function getOverdueInvoices() {
const today = new Date().toISOString().split('T')[0];
const result = await qboQuery(
`SELECT * FROM Invoice WHERE DueDate < '${today}' AND Balance > '0' ORDERBY DueDate ASC`
);
const invoices = result.QueryResponse.Invoice || [];
return invoices.map(inv => ({
invoiceNumber: inv.DocNumber,
customer: inv.CustomerRef.name,
amount: inv.TotalAmt,
balance: inv.Balance,
dueDate: inv.DueDate,
daysOverdue: Math.floor((Date.now() - new Date(inv.DueDate)) / 86400000)
}));
}than recalculating it.
DocNumber from the payload and let QBOauto-assign rather than guessing the next number.
SyncToken is per-record and increments on every write;re-fetch the invoice and retry with the fresh token.
ItemRef orCustomerRef, not a bad invoice ID — the message does not say which.
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.