Zoho Books is a comprehensive accounting software that helps businesses manage their finances, automate workflows, and stay compliant. This guide will show you how to integrate Zoho Books API into your Java application to automate accounting tasks, sync financial data, and build powerful business applications.
Why Integrate with Zoho Books?
- Automated Accounting: Sync invoices, expenses, and payments automatically
- Real-time Financial Data: Access up-to-date financial information
- Workflow Automation: Streamline business processes between your app and accounting
- Multi-currency Support: Handle international business transactions
- Compliance: Ensure tax compliance across different regions
Prerequisites
Before you begin, ensure you have:
- Zoho Books Account: Sign up for Zoho Books
- Registered Application: Create a Zoho API client in the Zoho Developer Console
- API Credentials: Obtain
Client ID,Client Secret, and configure redirect URIs - Java Project: Maven or Gradle-based project
Step 1: Add Dependencies
Add necessary dependencies for HTTP client and JSON processing.
For Maven (pom.xml):
<dependencies> <!-- HTTP Client --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.14</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.15.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.7</version> </dependency> </dependencies>
Step 2: Configure Zoho Books Client
Create a configuration class to handle OAuth authentication and API client setup.
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.client.methods.*;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.util.*;
public class ZohoBooksConfig {
private static final String AUTH_BASE_URL = "https://accounts.zoho.com";
private static final String API_BASE_URL = "https://www.zohoapis.com/books/v3";
private final String clientId;
private final String clientSecret;
private final String refreshToken;
private final String organizationId;
private final CloseableHttpClient httpClient;
private final ObjectMapper objectMapper;
public ZohoBooksConfig(String clientId, String clientSecret, String refreshToken, String organizationId) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.refreshToken = refreshToken;
this.organizationId = organizationId;
this.httpClient = HttpClients.createDefault();
this.objectMapper = new ObjectMapper();
}
private String getAccessToken() throws ZohoBooksException {
try {
String url = AUTH_BASE_URL + "/oauth/v2/token";
HttpPost request = new HttpPost(url);
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("client_id", clientId));
params.add(new BasicNameValuePair("client_secret", clientSecret));
params.add(new BasicNameValuePair("refresh_token", refreshToken));
params.add(new BasicNameValuePair("grant_type", "refresh_token"));
request.setEntity(new UrlEncodedFormEntity(params));
try (CloseableHttpResponse response = httpClient.execute(request)) {
String jsonResponse = EntityUtils.toString(response.getEntity());
Map<String, Object> result = objectMapper.readValue(jsonResponse, Map.class);
if (result.containsKey("access_token")) {
return (String) result.get("access_token");
} else {
throw new ZohoBooksException("Failed to get access token: " + jsonResponse);
}
}
} catch (Exception e) {
throw new ZohoBooksException("Error obtaining access token", e);
}
}
public CloseableHttpResponse executeRequest(HttpRequestBase request) throws ZohoBooksException {
try {
String accessToken = getAccessToken();
request.setHeader("Authorization", "Zoho-oauthtoken " + accessToken);
request.setHeader("X-com-zoho-books-organizationid", organizationId);
request.setHeader("Content-Type", "application/json");
return httpClient.execute(request);
} catch (Exception e) {
throw new ZohoBooksException("Error executing request", e);
}
}
}
Step 3: Create Service Classes
Invoice Service
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.client.methods.*;
import org.apache.http.entity.StringEntity;
import org.apache.http.util.EntityUtils;
import java.util.*;
@Service
public class InvoiceService {
private final ZohoBooksConfig config;
private final ObjectMapper objectMapper;
public InvoiceService(ZohoBooksConfig config) {
this.config = config;
this.objectMapper = new ObjectMapper();
}
public Map<String, Object> createInvoice(InvoiceRequest invoiceRequest) throws ZohoBooksException {
try {
String url = config.getApiBaseUrl() + "/invoices";
HttpPost request = new HttpPost(url);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("customer_id", invoiceRequest.getCustomerId());
requestBody.put("invoice_number", invoiceRequest.getInvoiceNumber());
requestBody.put("date", invoiceRequest.getDate());
requestBody.put("due_date", invoiceRequest.getDueDate());
requestBody.put("line_items", invoiceRequest.getLineItems());
requestBody.put("currency_code", invoiceRequest.getCurrencyCode());
String jsonBody = objectMapper.writeValueAsString(requestBody);
request.setEntity(new StringEntity(jsonBody));
try (CloseableHttpResponse response = config.executeRequest(request)) {
String jsonResponse = EntityUtils.toString(response.getEntity());
return objectMapper.readValue(jsonResponse, Map.class);
}
} catch (Exception e) {
throw new ZohoBooksException("Error creating invoice", e);
}
}
public Map<String, Object> getInvoice(String invoiceId) throws ZohoBooksException {
try {
String url = config.getApiBaseUrl() + "/invoices/" + invoiceId;
HttpGet request = new HttpGet(url);
try (CloseableHttpResponse response = config.executeRequest(request)) {
String jsonResponse = EntityUtils.toString(response.getEntity());
return objectMapper.readValue(jsonResponse, Map.class);
}
} catch (Exception e) {
throw new ZohoBooksException("Error retrieving invoice", e);
}
}
public Map<String, Object> listInvoices(Map<String, String> filters) throws ZohoBooksException {
try {
String url = config.getApiBaseUrl() + "/invoices";
if (filters != null && !filters.isEmpty()) {
StringBuilder queryString = new StringBuilder("?");
for (Map.Entry<String, String> entry : filters.entrySet()) {
queryString.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
url += queryString.toString();
}
HttpGet request = new HttpGet(url);
try (CloseableHttpResponse response = config.executeRequest(request)) {
String jsonResponse = EntityUtils.toString(response.getEntity());
return objectMapper.readValue(jsonResponse, Map.class);
}
} catch (Exception e) {
throw new ZohoBooksException("Error listing invoices", e);
}
}
public Map<String, Object> markAsSent(String invoiceId) throws ZohoBooksException {
try {
String url = config.getApiBaseUrl() + "/invoices/" + invoiceId + "/status/sent";
HttpPost request = new HttpPost(url);
try (CloseableHttpResponse response = config.executeRequest(request)) {
String jsonResponse = EntityUtils.toString(response.getEntity());
return objectMapper.readValue(jsonResponse, Map.class);
}
} catch (Exception e) {
throw new ZohoBooksException("Error marking invoice as sent", e);
}
}
}
Customer Service
@Service
public class CustomerService {
private final ZohoBooksConfig config;
private final ObjectMapper objectMapper;
public CustomerService(ZohoBooksConfig config) {
this.config = config;
this.objectMapper = new ObjectMapper();
}
public Map<String, Object> createCustomer(CustomerRequest customerRequest) throws ZohoBooksException {
try {
String url = config.getApiBaseUrl() + "/customers";
HttpPost request = new HttpPost(url);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("customer_name", customerRequest.getCustomerName());
requestBody.put("email", customerRequest.getEmail());
requestBody.put("company_name", customerRequest.getCompanyName());
requestBody.put("billing_address", customerRequest.getBillingAddress());
requestBody.put("shipping_address", customerRequest.getShippingAddress());
requestBody.put("currency_code", customerRequest.getCurrencyCode());
String jsonBody = objectMapper.writeValueAsString(requestBody);
request.setEntity(new StringEntity(jsonBody));
try (CloseableHttpResponse response = config.executeRequest(request)) {
String jsonResponse = EntityUtils.toString(response.getEntity());
return objectMapper.readValue(jsonResponse, Map.class);
}
} catch (Exception e) {
throw new ZohoBooksException("Error creating customer", e);
}
}
public Map<String, Object> listCustomers() throws ZohoBooksException {
try {
String url = config.getApiBaseUrl() + "/customers";
HttpGet request = new HttpGet(url);
try (CloseableHttpResponse response = config.executeRequest(request)) {
String jsonResponse = EntityUtils.toString(response.getEntity());
return objectMapper.readValue(jsonResponse, Map.class);
}
} catch (Exception e) {
throw new ZohoBooksException("Error listing customers", e);
}
}
}
Step 4: Create Request DTOs
@Data
public class InvoiceRequest {
private String customerId;
private String invoiceNumber;
private String date; // YYYY-MM-DD
private String dueDate; // YYYY-MM-DD
private List<LineItem> lineItems;
private String currencyCode = "USD";
@Data
public static class LineItem {
private String itemId;
private String name;
private String description;
private BigDecimal rate;
private Integer quantity;
private BigDecimal taxPercentage;
}
}
@Data
public class CustomerRequest {
private String customerName;
private String email;
private String companyName;
private Address billingAddress;
private Address shippingAddress;
private String currencyCode = "USD";
@Data
public static class Address {
private String address;
private String city;
private String state;
private String zip;
private String country;
}
}
Step 5: Create REST Controllers
@RestController
@RequestMapping("/api/zohobooks")
public class ZohoBooksController {
@Autowired
private InvoiceService invoiceService;
@Autowired
private CustomerService customerService;
@PostMapping("/invoices")
public ResponseEntity<?> createInvoice(@RequestBody InvoiceRequest invoiceRequest) {
try {
Map<String, Object> response = invoiceService.createInvoice(invoiceRequest);
return ResponseEntity.ok(response);
} catch (ZohoBooksException e) {
return ResponseEntity.badRequest().body(
new ErrorResponse("INVOICE_CREATION_ERROR", e.getMessage())
);
}
}
@GetMapping("/invoices/{invoiceId}")
public ResponseEntity<?> getInvoice(@PathVariable String invoiceId) {
try {
Map<String, Object> response = invoiceService.getInvoice(invoiceId);
return ResponseEntity.ok(response);
} catch (ZohoBooksException e) {
return ResponseEntity.badRequest().body(
new ErrorResponse("INVOICE_RETRIEVAL_ERROR", e.getMessage())
);
}
}
@GetMapping("/invoices")
public ResponseEntity<?> listInvoices(
@RequestParam(required = false) String status,
@RequestParam(required = false) String date) {
try {
Map<String, String> filters = new HashMap<>();
if (status != null) filters.put("status", status);
if (date != null) filters.put("date", date);
Map<String, Object> response = invoiceService.listInvoices(filters);
return ResponseEntity.ok(response);
} catch (ZohoBooksException e) {
return ResponseEntity.badRequest().body(
new ErrorResponse("INVOICE_LIST_ERROR", e.getMessage())
);
}
}
@PostMapping("/customers")
public ResponseEntity<?> createCustomer(@RequestBody CustomerRequest customerRequest) {
try {
Map<String, Object> response = customerService.createCustomer(customerRequest);
return ResponseEntity.ok(response);
} catch (ZohoBooksException e) {
return ResponseEntity.badRequest().body(
new ErrorResponse("CUSTOMER_CREATION_ERROR", e.getMessage())
);
}
}
}
Step 6: Exception Handling
public class ZohoBooksException extends Exception {
public ZohoBooksException(String message) {
super(message);
}
public ZohoBooksException(String message, Throwable cause) {
super(message, cause);
}
}
@Data
@AllArgsConstructor
public class ErrorResponse {
private String errorCode;
private String message;
private Timestamp timestamp;
public ErrorResponse(String errorCode, String message) {
this.errorCode = errorCode;
this.message = message;
this.timestamp = new Timestamp(System.currentTimeMillis());
}
}
@ControllerAdvice
public class ZohoBooksExceptionHandler {
@ExceptionHandler(ZohoBooksException.class)
public ResponseEntity<ErrorResponse> handleZohoBooksException(ZohoBooksException ex) {
ErrorResponse error = new ErrorResponse("ZOHO_BOOKS_ERROR", ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
Step 7: Configuration Properties
application.yml:
zoho:
books:
client-id: ${ZOHO_CLIENT_ID}
client-secret: ${ZOHO_CLIENT_SECRET}
refresh-token: ${ZOHO_REFRESH_TOKEN}
organization-id: ${ZOHO_ORGANIZATION_ID}
base-url: https://www.zohoapis.com/books/v3
Key Zoho Books API Concepts
- OAuth 2.0: Uses refresh token flow for server-to-server authentication
- Organization ID: Required for multi-organization accounts
- Modules: Invoices, Customers, Items, Expenses, Payments, etc.
- Pagination: Support for paginated results with
pageandper_pageparameters - Webhooks: Real-time notifications for data changes
Best Practices
- Token Management: Implement token refresh logic and store tokens securely
- Error Handling: Handle API rate limits and temporary failures with retry logic
- Data Validation: Validate data before sending to Zoho Books API
- Batching: Use batch operations for bulk data synchronization
- Webhook Security: Validate webhook signatures for incoming requests
- Logging: Log all API interactions for debugging and audit purposes
Example Usage
// Create a customer
CustomerRequest customer = new CustomerRequest();
customer.setCustomerName("John Doe");
customer.setEmail("[email protected]");
customer.setCompanyName("Doe Enterprises");
CustomerRequest.Address address = new CustomerRequest.Address();
address.setAddress("123 Main St");
address.setCity("New York");
address.setState("NY");
address.setZip("10001");
address.setCountry("USA");
customer.setBillingAddress(address);
customer.setShippingAddress(address);
Map<String, Object> customerResponse = customerService.createCustomer(customer);
String customerId = (String) ((Map<String, Object>) customerResponse.get("customer")).get("customer_id");
// Create an invoice for the customer
InvoiceRequest invoice = new InvoiceRequest();
invoice.setCustomerId(customerId);
invoice.setInvoiceNumber("INV-001");
invoice.setDate("2023-12-01");
invoice.setDueDate("2023-12-31");
List<InvoiceRequest.LineItem> lineItems = new ArrayList<>();
InvoiceRequest.LineItem item = new InvoiceRequest.LineItem();
item.setName("Consulting Services");
item.setDescription("Professional consulting services");
item.setRate(new BigDecimal("150.00"));
item.setQuantity(10);
lineItems.add(item);
invoice.setLineItems(lineItems);
Map<String, Object> invoiceResponse = invoiceService.createInvoice(invoice);
Conclusion
Integrating Zoho Books API with your Java application enables seamless synchronization of financial data and automates accounting workflows. By following this guide, you can implement robust integration that handles invoices, customers, items, and other accounting entities efficiently.
Remember to thoroughly test your integration with Zoho Books sandbox environment before moving to production, and implement proper error handling and logging to ensure reliability in production environments.