QuickBooks Online Customers — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited QuickBooks Online Customers (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.
Customers are the foundational entity in QuickBooks Online for MSP billing workflows. Each managed services client maps to a QBO Customer record. Customers hold billing addresses, payment terms, outstanding balances, and serve as the parent reference for invoices, payments, and estimates. MSPs commonly use sub-customers to break down billing by service line (e.g., "Acme Corp:Managed Services", "Acme Corp:Project Work").
Contracts, contacts, service history, and SLAs live in the PSA; use autotask-crm, connectwise-psa-companies, or halopsa-clients. A QBO Customer and a PSA company share a name and nothing else — the match is by name or a stored cross-reference, never by ID.
Contact; use xero-contacts.
qbo-expenses.
hubspot-companies.
hudu-companies or the relevant RMM's client skill.
mandates, and hosted checkout live on the collection platform, not in the ledger; use alternative-payments-customers.
QuickBooks Online supports a parent/sub-customer hierarchy for organizing billing:
Parent Customer: Acme Corporation
+-- Sub-Customer: Acme Corp:Managed Services
+-- Sub-Customer: Acme Corp:Project Work
+-- Sub-Customer: Acme Corp:HardwareSub-customers allow MSPs to track revenue and outstanding balances per service line while rolling up to a single client.
In QBO, "Jobs" are implemented as sub-customers. A project or engagement for a client is represented as a sub-customer under the parent.
Payment terms control when invoices are due:
| Term | Description | Common MSP Usage |
|---|---|---|
| Due on receipt | Due immediately | Break-fix work |
| Net 15 | Due in 15 days | Small clients |
| Net 30 | Due in 30 days | Standard managed services |
| Net 45 | Due in 45 days | Enterprise clients |
| Net 60 | Due in 60 days | Government/education |
QBO automatically tracks the customer balance (sum of all unpaid invoices minus unapplied payments). This is critical for MSP accounts receivable management.
DisplayName is the only required field on create and must be unique across the company file. Balance and BalanceWithJobs are read-only — the latter rolls up sub-customers. Sub-customers are defined by ParentRef.value plus Job: true, and FullyQualifiedName holds the colon-delimited path. SyncToken is required on every update.
See references/fields.md for the complete field reference.
GET /v3/company/{realmId}/query?query=SELECT * FROM Customer WHERE DisplayName LIKE '%Acme%'&minorversion=73
Authorization: Bearer {access_token}
Accept: application/jsoncurl example:
curl -s -H "Authorization: Bearer $QBO_ACCESS_TOKEN" \
-H "Accept: application/json" \
"https://quickbooks.api.intuit.com/v3/company/$QBO_REALM_ID/query?query=SELECT%20*%20FROM%20Customer%20WHERE%20DisplayName%20LIKE%20'%25Acme%25'&minorversion=73"Common Queries:
-- All active customers
SELECT * FROM Customer WHERE Active = true ORDERBY DisplayName
-- Customers with outstanding balance
SELECT * FROM Customer WHERE Balance > '0' ORDERBY Balance DESC
-- Find by company name
SELECT * FROM Customer WHERE CompanyName LIKE '%Tech%'
-- Find by email
SELECT * FROM Customer WHERE PrimaryEmailAddr = '[email protected]'
-- Count all customers
SELECT COUNT(*) FROM CustomerGET /v3/company/{realmId}/customer/123?minorversion=73
Authorization: Bearer {access_token}curl -s -H "Authorization: Bearer $QBO_ACCESS_TOKEN" \
-H "Accept: application/json" \
"https://quickbooks.api.intuit.com/v3/company/$QBO_REALM_ID/customer/123?minorversion=73"POST /v3/company/{realmId}/customer?minorversion=73
Content-Type: application/json
Authorization: Bearer {access_token}{
"DisplayName": "Acme Corporation",
"CompanyName": "Acme Corporation",
"GivenName": "John",
"FamilyName": "Smith",
"PrimaryPhone": {
"FreeFormNumber": "555-123-4567"
},
"PrimaryEmailAddr": {
"Address": "[email protected]"
},
"BillAddr": {
"Line1": "123 Main Street",
"City": "Springfield",
"CountrySubDivisionCode": "IL",
"PostalCode": "62704"
},
"SalesTermRef": {
"value": "3"
},
"PreferredDeliveryMethod": "Email",
"Notes": "MSP managed services client. Contract: 36-month. Primary contact: John Smith."
}curl -s -X POST \
-H "Authorization: Bearer $QBO_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
"https://quickbooks.api.intuit.com/v3/company/$QBO_REALM_ID/customer?minorversion=73" \
-d '{
"DisplayName": "Acme Corporation",
"CompanyName": "Acme Corporation",
"PrimaryEmailAddr": { "Address": "[email protected]" },
"SalesTermRef": { "value": "3" }
}'POST /v3/company/{realmId}/customer?minorversion=73
Content-Type: application/json
Authorization: Bearer {access_token}{
"Id": "123",
"SyncToken": "2",
"sparse": true,
"PrimaryPhone": {
"FreeFormNumber": "555-999-8888"
},
"Notes": "Updated billing contact: Jane Doe (555-999-8888)"
}{
"Id": "123",
"SyncToken": "2",
"sparse": true,
"Active": false
}{
"DisplayName": "Acme Corp:Managed Services",
"CompanyName": "Acme Corporation",
"ParentRef": {
"value": "123"
},
"Job": true,
"BillWithParent": true
}See references/examples.md for the full onboarding implementation.
async function getClientBalances() {
const query = `SELECT Id, DisplayName, Balance, BalanceWithJobs
FROM Customer
WHERE Active = true AND Balance > '0'
ORDERBY Balance DESC`;
const response = await qboQuery(query);
const customers = response.QueryResponse.Customer || [];
return customers.map(c => ({
id: c.Id,
name: c.DisplayName,
balance: c.Balance,
balanceWithJobs: c.BalanceWithJobs
}));
}async function offboardClient(customerId) {
// Get current customer with SyncToken
const customer = await getCustomer(customerId);
// Verify no outstanding balance
if (customer.Balance > 0) {
throw new Error(`Cannot offboard: outstanding balance of $${customer.Balance}`);
}
// Deactivate all sub-customers
const subs = await qboQuery(
`SELECT * FROM Customer WHERE ParentRef = '${customerId}'`
);
for (const sub of subs.QueryResponse.Customer || []) {
await updateCustomer({
Id: sub.Id,
SyncToken: sub.SyncToken,
sparse: true,
Active: false
});
}
// Deactivate parent
await updateCustomer({
Id: customer.Id,
SyncToken: customer.SyncToken,
sparse: true,
Active: false,
Notes: `${customer.Notes || ''}\nOffboarded: ${new Date().toISOString().split('T')[0]}`
});
}async function findCustomerByPsaId(psaId) {
// Search in Notes field for PSA ID reference
const allCustomers = await queryAll('Customer', "Active = true");
return allCustomers.find(c =>
c.Notes && c.Notes.includes(`PSA ID: ${psaId}`)
);
}uniqueness on DisplayName across Customers, Vendors, and Employees, so the recovery path is to look up the existing record rather than retry with a variant name.
Active: false via a sparseupdate). Deactivating a parent does not cascade to sub-customers — deactivate each sub-customer explicitly.
sparse: true with the current SyncToken for partial updates.
in Notes, which is not queryable with LIKE — fetch and filter client-side.
See references/errors.md for the full error-code and validation tables plus a recovery pattern.
sparse: true to avoid overwriting dataSee references/api.md for the complete endpoint reference.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.