From quickbooks-online
Manages QuickBooks Online customers for MSPs: create, search, update records; sub-customers, billing addresses, payment terms, balances, PSA cross-references.
npx claudepluginhub wyre-technology/msp-claude-plugins --plugin quickbooks-onlineThis skill uses the workspace's default tool permissions.
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", ...
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Reviews prose for communication issues impeding comprehension, outputs minimal fixes in a three-column table per Microsoft Writing Style Guide. Useful for 'review prose' or 'improve prose' requests.
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").
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.
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.
| Field | Type | Required | Description |
|---|---|---|---|
Id | string | System | Auto-generated unique identifier |
DisplayName | string | Yes | Unique display name (customer-facing) |
CompanyName | string | No | Legal company name |
GivenName | string | No | Contact first name |
FamilyName | string | No | Contact last name |
Active | boolean | No | Whether customer is active (default: true) |
Balance | decimal | Read-only | Outstanding balance |
BalanceWithJobs | decimal | Read-only | Balance including sub-customers |
SyncToken | string | Required for updates | Optimistic locking token |
| Field | Type | Description |
|---|---|---|
PrimaryPhone.FreeFormNumber | string | Primary phone number |
AlternatePhone.FreeFormNumber | string | Alternate phone |
Mobile.FreeFormNumber | string | Mobile phone |
Fax.FreeFormNumber | string | Fax number |
PrimaryEmailAddr.Address | string | Primary email (used for invoice delivery) |
WebAddr.URI | string | Website URL |
| Field | Type | Description |
|---|---|---|
BillAddr.Line1 | string | Billing street address |
BillAddr.City | string | Billing city |
BillAddr.CountrySubDivisionCode | string | Billing state/province |
BillAddr.PostalCode | string | Billing postal code |
BillAddr.Country | string | Billing country |
ShipAddr | object | Shipping address (same structure as BillAddr) |
| Field | Type | Description |
|---|---|---|
SalesTermRef.value | string | Payment terms ID (e.g., Net 30) |
PaymentMethodRef.value | string | Default payment method ID |
CurrencyRef.value | string | Currency code (e.g., "USD") |
PreferredDeliveryMethod | string | "Print", "Email", or "None" |
Taxable | boolean | Whether customer is taxable |
| Field | Type | Description |
|---|---|---|
ParentRef.value | string | Parent customer ID (for sub-customers) |
Job | boolean | Whether this is a job (sub-customer) |
Level | integer | Depth in hierarchy (0 = top-level) |
FullyQualifiedName | string | Full path (e.g., "Acme Corp:Managed Services") |
| Field | Type | Description |
|---|---|---|
MetaData.CreateTime | datetime | Creation timestamp |
MetaData.LastUpdatedTime | datetime | Last update timestamp |
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 /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": "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" }
}'
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
}
async function onboardMspClient(clientData) {
// Step 1: Create parent customer
const customer = await createCustomer({
DisplayName: clientData.companyName,
CompanyName: clientData.companyName,
GivenName: clientData.contactFirstName,
FamilyName: clientData.contactLastName,
PrimaryPhone: { FreeFormNumber: clientData.phone },
PrimaryEmailAddr: { Address: clientData.billingEmail },
BillAddr: {
Line1: clientData.address,
City: clientData.city,
CountrySubDivisionCode: clientData.state,
PostalCode: clientData.zip
},
SalesTermRef: { value: clientData.paymentTermId || '3' }, // Net 30
PreferredDeliveryMethod: 'Email',
Notes: `MSP client. Contract start: ${clientData.contractStart}. PSA ID: ${clientData.psaId}`
});
// Step 2: Create sub-customers for service lines
const serviceLines = ['Managed Services', 'Project Work', 'Hardware'];
for (const line of serviceLines) {
await createCustomer({
DisplayName: `${clientData.companyName}:${line}`,
ParentRef: { value: customer.Id },
Job: true,
BillWithParent: true
});
}
return customer;
}
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}`)
);
}
| Code | Message | Resolution |
|---|---|---|
| 6240 | Duplicate Name | Use a unique DisplayName |
| 610 | Object Not Found | Verify customer ID |
| 5010 | Stale Object | Re-fetch SyncToken and retry |
| 2050 | Invalid Reference | Check ParentRef or SalesTermRef values |
| 3200 | Auth Failed | Refresh access token |
| Error | Cause | Fix |
|---|---|---|
| DisplayName required | Missing DisplayName | Add DisplayName to request |
| Duplicate DisplayName | Name already exists | Use unique name or append qualifier |
| Invalid ParentRef | Non-existent parent | Verify parent customer ID |
| Invalid SalesTermRef | Bad term ID | Query Terms entity for valid IDs |
async function safeCreateCustomer(data) {
try {
return await createCustomer(data);
} catch (error) {
const fault = error.Fault;
if (!fault) throw error;
const errorCode = fault.Error?.[0]?.code;
if (errorCode === '6240') {
// Duplicate -- find existing customer
const existing = await qboQuery(
`SELECT * FROM Customer WHERE DisplayName = '${data.DisplayName}'`
);
return existing.QueryResponse.Customer?.[0];
}
if (errorCode === '5010') {
// Stale SyncToken -- re-fetch and retry
const fresh = await getCustomer(data.Id);
data.SyncToken = fresh.SyncToken;
return await updateCustomer(data);
}
throw error;
}
}
sparse: true to avoid overwriting data| Operation | Method | Endpoint |
|---|---|---|
| Create | POST | /v3/company/{realmId}/customer |
| Read | GET | /v3/company/{realmId}/customer/{id} |
| Update | POST | /v3/company/{realmId}/customer |
| Query | GET | /v3/company/{realmId}/query?query=... |