QuickBooks Online API provides comprehensive access to accounting data, allowing Java applications to automate financial operations, sync transaction data, and build integrated business solutions. With robust REST APIs and SDK support, Java developers can create powerful financial applications that seamlessly integrate with QuickBooks.
Why QuickBooks API for Java?
Key integration scenarios:
- Automated Bookkeeping - Sync transactions from e-commerce platforms
- Invoice Management - Create and manage invoices programmatically
- Financial Reporting - Extract data for custom analytics and dashboards
- Payment Processing - Connect with QuickBooks payments
- Multi-company Accounting - Manage multiple QuickBooks companies
OAuth 2.0 Authentication Setup
1. QuickBooks Configuration
@Configuration
@ConfigurationProperties(prefix = "quickbooks")
@Data
public class QuickBooksConfig {
private String clientId;
private String clientSecret;
private String redirectUri;
private String environment = "production";
private String baseUrl;
private String minorVersion = "65";
public String getAuthorizationUrl(String state) {
return String.format(
"https://appcenter.intuit.com/connect/oauth2?client_id=%s&response_type=code&scope=com.intuit.quickbooks.accounting&redirect_uri=%s&state=%s",
clientId, redirectUri, state
);
}
public String getTokenUrl() {
return "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer";
}
public String getApiUrl(String realmId) {
if ("sandbox".equals(environment)) {
return "https://sandbox-quickbooks.api.intuit.com/v3/company/" + realmId;
}
return "https://quickbooks.api.intuit.com/v3/company/" + realmId;
}
}
2. OAuth Service
@Service
@Slf4j
public class QuickBooksOAuthService {
private final QuickBooksConfig config;
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
public QuickBooksOAuthService(QuickBooksConfig config) {
this.config = config;
this.httpClient = new OkHttpClient();
this.objectMapper = new ObjectMapper();
}
public String getAuthorizationUrl(String state) {
return config.getAuthorizationUrl(state);
}
public OAuthTokens exchangeCodeForTokens(String authorizationCode) throws OAuthException {
try {
String credentials = Credentials.basic(config.getClientId(), config.getClientSecret());
FormBody formBody = new FormBody.Builder()
.add("grant_type", "authorization_code")
.add("code", authorizationCode)
.add("redirect_uri", config.getRedirectUri())
.build();
Request request = new Request.Builder()
.url(config.getTokenUrl())
.header("Authorization", credentials)
.header("Content-Type", "application/x-www-form-urlencoded")
.post(formBody)
.build();
Response response = httpClient.newCall(request).execute();
if (!response.isSuccessful()) {
throw new OAuthException("Failed to exchange code for tokens: " + response.code());
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, OAuthTokens.class);
} catch (Exception e) {
throw new OAuthException("OAuth token exchange failed", e);
}
}
public OAuthTokens refreshTokens(String refreshToken) throws OAuthException {
try {
String credentials = Credentials.basic(config.getClientId(), config.getClientSecret());
FormBody formBody = new FormBody.Builder()
.add("grant_type", "refresh_token")
.add("refresh_token", refreshToken)
.build();
Request request = new Request.Builder()
.url(config.getTokenUrl())
.header("Authorization", credentials)
.header("Content-Type", "application/x-www-form-urlencoded")
.post(formBody)
.build();
Response response = httpClient.newCall(request).execute();
if (!response.isSuccessful()) {
throw new OAuthException("Failed to refresh tokens: " + response.code());
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, OAuthTokens.class);
} catch (Exception e) {
throw new OAuthException("Token refresh failed", e);
}
}
public boolean validateToken(String accessToken) {
try {
// QuickBooks doesn't have a dedicated token validation endpoint
// We can make a lightweight API call to validate
Request request = new Request.Builder()
.url("https://sandbox-quickbooks.api.intuit.com/v3/company/companyinfo")
.header("Authorization", "Bearer " + accessToken)
.header("Accept", "application/json")
.build();
Response response = httpClient.newCall(request).execute();
return response.isSuccessful();
} catch (Exception e) {
return false;
}
}
@Data
public static class OAuthTokens {
private String accessToken;
private String refreshToken;
private String tokenType;
private Integer expiresIn;
private Integer xRefreshTokenExpiresIn;
private String idToken;
public Instant getAccessTokenExpiry() {
return Instant.now().plusSeconds(expiresIn != null ? expiresIn - 300 : 3600); // 5 min buffer
}
public Instant getRefreshTokenExpiry() {
return Instant.now().plusSeconds(xRefreshTokenExpiresIn != null ? xRefreshTokenExpiresIn - 300 : 8726400);
}
}
}
Core QuickBooks API Client
1. Base API Client
@Service
@Slf4j
public class QuickBooksApiClient {
private final QuickBooksConfig config;
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
public QuickBooksApiClient(QuickBooksConfig config) {
this.config = config;
this.objectMapper = new ObjectMapper();
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
this.httpClient = new OkHttpClient.Builder()
.addInterceptor(new QuickBooksAuthInterceptor())
.addInterceptor(new HttpLoggingInterceptor(log::info).setLevel(HttpLoggingInterceptor.Level.BASIC))
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
}
// Generic GET request
public <T> T get(String realmId, String endpoint, Class<T> responseType) throws QuickBooksApiException {
return executeRequest("GET", realmId, endpoint, null, responseType);
}
// Generic POST request
public <T> T post(String realmId, String endpoint, Object body, Class<T> responseType) throws QuickBooksApiException {
return executeRequest("POST", realmId, endpoint, body, responseType);
}
// Generic PUT request (update)
public <T> T put(String realmId, String endpoint, Object body, Class<T> responseType) throws QuickBooksApiException {
return executeRequest("POST", realmId, endpoint, body, responseType); // QuickBooks uses POST for updates
}
// Generic DELETE request
public void delete(String realmId, String endpoint) throws QuickBooksApiException {
executeRequest("DELETE", realmId, endpoint, null, Void.class);
}
// Query API (for complex searches)
public <T> List<T> query(String realmId, String query, Class<T> resultType) throws QuickBooksApiException {
try {
String encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8);
String endpoint = "/query?query=" + encodedQuery + "&minorversion=" + config.getMinorVersion();
QueryResponse response = get(realmId, endpoint, QueryResponse.class);
return response.getQueryResponse().get(resultType.getSimpleName().toLowerCase());
} catch (Exception e) {
throw new QuickBooksApiException("Query failed: " + query, e);
}
}
// Batch operations
public BatchResponse batch(String realmId, List<BatchItem> batchItems) throws QuickBooksApiException {
BatchRequest batchRequest = new BatchRequest();
batchRequest.setBatchItemRequest(batchItems.toArray(new BatchItem[0]));
return post(realmId, "/batch?minorversion=" + config.getMinorVersion(),
batchRequest, BatchResponse.class);
}
private <T> T executeRequest(String method, String realmId, String endpoint,
Object body, Class<T> responseType) throws QuickBooksApiException {
try {
String url = config.getApiUrl(realmId) + endpoint;
if (!endpoint.contains("minorversion")) {
url += (endpoint.contains("?") ? "&" : "?") + "minorversion=" + config.getMinorVersion();
}
Request.Builder requestBuilder = new Request.Builder()
.url(url)
.header("Accept", "application/json")
.header("Content-Type", "application/json");
if (body != null) {
String jsonBody = objectMapper.writeValueAsString(body);
requestBuilder.method(method, RequestBody.create(jsonBody, MediaType.parse("application/json")));
} else {
requestBuilder.method(method, null);
}
Request request = requestBuilder.build();
Response response = httpClient.newCall(request).execute();
if (!response.isSuccessful()) {
handleErrorResponse(response, method + " " + endpoint);
}
if (responseType == Void.class) {
return null;
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, responseType);
} catch (Exception e) {
throw new QuickBooksApiException("API request failed: " + method + " " + endpoint, e);
}
}
private void handleErrorResponse(Response response, String context) throws IOException, QuickBooksApiException {
int code = response.code();
String body = response.body() != null ? response.body().string() : "No response body";
log.error("QuickBooks API error - Status: {}, Context: {}, Body: {}", code, context, body);
try {
QuickBooksError error = objectMapper.readValue(body, QuickBooksError.class);
throw new QuickBooksApiException("API Error: " + error.getFault().getError()[0].getMessage(), code);
} catch (Exception e) {
// If we can't parse the error response, throw generic exception
throw new QuickBooksApiException("API error - Status: " + code + ", Context: " + context, code);
}
}
// Authentication interceptor (would be populated with current user's token)
private class QuickBooksAuthInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
// In a real implementation, this would get the current user's access token
String accessToken = getCurrentUserAccessToken();
Request authenticatedRequest = chain.request().newBuilder()
.header("Authorization", "Bearer " + accessToken)
.build();
return chain.proceed(authenticatedRequest);
}
private String getCurrentUserAccessToken() {
// Implementation would get token from security context or session
// This is a simplified example
return "current_user_access_token";
}
}
}
Entity Services
1. Invoice Service
@Service
@Slf4j
public class QuickBooksInvoiceService {
private final QuickBooksApiClient apiClient;
public QuickBooksInvoiceService(QuickBooksApiClient apiClient) {
this.apiClient = apiClient;
}
public Invoice createInvoice(String realmId, Invoice invoice) throws QuickBooksApiException {
return apiClient.post(realmId, "/invoice", invoice, Invoice.class);
}
public Invoice getInvoice(String realmId, String invoiceId) throws QuickBooksApiException {
return apiClient.get(realmId, "/invoice/" + invoiceId, Invoice.class);
}
public Invoice updateInvoice(String realmId, Invoice invoice) throws QuickBooksApiException {
invoice.setSparse(true); // Sparse update - only send changed fields
return apiClient.post(realmId, "/invoice", invoice, Invoice.class);
}
public void deleteInvoice(String realmId, String invoiceId) throws QuickBooksApiException {
Invoice invoice = new Invoice();
invoice.setId(invoiceId);
invoice.setSyncToken("0"); // Required for delete
invoice.setActive(false); // Soft delete
apiClient.post(realmId, "/invoice", invoice, Invoice.class);
}
public List<Invoice> findInvoicesByCustomer(String realmId, String customerId) throws QuickBooksApiException {
String query = "SELECT * FROM Invoice WHERE CustomerRef = '" + customerId + "'";
return apiClient.query(realmId, query, Invoice.class);
}
public List<Invoice> findInvoicesByDateRange(String realmId, LocalDate startDate, LocalDate endDate) throws QuickBooksApiException {
String query = String.format(
"SELECT * FROM Invoice WHERE TxnDate >= '%s' AND TxnDate <= '%s'",
startDate, endDate
);
return apiClient.query(realmId, query, Invoice.class);
}
public List<Invoice> findOverdueInvoices(String realmId) throws QuickBooksApiException {
String query = "SELECT * FROM Invoice WHERE Balance > '0' AND DueDate < '" + LocalDate.now() + "'";
return apiClient.query(realmId, query, Invoice.class);
}
public Invoice sendInvoice(String realmId, String invoiceId) throws QuickBooksApiException {
Invoice invoice = getInvoice(realmId, invoiceId);
invoice.setEmailStatus("EmailSent");
return updateInvoice(realmId, invoice);
}
public byte[] downloadInvoicePdf(String realmId, String invoiceId) throws QuickBooksApiException {
try {
String url = config.getApiUrl(realmId) + "/invoice/" + invoiceId + "/pdf?minorversion=" + config.getMinorVersion();
Request request = new Request.Builder()
.url(url)
.header("Authorization", "Bearer " + getCurrentUserAccessToken())
.header("Accept", "application/pdf")
.get()
.build();
Response response = apiClient.getHttpClient().newCall(request).execute();
if (!response.isSuccessful()) {
throw new QuickBooksApiException("Failed to download PDF: " + response.code());
}
return response.body().bytes();
} catch (Exception e) {
throw new QuickBooksApiException("Failed to download invoice PDF", e);
}
}
}
2. Customer Service
@Service
@Slf4j
public class QuickBooksCustomerService {
private final QuickBooksApiClient apiClient;
public QuickBooksCustomerService(QuickBooksApiClient apiClient) {
this.apiClient = apiClient;
}
public Customer createCustomer(String realmId, Customer customer) throws QuickBooksApiException {
return apiClient.post(realmId, "/customer", customer, Customer.class);
}
public Customer getCustomer(String realmId, String customerId) throws QuickBooksApiException {
return apiClient.get(realmId, "/customer/" + customerId, Customer.class);
}
public Customer updateCustomer(String realmId, Customer customer) throws QuickBooksApiException {
customer.setSparse(true);
return apiClient.post(realmId, "/customer", customer, Customer.class);
}
public List<Customer> getAllCustomers(String realmId) throws QuickBooksApiException {
String query = "SELECT * FROM Customer MAXRESULTS 1000";
return apiClient.query(realmId, query, Customer.class);
}
public List<Customer> findCustomersByName(String realmId, String name) throws QuickBooksApiException {
String query = "SELECT * FROM Customer WHERE DisplayName LIKE '%" + name + "%'";
return apiClient.query(realmId, query, Customer.class);
}
public List<Customer> findActiveCustomers(String realmId) throws QuickBooksApiException {
String query = "SELECT * FROM Customer WHERE Active = true";
return apiClient.query(realmId, query, Customer.class);
}
public CustomerBalance getCustomerBalance(String realmId, String customerId) throws QuickBooksApiException {
String query = "SELECT Balance FROM Customer WHERE Id = '" + customerId + "'";
List<Customer> customers = apiClient.query(realmId, query, Customer.class);
return customers.isEmpty() ? null : new CustomerBalance(customerId, customers.get(0).getBalance());
}
}
3. Payment Service
@Service
@Slf4j
public class QuickBooksPaymentService {
private final QuickBooksApiClient apiClient;
public QuickBooksPaymentService(QuickBooksApiClient apiClient) {
this.apiClient = apiClient;
}
public Payment createPayment(String realmId, Payment payment) throws QuickBooksApiException {
return apiClient.post(realmId, "/payment", payment, Payment.class);
}
public List<Payment> getPaymentsForInvoice(String realmId, String invoiceId) throws QuickBooksApiException {
String query = "SELECT * FROM Payment WHERE Line.DetailType = 'InvoiceLineDetail' AND Line.LinkedTxn.TxnId = '" + invoiceId + "'";
return apiClient.query(realmId, query, Payment.class);
}
public List<Payment> getPaymentsByDateRange(String realmId, LocalDate startDate, LocalDate endDate) throws QuickBooksApiException {
String query = String.format(
"SELECT * FROM Payment WHERE TxnDate >= '%s' AND TxnDate <= '%s'",
startDate, endDate
);
return apiClient.query(realmId, query, Payment.class);
}
public Payment applyPaymentToInvoice(String realmId, String invoiceId, BigDecimal amount,
String paymentMethod, LocalDate paymentDate) throws QuickBooksApiException {
Payment payment = new Payment();
payment.setTxnDate(paymentDate.toString());
payment.setTotalAmt(amount);
// Set customer reference (you'd need to get this from the invoice)
Invoice invoice = apiClient.get(realmId, "/invoice/" + invoiceId, Invoice.class);
payment.setCustomerRef(invoice.getCustomerRef());
// Create payment line
Line paymentLine = new Line();
paymentLine.setAmount(amount);
LinkedTxn linkedTxn = new LinkedTxn();
linkedTxn.setTxnId(invoiceId);
linkedTxn.setTxnType("Invoice");
paymentLine.setLinkedTxn(new LinkedTxn[]{linkedTxn});
payment.setLine(new Line[]{paymentLine});
return createPayment(realmId, payment);
}
}
Data Models
1. Core QuickBooks Entities
@Data
public class Invoice {
private String Id;
private String SyncToken;
private MetaData MetaData;
private List<Line> Line;
private ReferenceType CustomerRef;
private String TxnDate;
private String DueDate;
private BigDecimal TotalAmt;
private BigDecimal Balance;
private String EmailStatus;
private Boolean sparse;
private Boolean AllowIPNPayment;
private Boolean AllowOnlinePayment;
private Boolean AllowOnlineCreditCardPayment;
private Boolean AllowOnlineACHPayment;
@Data
public static class Line {
private String Id;
private Integer LineNum;
private String Description;
private BigDecimal Amount;
private String DetailType;
private SalesItemLineDetail SalesItemLineDetail;
}
@Data
public static class SalesItemLineDetail {
private ReferenceType ItemRef;
private BigDecimal Qty;
private ReferenceType TaxCodeRef;
}
}
@Data
public class Customer {
private String Id;
private String SyncToken;
private String DisplayName;
private String CompanyName;
private String GivenName;
private String FamilyName;
private Boolean Active;
private BigDecimal Balance;
private BigDecimal TotalExpense;
private PhysicalAddress BillAddr;
private PhysicalAddress ShipAddr;
private String PrimaryEmailAddr;
private String PrimaryPhone;
private Boolean sparse;
@Data
public static class PhysicalAddress {
private String Id;
private String Line1;
private String City;
private String CountrySubDivisionCode;
private String PostalCode;
private String Lat;
private String Long;
}
}
@Data
public class Payment {
private String Id;
private String SyncToken;
private ReferenceType CustomerRef;
private String TxnDate;
private BigDecimal TotalAmt;
private BigDecimal UnappliedAmt;
private ReferenceType PaymentMethodRef;
private Line[] Line;
private String PrivateNote;
private Boolean sparse;
@Data
public static class Line {
private BigDecimal Amount;
private LinkedTxn[] LinkedTxn;
}
@Data
public static class LinkedTxn {
private String TxnId;
private String TxnType;
}
}
@Data
public class ReferenceType {
private String value; // ID value
private String name; // Display name
}
@Data
public class MetaData {
private String CreateTime;
private String LastUpdatedTime;
}
// Response wrappers
@Data
public class QueryResponse {
private Map<String, Object> QueryResponse;
@SuppressWarnings("unchecked")
public <T> List<T> get(String entityName) {
Object entities = QueryResponse.get(entityName);
if (entities instanceof List) {
return (List<T>) entities;
}
return Collections.emptyList();
}
}
@Data
public class BatchRequest {
private BatchItem[] BatchItemRequest;
}
@Data
public class BatchItem {
private String bId;
private String operation;
private Object body;
}
@Data
public class BatchResponse {
private BatchItemResponse[] BatchItemResponse;
}
@Data
public class BatchItemResponse {
private String bId;
private Object body;
private Fault fault;
}
@Data
public class QuickBooksError {
private Fault fault;
@Data
public static class Fault {
private Error[] error;
private String type;
}
@Data
public static class Error {
private String message;
private String detail;
private String code;
}
}
REST Controller
1. QuickBooks Integration API
@RestController
@RequestMapping("/api/quickbooks")
@Slf4j
public class QuickBooksController {
private final QuickBooksInvoiceService invoiceService;
private final QuickBooksCustomerService customerService;
private final QuickBooksPaymentService paymentService;
private final QuickBooksOAuthService oauthService;
public QuickBooksController(QuickBooksInvoiceService invoiceService,
QuickBooksCustomerService customerService,
QuickBooksPaymentService paymentService,
QuickBooksOAuthService oauthService) {
this.invoiceService = invoiceService;
this.customerService = customerService;
this.paymentService = paymentService;
this.oauthService = oauthService;
}
@GetMapping("/auth/url")
public ResponseEntity<Map<String, String>> getAuthUrl() {
String state = UUID.randomUUID().toString();
String authUrl = oauthService.getAuthorizationUrl(state);
return ResponseEntity.ok(Map.of(
"authorizationUrl", authUrl,
"state", state
));
}
@PostMapping("/auth/callback")
public ResponseEntity<OAuthTokens> handleAuthCallback(@RequestBody AuthCallbackRequest request) {
try {
OAuthTokens tokens = oauthService.exchangeCodeForTokens(request.getCode());
return ResponseEntity.ok(tokens);
} catch (OAuthException e) {
log.error("OAuth callback failed", e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
}
// Invoice endpoints
@PostMapping("/{realmId}/invoices")
public ResponseEntity<Invoice> createInvoice(@PathVariable String realmId,
@RequestBody Invoice invoice) {
try {
Invoice created = invoiceService.createInvoice(realmId, invoice);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
} catch (QuickBooksApiException e) {
log.error("Failed to create invoice", e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
}
@GetMapping("/{realmId}/invoices/{invoiceId}")
public ResponseEntity<Invoice> getInvoice(@PathVariable String realmId,
@PathVariable String invoiceId) {
try {
Invoice invoice = invoiceService.getInvoice(realmId, invoiceId);
return ResponseEntity.ok(invoice);
} catch (QuickBooksApiException e) {
log.error("Failed to get invoice: {}", invoiceId, e);
return ResponseEntity.notFound().build();
}
}
@GetMapping("/{realmId}/invoices")
public ResponseEntity<List<Invoice>> getInvoicesByCustomer(
@PathVariable String realmId,
@RequestParam String customerId) {
try {
List<Invoice> invoices = invoiceService.findInvoicesByCustomer(realmId, customerId);
return ResponseEntity.ok(invoices);
} catch (QuickBooksApiException e) {
log.error("Failed to get invoices for customer: {}", customerId, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping("/{realmId}/invoices/overdue")
public ResponseEntity<List<Invoice>> getOverdueInvoices(@PathVariable String realmId) {
try {
List<Invoice> invoices = invoiceService.findOverdueInvoices(realmId);
return ResponseEntity.ok(invoices);
} catch (QuickBooksApiException e) {
log.error("Failed to get overdue invoices", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
// Customer endpoints
@GetMapping("/{realmId}/customers")
public ResponseEntity<List<Customer>> getAllCustomers(@PathVariable String realmId) {
try {
List<Customer> customers = customerService.getAllCustomers(realmId);
return ResponseEntity.ok(customers);
} catch (QuickBooksApiException e) {
log.error("Failed to get customers", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@PostMapping("/{realmId}/customers")
public ResponseEntity<Customer> createCustomer(@PathVariable String realmId,
@RequestBody Customer customer) {
try {
Customer created = customerService.createCustomer(realmId, customer);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
} catch (QuickBooksApiException e) {
log.error("Failed to create customer", e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
}
// Payment endpoints
@PostMapping("/{realmId}/payments")
public ResponseEntity<Payment> createPayment(@PathVariable String realmId,
@RequestBody Payment payment) {
try {
Payment created = paymentService.createPayment(realmId, payment);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
} catch (QuickBooksApiException e) {
log.error("Failed to create payment", e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
}
@PostMapping("/{realmId}/payments/apply-to-invoice")
public ResponseEntity<Payment> applyPaymentToInvoice(
@PathVariable String realmId,
@RequestBody ApplyPaymentRequest request) {
try {
Payment payment = paymentService.applyPaymentToInvoice(
realmId,
request.getInvoiceId(),
request.getAmount(),
request.getPaymentMethod(),
request.getPaymentDate()
);
return ResponseEntity.ok(payment);
} catch (QuickBooksApiException e) {
log.error("Failed to apply payment to invoice: {}", request.getInvoiceId(), e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
}
@Data
public static class AuthCallbackRequest {
private String code;
private String state;
private String realmId;
}
@Data
public static class ApplyPaymentRequest {
private String invoiceId;
private BigDecimal amount;
private String paymentMethod;
private LocalDate paymentDate;
}
}
Error Handling
1. Custom Exceptions
public class QuickBooksApiException extends Exception {
private final Integer statusCode;
public QuickBooksApiException(String message) {
super(message);
this.statusCode = null;
}
public QuickBooksApiException(String message, Integer statusCode) {
super(message);
this.statusCode = statusCode;
}
public QuickBooksApiException(String message, Throwable cause) {
super(message, cause);
this.statusCode = null;
}
public QuickBooksApiException(String message, Integer statusCode, Throwable cause) {
super(message, cause);
this.statusCode = statusCode;
}
public Integer getStatusCode() {
return statusCode;
}
}
public class OAuthException extends Exception {
public OAuthException(String message) {
super(message);
}
public OAuthException(String message, Throwable cause) {
super(message, cause);
}
}
@ControllerAdvice
public class QuickBooksExceptionHandler {
@ExceptionHandler(QuickBooksApiException.class)
public ResponseEntity<ErrorResponse> handleQuickBooksApiException(QuickBooksApiException e) {
log.error("QuickBooks API error", e);
ErrorResponse error = new ErrorResponse(
"QUICKBOOKS_API_ERROR",
e.getMessage(),
e.getStatusCode(),
LocalDateTime.now()
);
HttpStatus status = e.getStatusCode() != null ?
HttpStatus.valueOf(e.getStatusCode()) : HttpStatus.INTERNAL_SERVER_ERROR;
return ResponseEntity.status(status).body(error);
}
@ExceptionHandler(OAuthException.class)
public ResponseEntity<ErrorResponse> handleOAuthException(OAuthException e) {
log.error("QuickBooks OAuth error", e);
ErrorResponse error = new ErrorResponse(
"QUICKBOOKS_OAUTH_ERROR",
"Authentication failed",
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
}
@Data
public static class ErrorResponse {
private final String error;
private final String message;
private final Integer statusCode;
private final LocalDateTime timestamp;
public ErrorResponse(String error, String message, LocalDateTime timestamp) {
this(error, message, null, timestamp);
}
}
}
Configuration
1. Application Properties
# application.yml
quickbooks:
client-id: ${QUICKBOOKS_CLIENT_ID}
client-secret: ${QUICKBOOKS_CLIENT_SECRET}
redirect-uri: ${QUICKBOOKS_REDIRECT_URI}
environment: sandbox # or production
minor-version: 65
Best Practices
- Token Management - Implement robust token refresh logic
- Error Handling - Handle QuickBooks-specific error codes and rate limits
- Idempotency - Use Idempotency-Key header for retry safety
- Batch Operations - Use batch API for multiple operations
- Webhooks - Implement webhook handlers for real-time updates
- Data Sync - Implement delta queries for efficient data synchronization
- Security - Secure OAuth tokens and validate webhook signatures
Conclusion
Integrating QuickBooks Online API with Java applications enables powerful financial automation and data synchronization capabilities. By implementing the patterns shown here, Java developers can:
- Automate Accounting - Streamline financial operations and reduce manual data entry
- Sync Business Data - Keep customer, invoice, and payment data synchronized
- Generate Reports - Extract financial data for custom reporting and analytics
- Enable Payments - Integrate with QuickBooks payment processing
- Scale Operations - Handle multiple companies and high transaction volumes
The combination of robust OAuth handling, comprehensive entity services, and proper error management creates an enterprise-ready QuickBooks integration that can power sophisticated financial applications.