This document provides methods to automate the process for QuickBooks sync
1. QuickBooks Customer Creation Automation
To automate QuickBooks Customer creation, you can use either a Flow or Apex code.
1.1 Automation using Flow
To automate the process, add an Apex Action within a Record-Triggered Flow named Call QB Service. You will need to provide the Triggering Record Id and set the requestType to Customer.
If a corresponding Custom Field Mapping record exists for the object with the provided ID, the QB Customer record will be created. If no matching mapping record is found, the customer will not be created.
Optional fields available on the Flow action input:
companyRecordId — The Salesforce Record Id of the cm_finance__Quickbooks_Company__c record. If not provided, the default company configured in cm_finance__Quickbooks_Common_Settings__c is used.
parentCustomerRecordId — The Salesforce Record Id of an existing cm_finance__Quickbooks_Customer__c record. Provide this when the new customer should be created as a sub-customer under that parent in QuickBooks.
1.2 Automation using Code
Basic (single customer, default company):
List<cm_finance.InvocableAutomateQBService.QBWrapper> lstWrappers = new List<cm_finance.InvocableAutomateQBService.QBWrapper>();
cm_finance.InvocableAutomateQBService.QBWrapper oWrapper = new cm_finance.InvocableAutomateQBService.QBWrapper();
oWrapper.recordId = 'RecordId'; // Replace with the Salesforce Record Id of the source object
oWrapper.requestType = 'Customer';
lstWrappers.add(oWrapper);
cm_finance.InvocableAutomateQBService.callQBService(lstWrappers);
With a specific QB Company:
List<cm_finance.InvocableAutomateQBService.QBWrapper> lstWrappers = new List<cm_finance.InvocableAutomateQBService.QBWrapper>();
cm_finance.InvocableAutomateQBService.QBWrapper oWrapper = new cm_finance.InvocableAutomateQBService.QBWrapper();
oWrapper.recordId = 'RecordId'; // Replace with the source Salesforce Record Id
oWrapper.requestType = 'Customer';
oWrapper.companyRecordId = 'CompanyRecordId'; // Replace with the Salesforce Id of the QB Company record
lstWrappers.add(oWrapper);
cm_finance.InvocableAutomateQBService.callQBService(lstWrappers);
With a Sub-Customer (Parent Customer):
List<cm_finance.InvocableAutomateQBService.QBWrapper> lstWrappers = new List<cm_finance.InvocableAutomateQBService.QBWrapper>();
cm_finance.InvocableAutomateQBService.QBWrapper oWrapper = new cm_finance.InvocableAutomateQBService.QBWrapper();
oWrapper.recordId = 'RecordId'; // Source Salesforce Record Id
oWrapper.requestType = 'Customer';
oWrapper.companyRecordId = 'CompanyRecordId'; // Salesforce Id of the QB Company record
oWrapper.parentCustomerRecordId = 'ParentCustomerRecordId'; // Salesforce Id of the parent cm_finance__Quickbooks_Customer__c record
lstWrappers.add(oWrapper);
cm_finance.InvocableAutomateQBService.callQBService(lstWrappers);
Behavior notes:
Scenario
Processing Mode
Single record submitted
Dispatched immediately as a QueueableJob — real-time HTTP callout to QuickBooks Online
Multiple records submitted
Staged in Salesforce with Process_via_Batch__c = true — a scheduled batch job processes them asynchronously
QB Customer already exists for the source record
Service performs an update (existing QB Id and Sync Token are fetched automatically)
No Custom Field Mapping found
cm_finance__Quickbooks_Customer__c record created with Status = 'Failed' and reason Field Mapping not found for customer
API or processing failure
cm_finance__Quickbooks_Customer__c record created with Status = 'Failed' and the error message
2. QuickBooks Product / Item Creation Automation
To automate QuickBooks Product (Item) creation, you can use either a Flow or Apex code.
2.1 Automation using Flow
To automate the process, add an Apex Action within a Record-Triggered Flow named Call QB Service. You will need to provide the Triggering Record Id and set the requestType to Product.
If a corresponding Custom Field Mapping record exists for the object with the provided ID, the QB Product/Item record will be created. If no matching mapping record is found, the product will not be created.
Optional fields available on the Flow action input:
companyRecordId — The Salesforce Record Id of the QB Company. Defaults to the default company if not provided.
incomeAccountId — The Salesforce Record Id of a cm_finance__Quickbooks_Account__c record to assign as the Income Account on the QB Item.
expenseAccountId — The Salesforce Record Id of a cm_finance__Quickbooks_Account__c record to assign as the Expense Account on the QB Item.
assetAccountId — The Salesforce Record Id of a cm_finance__Quickbooks_Account__c record to assign as the Asset Account on the QB Item.
Note: Unlike Customer, all Product requests — regardless of record count — are always staged via batch (Process_via_Batch__c = true). There is no synchronous real-time path for Products.
2.2 Automation using Code
Basic (single product, default company):
List<cm_finance.InvocableAutomateQBService.QBWrapper> lstWrappers = new List<cm_finance.InvocableAutomateQBService.QBWrapper>();
cm_finance.InvocableAutomateQBService.QBWrapper oWrapper = new cm_finance.InvocableAutomateQBService.QBWrapper();
oWrapper.recordId = 'RecordId'; // Replace with the Salesforce Record Id of the source object
oWrapper.requestType = 'Product';
lstWrappers.add(oWrapper);
cm_finance.InvocableAutomateQBService.callQBService(lstWrappers);
With account references and a specific QB Company:
List<cm_finance.InvocableAutomateQBService.QBWrapper> lstWrappers = new List<cm_finance.InvocableAutomateQBService.QBWrapper>();
cm_finance.InvocableAutomateQBService.QBWrapper oWrapper = new cm_finance.InvocableAutomateQBService.QBWrapper();
oWrapper.recordId = 'RecordId'; // Source Salesforce Record Id
oWrapper.requestType = 'Product';
oWrapper.companyRecordId = 'CompanyRecordId'; // Salesforce Id of the QB Company record
oWrapper.incomeAccountId = 'IncomeAccountId'; // Salesforce Id of the cm_finance__Quickbooks_Account__c (Income)
oWrapper.expenseAccountId = 'ExpenseAccountId'; // Salesforce Id of the cm_finance__Quickbooks_Account__c (Expense)
oWrapper.assetAccountId = 'AssetAccountId'; // Salesforce Id of the cm_finance__Quickbooks_Account__c (Asset)
lstWrappers.add(oWrapper);
cm_finance.InvocableAutomateQBService.callQBService(lstWrappers);
Behavior notes:
Scenario
Processing Mode
Any number of records submitted
Always staged with Process_via_Batch__c = true — processed asynchronously by batch
QB Product already exists for the source record
Service performs an update (upserted on Composite_Unique_Key__c)
Account Ids provided
Resolved to their QB Account Ids and stored on the product record (cm_finance__QB_Income_Account_QB_Id__c, cm_finance__QB_Expense_Account_QB_Id__c, cm_finance__QB_Asset_Account_QB_Id__c)
No Custom Field Mapping found
cm_finance__Quickbooks_Product__c record created with Status = 'Failed' and reason Field Mapping not found for product
API or processing failure
cm_finance__Quickbooks_Product__c record created with Status = 'Failed' and the error message
3. QuickBooks Invoice Creation Automation
Invoice automation through InvocableAutomateQBService is only available via Apex code. It cannot be triggered directly from a standard Flow Apex Action because the invoice request wrapper (oRequestWrapper) is not an @InvocableVariable. Invoice creation is typically triggered from a Trigger Handler on the source object or from custom Apex.
Invoice creation requires a Custom Field Mapping record (cm_finance__Custom_Field_Mapping__c) configured for the source object with cm_finance__Mapping_Type__c = 'Invoice'. This mapping defines:
Mapping Field
Purpose
cm_finance__Field_Mapping__c
JSON: QB Invoice field → source object Salesforce field
JSON config for auto-populating line items from child records (query, lookupFieldName, sfProductId, QBClassLookup, ProductQuantity, ProductUnitPrice, ProductDescription)
cm_finance__QB_Custom_Fields_Mapping__c
Optional JSON: QB Custom Field Definition Id → source object Salesforce field
3.1 Automation using Code
Single invoice (real-time Queueable path):
List<cm_finance.InvocableAutomateQBService.QBWrapper> lstWrappers = new List<cm_finance.InvocableAutomateQBService.QBWrapper>();
cm_finance.InvocableAutomateQBService.QBWrapper oWrapper = new cm_finance.InvocableAutomateQBService.QBWrapper();
oWrapper.recordId = 'SourceRecordId'; // Salesforce Id of the source record (e.g. Opportunity Id)
oWrapper.oRequestWrapper = new cm_finance.QBApiDataWrapper.QBRequestWrapper();
oWrapper.oRequestWrapper.companyId = 'CompanyRecordId'; // Salesforce Id of the cm_finance__Quickbooks_Company__c record
lstWrappers.add(oWrapper);
cm_finance.InvocableAutomateQBService.createQBInvoiceFromMapping(lstWrappers);
Multiple invoices (bulk batch path):
List<cm_finance.InvocableAutomateQBService.QBWrapper> lstWrappers = new List<cm_finance.InvocableAutomateQBService.QBWrapper>();
for (Id sourceRecordId : setSourceRecordIds) {
cm_finance.InvocableAutomateQBService.QBWrapper oWrapper = new cm_finance.InvocableAutomateQBService.QBWrapper();
oWrapper.recordId = sourceRecordId;
oWrapper.oRequestWrapper = new cm_finance.QBApiDataWrapper.QBRequestWrapper();
oWrapper.oRequestWrapper.companyId = 'CompanyRecordId'; // Same Salesforce Id of the QB Company record for all
lstWrappers.add(oWrapper);
}
cm_finance.InvocableAutomateQBService.createQBInvoiceFromMapping(lstWrappers);
Behavior notes:
Scenario
Processing Mode
Single invoice
Dispatched as a QueueableJob — real-time HTTP callout to QuickBooks Online
Multiple invoices
Staged with Process_via_Batch__c = true — processed asynchronously by batch
QB Customer for the source record is not yet synced
Service triggers Customer creation first, then chains Invoice creation in a subsequent QueueableJob
More than one record is missing its QB Customer
All records without a resolved customer are immediately failed; only records with a valid customer proceed
Line items
Auto-populated from child records per Invoice_Product_Pre_Population_Mapping__c; Products resolved via cm_finance__Parent_Record_Id__c; QB Classes resolved via Salesforce Id
QB Custom Fields
Resolved from cm_finance__Quickbooks_Custom_Field__c by cm_finance__Definition_Id__c; stored in cm_finance__Custom_Field_1/2/3__c
GenerateInvoicePDF setting enabled
Successfully created invoices begin in status Invoice PDF Pending instead of Awaiting Salesforce Sync
No Custom Field Mapping found
cm_finance__Quickbooks_Invoice__c record created with Status = 'Failed' and reason Custom Field Mapping not found for current object
No QB Company on request wrapper
cm_finance__Quickbooks_Invoice__c record created with Status = 'Failed' and reason Quickbooks Company not found on Request wrapper
Customer reference field empty on source
cm_finance__Quickbooks_Invoice__c record created with Status = 'Failed' and reason Customer reference field not populated on source object
No line items resolved
cm_finance__Quickbooks_Invoice__c record created with Status = 'Failed' and reason Invoice Line Items not found on source object
4. QuickBooks Journal Entry Creation Automation
Journal Entry creation is only available via Apex code. It cannot be triggered from a Flow Apex Action because companyId (QB Realm Id) and requestBody are not @InvocableVariable fields on QBWrapper. The entry point is InvocableAutomateQBService.createJournalLedgerRecords(), which internally calls QuickbooksController.createJournalLedgerInQuickbooks() and makes a synchronous real-time callout to QuickBooks Online.
Request Body Structure
The requestBody is a JSON string with the following shape:
Entity is only required on Accounts Receivable lines (set Type = 'Customer') and Accounts Payable lines (set Type = 'Vendor'). For all other account types, omit the Entity block entirely.
Balance constraint: Total Debit amount must equal total Credit amount across all lines. The system does not enforce this — QuickBooks will reject the payload if the journal entry is out of balance.
4.1 Automation using Code
Basic (two-line balanced journal entry, no entity):
Map<String, Object> requestPayload = new Map<String, Object>{
'TxnDate' => '2025-05-04',
'PrivateNote' => 'Monthly revenue recognition',
'Line' => new List<Object>{
new Map<String, Object>{
'DetailType' => 'JournalEntryLineDetail',
'Amount' => 500.00,
'Description' => 'Revenue - Services',
'JournalEntryLineDetail' => new Map<String, Object>{
'PostingType' => 'Debit',
'AccountRef' => new Map<String, Object>{
'value' => 'QB_Account_Id_1', // QB Account Id (cm_finance__Quickbooks_Id__c)
'name' => 'Accounts Receivable',
'recordId' => 'SF_Account_SF_Id_1' // Salesforce Id of cm_finance__Quickbooks_Account__c
}
}
},
new Map<String, Object>{
'DetailType' => 'JournalEntryLineDetail',
'Amount' => 500.00,
'Description' => 'Revenue - Services',
'JournalEntryLineDetail' => new Map<String, Object>{
'PostingType' => 'Credit',
'AccountRef' => new Map<String, Object>{
'value' => 'QB_Account_Id_2',
'name' => 'Services Revenue',
'recordId' => 'SF_Account_SF_Id_2'
}
}
}
}
};
List<cm_finance.InvocableAutomateQBService.QBWrapper> lstRecords = new List<cm_finance.InvocableAutomateQBService.QBWrapper>();
cm_finance.InvocableAutomateQBService.QBWrapper oWrapper = new cm_finance.InvocableAutomateQBService.QBWrapper();
oWrapper.companyRecordId = 'CompanyRecordId'; // Salesforce Id of cm_finance__Quickbooks_Company__c
oWrapper.companyId = 'QB_Realm_Id'; // QuickBooks Realm Id (cm_finance__Company_Id__c on the Company record)
oWrapper.requestBody = JSON.serialize(requestPayload);
lstRecords.add(oWrapper);
Map<String, Object> result = cm_finance.InvocableAutomateQBService.createJournalLedgerRecords(lstRecords);
Boolean isSuccess = (Boolean) result.get('isSuccess');
String message = (String) result.get('message');
With Entity (AR / AP lines — Customer or Vendor reference):
Map<String, Object> requestPayload = new Map<String, Object>{
'TxnDate' => '2025-05-04',
'PrivateNote' => 'AR entry for customer payment',
'Line' => new List<Object>{
new Map<String, Object>{
'DetailType' => 'JournalEntryLineDetail',
'Amount' => 1000.00,
'Description' => 'AR - Customer Invoice',
'JournalEntryLineDetail' => new Map<String, Object>{
'PostingType' => 'Debit',
'AccountRef' => new Map<String, Object>{
'value' => 'QB_AR_Account_Id',
'name' => 'Accounts Receivable',
'recordId' => 'SF_AR_Account_SF_Id'
},
'Entity' => new Map<String, Object>{ // Required for AR lines
'Type' => 'Customer',
'EntityRef' => new Map<String, Object>{
'value' => 'QB_Customer_Id', // QB Customer Id (Quickbooks_Id__c)
'name' => 'Acme Corp',
'recordId' => 'SF_Customer_SF_Id' // Salesforce Id of cm_finance__Quickbooks_Customer__c
}
}
}
},
new Map<String, Object>{
'DetailType' => 'JournalEntryLineDetail',
'Amount' => 1000.00,
'Description' => 'Revenue - Product Sales',
'JournalEntryLineDetail' => new Map<String, Object>{
'PostingType' => 'Credit',
'AccountRef' => new Map<String, Object>{
'value' => 'QB_Revenue_Account_Id',
'name' => 'Product Revenue',
'recordId' => 'SF_Revenue_Account_SF_Id'
}
}
}
}
};
List<cm_finance.InvocableAutomateQBService.QBWrapper> lstRecords = new List<cm_finance.InvocableAutomateQBService.QBWrapper>();
cm_finance.InvocableAutomateQBService.QBWrapper oWrapper = new cm_finance.InvocableAutomateQBService.QBWrapper();
oWrapper.companyRecordId = 'CompanyRecordId'; // Salesforce Id of cm_finance__Quickbooks_Company__c
oWrapper.companyId = 'QB_Realm_Id'; // QuickBooks Realm Id (cm_finance__Company_Id__c)
oWrapper.requestBody = JSON.serialize(requestPayload);
lstRecords.add(oWrapper);
Map<String, Object> result = cm_finance.InvocableAutomateQBService.createJournalLedgerRecords(lstRecords);
Boolean isSuccess = (Boolean) result.get('isSuccess');
String message = (String) result.get('message');
Behavior notes:
Scenario
Result
Successful callout
cm_finance__Quickbooks_Journal_Entry__c record created with line items as cm_finance__Quickbooks_Journal_Entry_Item__c children; status Awaiting Salesforce Sync
companyRecordId, companyId, or requestBody missing
Returns { isSuccess: false, message: 'Company Id, Company Record Id and Request Body are mandatory for API callout' } — no record created
Resolved to their Salesforce lookup fields on the journal line item record
API or processing exception
Returns { isSuccess: false, message: 'Exception --> <error> at line number : <n>' }
5. QuickBooks Sales Receipt Creation Automation
Sales Receipt automation through InvocableAutomateQBService is only available via Apex code. It cannot be triggered directly from a standard Flow Apex Action because the invoice request wrapper (oRequestWrapper) is not an @InvocableVariable. Sales Receipt creation is typically triggered from a Trigger Handler on the source object or from custom Apex.
Sales Receipt creation requires a Custom Field Mapping record (cm_finance__Custom_Field_Mapping__c) configured for the source object with cm_finance__Mapping_Type__c = 'SalesReceipt'. This mapping defines:
Mapping Field
Purpose
cm_finance__Field_Mapping__c
JSON: QB Sales Receipt field → source object Salesforce field
JSON config for auto-populating line items from child records (query, lookupFieldName, sfProductId, QBClassLookup, ProductQuantity, ProductUnitPrice, ProductDescription)
cm_finance__QB_Custom_Fields_Mapping__c
Optional JSON: QB Custom Field Definition Id → source object Salesforce field
5.1 Automation using Code
Single Sales Receipt (real-time Queueable path):
List<cm_finance.InvocableAutomateQBService.QBWrapper> lstWrappers = new List<cm_finance.InvocableAutomateQBService.QBWrapper>();
cm_finance.InvocableAutomateQBService.QBWrapper oWrapper = new cm_finance.InvocableAutomateQBService.QBWrapper();
oWrapper.recordId = 'SourceRecordId'; // Salesforce Id of the source record (e.g. Opportunity Id)
oWrapper.oRequestWrapper = new cm_finance.QBApiDataWrapper.QBRequestWrapper();
oWrapper.oRequestWrapper.companyId = 'CompanyRecordId'; // Salesforce Id of the cm_finance__Quickbooks_Company__c record
lstWrappers.add(oWrapper);
cm_finance.InvocableAutomateQBService.createQBSalesReceiptFromMapping(lstWrappers);
Multiple Sales Receipts (bulk batch path):
List<cm_finance.InvocableAutomateQBService.QBWrapper> lstWrappers = new List<cm_finance.InvocableAutomateQBService.QBWrapper>();
for (Id sourceRecordId : setSourceRecordIds) {
cm_finance.InvocableAutomateQBService.QBWrapper oWrapper = new cm_finance.InvocableAutomateQBService.QBWrapper();
oWrapper.recordId = sourceRecordId;
oWrapper.oRequestWrapper = new cm_finance.QBApiDataWrapper.QBRequestWrapper();
oWrapper.oRequestWrapper.companyId = 'CompanyRecordId'; // Same Salesforce Id of the QB Company record for all
lstWrappers.add(oWrapper);
}
cm_finance.InvocableAutomateQBService.createQBSalesReceiptFromMapping(lstWrappers);
Behavior notes:
Scenario
Processing Mode
Single sales receipt
Dispatched as a QueueableJob — real-time HTTP callout to QuickBooks Online
Multiple sales receipts
Staged with Process_via_Batch__c = true — processed asynchronously by batch
QB Customer for the source record is not yet synced
Service triggers Customer creation first, then chains sales receipt creation in a subsequent QueueableJob
More than one record is missing its QB Customer
All records without a resolved customer are immediately failed; only records with a valid customer proceed
Line items
Auto-populated from child records per Invoice_Product_Pre_Population_Mapping__c; Products resolved via cm_finance__Parent_Record_Id__c; QB Classes resolved via Salesforce Id
QB Custom Fields
Resolved from cm_finance__Quickbooks_Custom_Field__c by cm_finance__Definition_Id__c; stored in cm_finance__Custom_Field_1/2/3__c
GenerateSalesReceiptPDF setting enabled
Successfully created invoices begin in status Sales Receipt PDF Pending instead of Awaiting Salesforce Sync
No Custom Field Mapping found
cm_finance__Quickbooks_Sales_Receipt__c record created with Status = 'Failed' and reason Custom Field Mapping not found for current object
No QB Company on request wrapper
cm_finance__Quickbooks_Sales_Receipt__c record created with Status = 'Failed' and reason Quickbooks Company not found on Request wrapper
Customer reference field empty on source
cm_finance__Quickbooks_Sales_Receipt__c record created with Status = 'Failed' and reason Customer reference field not populated on source object
No line items resolved
cm_finance__Quickbooks_Sales_Receipt__c record created with Status = 'Failed' and reason Sales Receipt Line Items not found on source object