QuickBooks Online Customer Management
Overview
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").
Anti-triggers
- The client record of record — a QBO Customer is a billing entity.
Contracts, contacts, service history, and SLAs live in the PSA; use
autotask-crm,connectwise-psa-companies, orhalopsa-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. - The books are in Xero, not QuickBooks — Xero calls the same thing a
Contact; use
xero-contacts. - A supplier — vendors are a separate entity here; use
qbo-expenses. - A sales prospect — pre-sale relationships live in the CRM; use
hubspot-companies. - The client's documentation or monitoring record — use
hudu-companiesor the relevant RMM's client skill. - The customer as the payment rail holds them — payment methods,
mandates, and hosted checkout live on the collection platform, not in
the ledger; use
alternative-payments-customers.
Key Concepts
Customer Hierarchy
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:Hardware
Sub-customers allow MSPs to track revenue and outstanding balances per service line while rolling up to a single client.
Customer vs Job
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
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 |
Balance Tracking
QBO automatically tracks the customer balance (sum of all unpaid invoices minus unapplied payments). This is critical for MSP accounts receivable management.
Key Fields
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.
API Patterns
Query Customers
GET /v3/company/{realmId}/query?query=SELECT * FROM Customer WHERE DisplayName LIKE '%Acme%'&minorversion=73
Authorization: Bearer {access_token}
Accept: application/json
curl 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 = 'billing@acmecorp.com'
-- Count all customers
SELECT COUNT(*) FROM Customer
Get Single Customer
GET /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"
Create Customer
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": "billing@acmecorp.com"
},
"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": "billing@acmecorp.com" },
"SalesTermRef": { "value": "3" }
}'
Update Customer (Sparse)
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)"
}
Deactivate Customer
{
"Id": "123",
"SyncToken": "2",
"sparse": true,
"Active": false
}
Create Sub-Customer
{
"DisplayName": "Acme Corp:Managed Services",
"CompanyName": "Acme Corporation",
"ParentRef": {
"value": "123"
},
"Job": true,
"BillWithParent": true
}
Common Workflows
New MSP Client Onboarding
- Create parent customer with billing info and payment terms
- Create sub-customers for each service line (managed services, projects, hardware)
- Set payment terms based on contract (typically Net 30)
- Configure email delivery for automated invoice sending
- Link to PSA using Notes or custom fields for cross-reference
See references/examples.md for the full onboarding implementation.
Customer Balance Review
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
}));
}
Client Offboarding
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]}`
});
}
PSA Cross-Reference Lookup
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}`)
);
}
Error Handling
- 6240 Duplicate Name is the most common create failure. QBO enforces
uniqueness on
DisplayNameacross Customers, Vendors, and Employees, so the recovery path is to look up the existing record rather than retry with a variant name. - Customers cannot be deleted, only deactivated (
Active: falsevia a sparse update). Deactivating a parent does not cascade to sub-customers — deactivate each sub-customer explicitly. - Non-sparse updates overwrite omitted fields with null. Always send
sparse: truewith the currentSyncTokenfor partial updates. - There is no custom field for PSA IDs by default, so cross-references live
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.
Best Practices
- Use DisplayName for uniqueness - QBO enforces unique DisplayNames; include a qualifier if needed
- Set CompanyName separately - CompanyName can differ from DisplayName and does not need to be unique
- Create sub-customers for service lines - Track revenue per service type
- Set payment terms at creation - Ensures invoices have correct due dates
- Use email delivery - Set PreferredDeliveryMethod to "Email" for automated invoice sending
- Include billing address - Required for mailed invoices and tax calculation
- Track PSA IDs in Notes - Cross-reference QBO customers with PSA records
- Deactivate, don't delete - Preserve transaction history by deactivating former clients
- Review balances regularly - Use balance queries to monitor aged receivables
- Use sparse updates - Only send changed fields with
sparse: trueto avoid overwriting data
See references/api.md for the complete endpoint reference.
Related Skills
- QBO Invoices - Invoice management for customers
- QBO Payments - Payment processing
- QBO Reports - A/R Aging and balance reports
- QBO API Patterns - API reference