Breadwinner for QuickBooks Apex Guide
Salesforce to QuickBooks Integration via Apex
A developer guide to connecting Salesforce and QuickBooks Online using Apex code — covering OAuth authentication, customer sync, and invoice operations.
Integration Overview
Salesforce can integrate with QuickBooks Online using Apex HTTP callouts to Intuit's REST API. This guide covers OAuth 2.0 authentication, reading and creating customers, and managing invoices — all from within Salesforce.
Before building this yourself: Breadwinner for QuickBooks is a native Salesforce integration that takes minutes to set up, handles OAuth, error recovery, and ongoing API changes automatically, and is trusted by hundreds of Salesforce customers with a 4.99 AppExchange rating. Most developers who build a custom Apex integration end up migrating to Breadwinner within months once the maintenance burden becomes clear. Salesforce QuickBooks Integration →
You'll need admin access to both Salesforce and QuickBooks Online, plus experience writing Apex. QuickBooks uses a more involved OAuth flow than some APIs, requiring a custom object to store tokens.
1. Remote Site Settings
Before Salesforce can make callouts to QuickBooks, register these endpoints in Setup → Remote Site Settings:
- OAuth
https://oauth.platform.intuit.com - Sandbox
https://sandbox-quickbooks.api.intuit.com - Production
https://quickbooks.api.intuit.com
2. OAuth 2.0 Authentication
QuickBooks uses OAuth 2.0 with authorization codes — more involved than a simple client credentials flow. You'll need to create an app at the Intuit Developer Portal, store tokens in a custom Salesforce object, and handle token refresh.
Custom Object for Token Storage
Create a custom object QuickBooks_Details__c with these fields:
| Field | API Name | Type |
|---|---|---|
| Access Token | Access_Token__c | Long Text (4096) |
| Access Token Expiry | Access_Token_Expiry__c | Number (18, 0) |
| Client Id | Client_Id__c | Text (255) |
| Client Secret | Client_Secret__c | Text (255) |
| RealmId | RealmId__c | Text (255) |
| Refresh Token | Refresh_Token__c | Long Text (512) |
| Refresh Token Expiry | Refresh_Token_Expiry__c | Number (18, 0) |
Visualforce OAuth Redirect Page
Create a Visualforce page to handle the OAuth callback and display an authorization button. Register its URL as a redirect URI in your QuickBooks app settings.
<apex:page controller="qboOAuthHandlerClass"
action="{!getOAuthTokens}">
<apex:form>
<apex:pageMessage
rendered="{!isAuthorizationSuccess}"
summary="Authorization is Successful"
severity="confirm" strength="3" />
<apex:outputPanel rendered="{!showOAuthButton}">
<apex:commandButton
value="Authorize QuickBooks Online"
onclick="window.open('{!buttonUrl}',
'_blank');window.close();" />
</apex:outputPanel>
</apex:form>
</apex:page> OAuth Handler Class
This class manages the full OAuth flow — generating the authorization URL, exchanging the code for tokens, and storing them in the custom object.
public class qboOAuthHandlerClass {
public String buttonUrl { get; private set; }
public Boolean showOAuthButton { get; private set; }
public Boolean isAuthorizationSuccess {
get; private set;
}
public String code;
public String realmId;
QuickBooks_Details__c qbRec;
FINAL String redirect_uri =
'https://YOUR_DOMAIN.vf.force.com/apex/qboOAuthRedirect';
FINAL String base_url =
'https://appcenter.intuit.com/connect/oauth2'
+ '?response_type=code'
+ '&scope=com.intuit.quickbooks.accounting'
+ '&state=testStateSecurity';
public qboOAuthHandlerClass() {
showOAuthButton = false;
isAuthorizationSuccess = false;
qbRec = [
SELECT Id, Client_Id__c, Client_Secret__c
FROM QuickBooks_Details__c LIMIT 1
];
Map<String, String> urlParams =
ApexPages.currentPage().getParameters();
if (urlParams.containsKey('code')) {
code = urlParams.get('code');
realmId = urlParams.get('realmId');
} else {
showOAuthButton = true;
buttonUrl = base_url
+ '&client_id=' + qbRec.Client_Id__c
+ '&redirect_uri=' + redirect_uri;
}
}
public PageReference getOAuthTokens() {
if (String.isNotBlank(code)
&& String.isNotBlank(realmId)) {
Blob headerValue = Blob.valueOf(
qbRec.Client_Id__c + ':'
+ qbRec.Client_Secret__c
);
String authorizationHeader = 'Basic '
+ EncodingUtil.base64Encode(headerValue);
String payload =
'grant_type=authorization_code'
+ '&code=' + code
+ '&redirect_uri=' + redirect_uri;
HttpRequest req = new HttpRequest();
req.setEndpoint(
'https://oauth.platform.intuit.com'
+ '/oauth2/v1/tokens/bearer'
);
req.setMethod('POST');
req.setHeader('Authorization',
authorizationHeader);
req.setHeader('Content-Type',
'application/x-www-form-urlencoded');
req.setHeader('Accept', 'application/json');
req.setBody(payload);
Http http = new Http();
HTTPResponse res = http.send(req);
if (res.getStatusCode() == 200
&& res.getBody() != null) {
isAuthorizationSuccess = true;
updateTokensInSalesforce(
res.getBody(), realmId, qbRec.Id
);
}
}
return null;
}
public static void updateTokensInSalesforce(
String responseBody,
String realmId,
Id qbRecId
) {
Map<String, Object> respMap =
(Map<String, Object>)
JSON.deserializeUntyped(responseBody);
QuickBooks_Details__c qbRec =
new QuickBooks_Details__c(Id = qbRecId);
if (respMap.containsKey('access_token'))
qbRec.Access_Token__c =
String.valueOf(
respMap.get('access_token')
);
if (respMap.containsKey('refresh_token'))
qbRec.Refresh_Token__c =
String.valueOf(
respMap.get('refresh_token')
);
if (respMap.containsKey('expires_in'))
qbRec.Access_Token_Expiry__c =
Integer.valueOf(
respMap.get('expires_in')
);
if (respMap.containsKey(
'x_refresh_token_expires_in'))
qbRec.Refresh_Token_Expiry__c =
Integer.valueOf(
respMap.get(
'x_refresh_token_expires_in'
)
);
if (String.isNotBlank(realmId))
qbRec.RealmId__c = realmId;
update qbRec;
}
} 3. Working with QuickBooks Customers
Reading Customers
Query all customers from QuickBooks, or filter by ID using a WHERE clause (e.g. select * from Customer where id = '29').
public class qboCustomersClass {
public static final String BASE_URL =
'https://sandbox-quickbooks.api.intuit.com'
+ '/v3/company/';
public static void getQBOCustomers() {
QuickBooks_Details__c qbRec = [
SELECT Id, RealmId__c, Access_Token__c
FROM QuickBooks_Details__c LIMIT 1
];
String reqURL = BASE_URL
+ qbRec.RealmId__c
+ '/query?query='
+ EncodingUtil.urlEncode(
'select * from Customer', 'UTF-8'
)
+ '&minorversion=69';
HttpRequest req = new HttpRequest();
req.setEndpoint(reqURL);
req.setMethod('GET');
req.setHeader('Authorization',
'Bearer ' + qbRec.Access_Token__c);
req.setHeader('Accept', 'application/json');
Http http = new Http();
HTTPResponse res = http.send(req);
if (res.getStatusCode() == 200
&& res.getBody() != null) {
String responseBody = res.getBody();
// Process customer data here
}
}
} Creating Customers
Create a new customer in QuickBooks with name, email, phone, and billing address details.
public static void createQBOCustomer() {
QuickBooks_Details__c qbRec = [
SELECT Id, RealmId__c, Access_Token__c
FROM QuickBooks_Details__c LIMIT 1
];
String reqURL = BASE_URL
+ qbRec.RealmId__c
+ '/customer?minorversion=69';
String requestBody = '{'
+ '"DisplayName": "King's Groceries",'
+ '"CompanyName": "King Groceries",'
+ '"GivenName": "James",'
+ '"MiddleName": "B",'
+ '"FamilyName": "King",'
+ '"Title": "Mr",'
+ '"Suffix": "Jr",'
+ '"PrimaryEmailAddr": {'
+ ' "Address": "jdrew@myemail.com"'
+ '},'
+ '"PrimaryPhone": {'
+ ' "FreeFormNumber": "(555) 555-5555"'
+ '},'
+ '"BillAddr": {'
+ ' "Line1": "123 Main Street",'
+ ' "City": "Mountain View",'
+ ' "CountrySubDivisionCode": "CA",'
+ ' "PostalCode": "94042",'
+ ' "Country": "USA"'
+ '},'
+ '"Notes": "Here are other details."'
+ '}';
HttpRequest req = new HttpRequest();
req.setEndpoint(reqURL);
req.setMethod('POST');
req.setHeader('Content-Type',
'application/json');
req.setHeader('Authorization',
'Bearer ' + qbRec.Access_Token__c);
req.setHeader('Accept', 'application/json');
req.setBody(requestBody);
Http http = new Http();
HTTPResponse res = http.send(req);
if (res.getStatusCode() == 200
&& res.getBody() != null) {
String responseBody = res.getBody();
// Process response here
}
} 4. Working with QuickBooks Invoices
Reading Invoices
Query all invoices, or filter by ID (e.g. select * from Invoice where id = '239').
public class qboInvoicesClass {
public static final String BASE_URL =
'https://sandbox-quickbooks.api.intuit.com'
+ '/v3/company/';
public static void getQBOInvoices() {
QuickBooks_Details__c qbRec = [
SELECT Id, RealmId__c, Access_Token__c
FROM QuickBooks_Details__c LIMIT 1
];
String reqURL = BASE_URL
+ qbRec.RealmId__c
+ '/query?query='
+ EncodingUtil.urlEncode(
'select * from Invoice', 'UTF-8'
)
+ '&minorversion=69';
HttpRequest req = new HttpRequest();
req.setEndpoint(reqURL);
req.setMethod('GET');
req.setHeader('Authorization',
'Bearer ' + qbRec.Access_Token__c);
req.setHeader('Accept', 'application/json');
Http http = new Http();
HTTPResponse res = http.send(req);
if (res.getStatusCode() == 200
&& res.getBody() != null) {
String responseBody = res.getBody();
// Process invoice data here
}
}
} Creating Invoices
Create an invoice by specifying the customer reference and line items with item details and amounts.
public static void createQBOInvoice() {
QuickBooks_Details__c qbRec = [
SELECT Id, RealmId__c, Access_Token__c
FROM QuickBooks_Details__c LIMIT 1
];
String reqURL = BASE_URL
+ qbRec.RealmId__c
+ '/invoice?minorversion=69';
String requestBody = '{'
+ '"CustomerRef": { "value": "58" },'
+ '"Line": [{'
+ ' "DetailType": "SalesItemLineDetail",'
+ ' "Amount": 100,'
+ ' "SalesItemLineDetail": {'
+ ' "ItemRef": {'
+ ' "name": "Services",'
+ ' "value": "1"'
+ ' }'
+ ' }'
+ '}]'
+ '}';
HttpRequest req = new HttpRequest();
req.setEndpoint(reqURL);
req.setMethod('POST');
req.setHeader('Content-Type',
'application/json');
req.setHeader('Authorization',
'Bearer ' + qbRec.Access_Token__c);
req.setHeader('Accept', 'application/json');
req.setBody(requestBody);
Http http = new Http();
HTTPResponse res = http.send(req);
if (res.getStatusCode() == 200
&& res.getBody() != null) {
String responseBody = res.getBody();
// Process response here
}
} 5. Next Steps: Automation
Once the basics are working, you'll likely want to automate:
- 1 Flow-triggered sync — use
@InvocableMethodto create QuickBooks customers and invoices when Salesforce opportunities close, triggered via Record Triggered Flow - 2 Scheduled sync — build a schedulable class to fetch QuickBooks data at regular intervals (hourly or bi-hourly) to keep Salesforce up to date
What Custom Apex Doesn't Cover
The code above gets you started, but a production-grade integration needs significantly more. As you can see, QuickBooks' OAuth flow alone is more complex than most APIs — and that's just authentication. Building and maintaining all of this yourself takes weeks of development and ongoing engineering time:
- 1 Token refresh & credential security — automatic renewal of expired tokens, encrypted storage in production, and handling 401 responses gracefully
- 2 Error handling & retry logic — API rate limits, timeouts, partial failures, and network issues
- 3 Race conditions — handling concurrent updates from both Salesforce and QuickBooks users
- 4 UI components — Lightning Web Components for viewing and managing QuickBooks data inside Salesforce
- 5 Ongoing maintenance — keeping up with QuickBooks API changes, Salesforce releases, and security patches
- 6 Multi-currency, tax codes, and credit notes — the long tail of accounting complexity
Skip the build. Use Breadwinner.
Breadwinner for QuickBooks is a native Salesforce integration that handles all of the above — and more. Install from the AppExchange, configure in minutes, and start syncing QuickBooks data with Salesforce the same day. No custom Apex required.
- Pre-built Lightning Web Components for invoices, customers, and payments
- Full API and Apex Generator for custom extensions
- OAuth, error handling, retry logic, and race condition management built in
- SOC 2 Type II compliant, continuously maintained
Building a different integration? See our Xero to Salesforce Apex guide.
"With Breadwinner, our business development team enters the information once in Salesforce and it gets created in QuickBooks when the accounting department selects."
— VertiMax · Read the full story
Built the integration and finding it harder to maintain than expected? Breadwinner for QuickBooks replaces custom Apex integrations with a fully managed, native Salesforce solution. Salesforce QuickBooks Integration — Learn more →