QuickBooks Connector

Automations

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

cm_finance__Source_Invoice_Customer_Lookup_field__c

API name of the field on the source object that holds the related record whose QB Customer will be set on the invoice

cm_finance__Invoice_Product_Pre_Population_Mapping__c

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:

{
    "TxnDate"     : "YYYY-MM-DD",
    "PrivateNote" : "Optional memo or private note",
    "Line" : [
        {
            "DetailType"  : "JournalEntryLineDetail",
            "Amount"      : 500.00,
            "Description" : "Optional line description",
            "JournalEntryLineDetail" : {
                "PostingType" : "Debit",
                "AccountRef"  : {
                    "value"    : "<QB_Account_Id>",
                    "name"     : "<Account_Name>",
                    "recordId" : "<Salesforce_cm_finance__Quickbooks_Account__c_Id>"
                }
            }
        },
        {
            "DetailType"  : "JournalEntryLineDetail",
            "Amount"      : 500.00,
            "Description" : "Optional line description",
            "JournalEntryLineDetail" : {
                "PostingType" : "Credit",
                "AccountRef"  : {
                    "value"    : "<QB_Account_Id>",
                    "name"     : "<Account_Name>",
                    "recordId" : "<Salesforce_cm_finance__Quickbooks_Account__c_Id>"
                },
                "Entity" : {
                    "Type" : "Customer",
                    "EntityRef" : {
                        "value"    : "<QB_Customer_Id>",
                        "name"     : "<Customer_Name>",
                        "recordId" : "<Salesforce_cm_finance__Quickbooks_Customer__c_Id>"
                    }
                }
            }
        }
    ]
}

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

QB rejects unbalanced entry

Returns { isSuccess: false, message: '<QB error message>' }

Account / Customer / Vendor recordId provided on AccountRef / EntityRef

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

cm_finance__Source_Invoice_Customer_Lookup_field__c

API name of the field on the source object that holds the related record whose QB Customer will be set on the Sales Receipt

cm_finance__Invoice_Product_Pre_Population_Mapping__c

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