FreshBooks Classic API in Java: Legacy Accounting Integration

FreshBooks Classic API provides a comprehensive interface for integrating with the legacy version of FreshBooks' accounting platform. While FreshBooks has moved to a newer API, many businesses still rely on the Classic API for their existing integrations. This guide covers practical patterns for connecting Java applications with FreshBooks Classic, handling authentication, and performing essential accounting operations.

Understanding FreshBooks Classic API Architecture

FreshBooks Classic API uses:

  • XML-RPC protocol over HTTPS
  • Token-based authentication with API tokens
  • Structured XML requests and responses
  • Domain-specific methods for invoices, clients, expenses

Core Integration Patterns

1. Project Setup and Dependencies

Configure XML-RPC client dependencies for FreshBooks integration.

Maven Configuration:

<dependencies>
<!-- XML-RPC Client -->
<dependency>
<groupId>org.apache.xmlrpc</groupId>
<artifactId>xmlrpc-client</artifactId>
<version>3.1.3</version>
</dependency>
<!-- XML Processing -->
<dependency>
<groupId>org.apache.xmlrpc</groupId>
<artifactId>xmlrpc-common</artifactId>
<version>3.1.3</version>
</dependency>
<!-- HTTP Components -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
</dependencies>

2. Configuration and Authentication

Set up secure authentication and client configuration.

FreshBooks Configuration:

@Configuration
@ConfigurationProperties(prefix = "freshbooks.classic")
@Data
public class FreshBooksConfig {
private String apiUrl;
private String apiToken;
private String accountUrl;
private int timeout = 30000;
@Bean
public XmlRpcClientConfig freshBooksClientConfig() {
XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl();
config.setServerURL(buildApiUrl());
config.setConnectionTimeout(timeout);
config.setReplyTimeout(timeout);
return config;
}
private URL buildApiUrl() {
try {
String fullUrl = String.format("%s/%s", apiUrl, accountUrl);
return new URL(fullUrl);
} catch (MalformedURLException e) {
throw new RuntimeException("Invalid FreshBooks API URL", e);
}
}
}
@Component
public class FreshBooksAuthenticator {
private final FreshBooksConfig config;
public FreshBooksAuthenticator(FreshBooksConfig config) {
this.config = config;
}
public Object[] buildAuthParams() {
Map<String, String> auth = new HashMap<>();
auth.put("Authentication", "token");
auth.put("API_Token", config.getApiToken());
return new Object[]{auth};
}
}

3. Base XML-RPC Client

Create a robust client for FreshBooks API calls.

Base Client Implementation:

@Service
@Slf4j
public class FreshBooksClient {
private final XmlRpcClientConfig config;
private final FreshBooksAuthenticator authenticator;
public FreshBooksClient(XmlRpcClientConfig config, FreshBooksAuthenticator authenticator) {
this.config = config;
this.authenticator = authenticator;
}
public Object executeMethod(String method, Object[] params) throws FreshBooksException {
try {
XmlRpcClient client = new XmlRpcClient();
client.setConfig(config);
Object[] allParams = combineParams(authenticator.buildAuthParams(), params);
return client.execute(method, allParams);
} catch (XmlRpcException e) {
log.error("FreshBooks API call failed for method: {}", method, e);
throw new FreshBooksException("API call failed: " + e.getMessage(), e);
}
}
@SuppressWarnings("unchecked")
public Map<String, Object> executeMethodWithMapResponse(String method, Object[] params) 
throws FreshBooksException {
Object result = executeMethod(method, params);
if (result instanceof Map) {
return (Map<String, Object>) result;
}
throw new FreshBooksException("Unexpected response type for method: " + method);
}
private Object[] combineParams(Object[] authParams, Object[] methodParams) {
if (methodParams == null || methodParams.length == 0) {
return authParams;
}
Object[] combined = new Object[authParams.length + methodParams.length];
System.arraycopy(authParams, 0, combined, 0, authParams.length);
System.arraycopy(methodParams, 0, combined, authParams.length, methodParams.length);
return combined;
}
}

4. Domain Models for FreshBooks Entities

Create Java models that map to FreshBooks entities.

Core Domain Models:

@Data
public class FreshBooksClient {
private String clientId;
private String firstName;
private String lastName;
private String organization;
private String email;
private String username;
private String workPhone;
private String homePhone;
private String mobile;
private String fax;
private String language;
private String currencyCode;
private String notes;
private String status;
private FreshBooksAddress address;
@Data
public static class FreshBooksAddress {
private String street1;
private String street2;
private String city;
private String state;
private String country;
private String postalCode;
}
}
@Data
public class FreshBooksInvoice {
private String invoiceId;
private String clientId;
private String number;
private String status;
private Date date;
private Date updated;
private BigDecimal amount;
private BigDecimal amountOutstanding;
private String notes;
private String terms;
private String discountDescription;
private BigDecimal discountValue;
private String discountType; // "percentage" or "amount"
private String poNumber;
private List<FreshBooksInvoiceLine> lines;
@Data
public static class FreshBooksInvoiceLine {
private String name;
private String description;
private BigDecimal unitCost;
private Integer quantity;
private String tax1Name;
private BigDecimal tax1Percent;
private String tax2Name;
private BigDecimal tax2Percent;
public BigDecimal getLineTotal() {
BigDecimal total = unitCost.multiply(BigDecimal.valueOf(quantity));
// Apply taxes if needed
return total;
}
}
}
@Data
public class FreshBooksPayment {
private String paymentId;
private String clientId;
private String invoiceId;
private Date date;
private BigDecimal amount;
private String type; // "Credit" "Cash" "Check" etc.
private String notes;
private String status;
}

5. Client Management Service

Manage client operations in FreshBooks.

Client Service:

@Service
@Slf4j
public class FreshBooksClientService {
private final FreshBooksClient apiClient;
private final ObjectMapper objectMapper;
public FreshBooksClientService(FreshBooksClient apiClient) {
this.apiClient = apiClient;
this.objectMapper = new ObjectMapper();
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
public String createClient(FreshBooksClient client) throws FreshBooksException {
try {
Map<String, Object> clientData = convertClientToMap(client);
Object[] params = new Object[]{clientData};
Map<String, Object> response = apiClient.executeMethodWithMapResponse(
"client.create", params);
return (String) response.get("client_id");
} catch (Exception e) {
throw new FreshBooksException("Failed to create client", e);
}
}
public FreshBooksClient getClient(String clientId) throws FreshBooksException {
try {
Object[] params = new Object[]{clientId};
Map<String, Object> response = apiClient.executeMethodWithMapResponse(
"client.get", params);
@SuppressWarnings("unchecked")
Map<String, Object> clientData = (Map<String, Object>) response.get("client");
return convertMapToClient(clientData);
} catch (Exception e) {
throw new FreshBooksException("Failed to get client: " + clientId, e);
}
}
public List<FreshBooksClient> listClients() throws FreshBooksException {
try {
Object[] params = new Object[]{};
Map<String, Object> response = apiClient.executeMethodWithMapResponse(
"client.list", params);
@SuppressWarnings("unchecked")
List<Map<String, Object>> clientsData = 
(List<Map<String, Object>>) response.get("clients");
return clientsData.stream()
.map(this::convertMapToClient)
.collect(Collectors.toList());
} catch (Exception e) {
throw new FreshBooksException("Failed to list clients", e);
}
}
public boolean updateClient(FreshBooksClient client) throws FreshBooksException {
try {
Map<String, Object> clientData = convertClientToMap(client);
Object[] params = new Object[]{client.getClientId(), clientData};
Map<String, Object> response = apiClient.executeMethodWithMapResponse(
"client.update", params);
return "ok".equals(response.get("status"));
} catch (Exception e) {
throw new FreshBooksException("Failed to update client: " + client.getClientId(), e);
}
}
private Map<String, Object> convertClientToMap(FreshBooksClient client) {
Map<String, Object> data = new HashMap<>();
data.put("first_name", client.getFirstName());
data.put("last_name", client.getLastName());
data.put("organization", client.getOrganization());
data.put("email", client.getEmail());
data.put("work_phone", client.getWorkPhone());
data.put("home_phone", client.getHomePhone());
data.put("mobile", client.getMobile());
data.put("notes", client.getNotes());
data.put("currency_code", client.getCurrencyCode());
if (client.getAddress() != null) {
data.put("p_street1", client.getAddress().getStreet1());
data.put("p_street2", client.getAddress().getStreet2());
data.put("p_city", client.getAddress().getCity());
data.put("p_state", client.getAddress().getState());
data.put("p_country", client.getAddress().getCountry());
data.put("p_code", client.getAddress().getPostalCode());
}
return data;
}
private FreshBooksClient convertMapToClient(Map<String, Object> data) {
FreshBooksClient client = new FreshBooksClient();
client.setClientId((String) data.get("client_id"));
client.setFirstName((String) data.get("first_name"));
client.setLastName((String) data.get("last_name"));
client.setOrganization((String) data.get("organization"));
client.setEmail((String) data.get("email"));
client.setWorkPhone((String) data.get("work_phone"));
client.setHomePhone((String) data.get("home_phone"));
client.setMobile((String) data.get("mobile"));
client.setNotes((String) data.get("notes"));
client.setCurrencyCode((String) data.get("currency_code"));
client.setStatus((String) data.get("status"));
FreshBooksAddress address = new FreshBooksAddress();
address.setStreet1((String) data.get("p_street1"));
address.setStreet2((String) data.get("p_street2"));
address.setCity((String) data.get("p_city"));
address.setState((String) data.get("p_state"));
address.setCountry((String) data.get("p_country"));
address.setPostalCode((String) data.get("p_code"));
client.setAddress(address);
return client;
}
}

6. Invoice Management Service

Handle invoice operations including creation and updates.

Invoice Service:

@Service
@Slf4j
public class FreshBooksInvoiceService {
private final FreshBooksClient apiClient;
public FreshBooksInvoiceService(FreshBooksClient apiClient) {
this.apiClient = apiClient;
}
public String createInvoice(FreshBooksInvoice invoice) throws FreshBooksException {
try {
Map<String, Object> invoiceData = convertInvoiceToMap(invoice);
Object[] params = new Object[]{invoiceData};
Map<String, Object> response = apiClient.executeMethodWithMapResponse(
"invoice.create", params);
return (String) response.get("invoice_id");
} catch (Exception e) {
throw new FreshBooksException("Failed to create invoice", e);
}
}
public FreshBooksInvoice getInvoice(String invoiceId) throws FreshBooksException {
try {
Object[] params = new Object[]{invoiceId};
Map<String, Object> response = apiClient.executeMethodWithMapResponse(
"invoice.get", params);
@SuppressWarnings("unchecked")
Map<String, Object> invoiceData = (Map<String, Object>) response.get("invoice");
return convertMapToInvoice(invoiceData);
} catch (Exception e) {
throw new FreshBooksException("Failed to get invoice: " + invoiceId, e);
}
}
public List<FreshBooksInvoice> listInvoices(String clientId, String status) 
throws FreshBooksException {
try {
Map<String, String> filters = new HashMap<>();
if (clientId != null) filters.put("client_id", clientId);
if (status != null) filters.put("status", status);
Object[] params = new Object[]{filters};
Map<String, Object> response = apiClient.executeMethodWithMapResponse(
"invoice.list", params);
@SuppressWarnings("unchecked")
List<Map<String, Object>> invoicesData = 
(List<Map<String, Object>>) response.get("invoices");
return invoicesData.stream()
.map(this::convertMapToInvoice)
.collect(Collectors.toList());
} catch (Exception e) {
throw new FreshBooksException("Failed to list invoices", e);
}
}
public boolean sendInvoiceByEmail(String invoiceId, String subject, String message) 
throws FreshBooksException {
try {
Object[] params = new Object[]{invoiceId, subject, message};
Map<String, Object> response = apiClient.executeMethodWithMapResponse(
"invoice.sendByEmail", params);
return "ok".equals(response.get("status"));
} catch (Exception e) {
throw new FreshBooksException("Failed to send invoice: " + invoiceId, e);
}
}
private Map<String, Object> convertInvoiceToMap(FreshBooksInvoice invoice) {
Map<String, Object> data = new HashMap<>();
data.put("client_id", invoice.getClientId());
data.put("number", invoice.getNumber());
data.put("status", invoice.getStatus());
data.put("date", invoice.getDate());
data.put("notes", invoice.getNotes());
data.put("terms", invoice.getTerms());
data.put("po_number", invoice.getPoNumber());
if (invoice.getLines() != null) {
List<Map<String, Object>> lines = invoice.getLines().stream()
.map(this::convertLineToMap)
.collect(Collectors.toList());
data.put("lines", lines);
}
return data;
}
private Map<String, Object> convertLineToMap(FreshBooksInvoiceLine line) {
Map<String, Object> lineData = new HashMap<>();
lineData.put("name", line.getName());
lineData.put("description", line.getDescription());
lineData.put("unit_cost", line.getUnitCost());
lineData.put("quantity", line.getQuantity());
lineData.put("tax1_name", line.getTax1Name());
lineData.put("tax1_percent", line.getTax1Percent());
lineData.put("tax2_name", line.getTax2Name());
lineData.put("tax2_percent", line.getTax2Percent());
return lineData;
}
private FreshBooksInvoice convertMapToInvoice(Map<String, Object> data) {
FreshBooksInvoice invoice = new FreshBooksInvoice();
invoice.setInvoiceId((String) data.get("invoice_id"));
invoice.setClientId((String) data.get("client_id"));
invoice.setNumber((String) data.get("number"));
invoice.setStatus((String) data.get("status"));
invoice.setDate((Date) data.get("date"));
invoice.setUpdated((Date) data.get("updated"));
Object amount = data.get("amount");
if (amount != null) {
invoice.setAmount(new BigDecimal(amount.toString()));
}
Object amountOutstanding = data.get("amount_outstanding");
if (amountOutstanding != null) {
invoice.setAmountOutstanding(new BigDecimal(amountOutstanding.toString()));
}
return invoice;
}
}

7. Payment Processing Service

Handle payment operations and reconciliation.

Payment Service:

@Service
public class FreshBooksPaymentService {
private final FreshBooksClient apiClient;
public FreshBooksPaymentService(FreshBooksClient apiClient) {
this.apiClient = apiClient;
}
public String createPayment(FreshBooksPayment payment) throws FreshBooksException {
try {
Map<String, Object> paymentData = new HashMap<>();
paymentData.put("client_id", payment.getClientId());
paymentData.put("invoice_id", payment.getInvoiceId());
paymentData.put("date", payment.getDate());
paymentData.put("amount", payment.getAmount());
paymentData.put("type", payment.getType());
paymentData.put("notes", payment.getNotes());
Object[] params = new Object[]{paymentData};
Map<String, Object> response = apiClient.executeMethodWithMapResponse(
"payment.create", params);
return (String) response.get("payment_id");
} catch (Exception e) {
throw new FreshBooksException("Failed to create payment", e);
}
}
public List<FreshBooksPayment> listPayments(String clientId, Date fromDate, Date toDate) 
throws FreshBooksException {
try {
Map<String, Object> filters = new HashMap<>();
if (clientId != null) filters.put("client_id", clientId);
if (fromDate != null) filters.put("date_from", fromDate);
if (toDate != null) filters.put("date_to", toDate);
Object[] params = new Object[]{filters};
Map<String, Object> response = apiClient.executeMethodWithMapResponse(
"payment.list", params);
@SuppressWarnings("unchecked")
List<Map<String, Object>> paymentsData = 
(List<Map<String, Object>>) response.get("payments");
return paymentsData.stream()
.map(this::convertMapToPayment)
.collect(Collectors.toList());
} catch (Exception e) {
throw new FreshBooksException("Failed to list payments", e);
}
}
private FreshBooksPayment convertMapToPayment(Map<String, Object> data) {
FreshBooksPayment payment = new FreshBooksPayment();
payment.setPaymentId((String) data.get("payment_id"));
payment.setClientId((String) data.get("client_id"));
payment.setInvoiceId((String) data.get("invoice_id"));
payment.setDate((Date) data.get("date"));
Object amount = data.get("amount");
if (amount != null) {
payment.setAmount(new BigDecimal(amount.toString()));
}
payment.setType((String) data.get("type"));
payment.setNotes((String) data.get("notes"));
return payment;
}
}

8. Error Handling and Resilience

Implement comprehensive error handling.

Custom Exceptions and Error Handling:

public class FreshBooksException extends Exception {
private final String errorCode;
public FreshBooksException(String message) {
super(message);
this.errorCode = "UNKNOWN";
}
public FreshBooksException(String message, String errorCode) {
super(message);
this.errorCode = errorCode;
}
public FreshBooksException(String message, Throwable cause) {
super(message, cause);
this.errorCode = "UNKNOWN";
}
public String getErrorCode() {
return errorCode;
}
}
@ControllerAdvice
public class FreshBooksExceptionHandler {
@ExceptionHandler(FreshBooksException.class)
public ResponseEntity<ErrorResponse> handleFreshBooksException(FreshBooksException ex) {
ErrorResponse error = new ErrorResponse(
"FRESHBOOKS_ERROR",
ex.getMessage(),
Instant.now()
);
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(error);
}
@ExceptionHandler(XmlRpcException.class)
public ResponseEntity<ErrorResponse> handleXmlRpcException(XmlRpcException ex) {
ErrorResponse error = new ErrorResponse(
"XML_RPC_ERROR",
"FreshBooks communication error: " + ex.getMessage(),
Instant.now()
);
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(error);
}
}

Best Practices for FreshBooks Classic API Integration

  1. Error Handling: Implement robust error handling for network issues and API limits
  2. Rate Limiting: Respect FreshBooks API rate limits with proper backoff strategies
  3. Data Validation: Validate all data before sending to FreshBooks
  4. Logging: Log all API calls for debugging and audit purposes
  5. Caching: Cache frequently accessed data like client lists
  6. Testing: Use FreshBooks sandbox for testing integration
  7. Monitoring: Implement health checks for API connectivity

Migration Considerations

Note: FreshBooks Classic API is a legacy service. Consider migrating to the newer FreshBooks API which offers:

  • RESTful architecture
  • OAuth 2.0 authentication
  • JSON payloads
  • Enhanced features and better performance

Conclusion: Reliable Legacy Integration

While FreshBooks Classic API uses older XML-RPC technology, it remains a reliable option for businesses with established integrations. The Java XML-RPC client provides a solid foundation for interacting with FreshBooks Classic, enabling comprehensive accounting automation while maintaining data consistency and business process integration.

This integration demonstrates that legacy APIs can still provide significant business value when wrapped in modern Java services with proper error handling, logging, and monitoring—ensuring continuity while planning for future migration to newer API versions.

Java Observability, Logging Intelligence & AI-Driven Monitoring (APM, Tracing, Logs & Anomaly Detection)

https://macronepal.com/blog/beyond-metrics-observing-serverless-and-traditional-java-applications-with-thundra-apm/
Explains using Thundra APM to observe both serverless and traditional Java applications by combining tracing, metrics, and logs into a unified observability platform for faster debugging and performance insights.

https://macronepal.com/blog/dynatrace-oneagent-in-java-2/
Explains Dynatrace OneAgent for Java, which automatically instruments JVM applications to capture metrics, traces, and logs, enabling full-stack monitoring and root-cause analysis with minimal configuration.

https://macronepal.com/blog/lightstep-java-sdk-distributed-tracing-and-observability-implementation/
Explains Lightstep Java SDK for distributed tracing, helping developers track requests across microservices and identify latency issues using OpenTelemetry-based observability.

https://macronepal.com/blog/honeycomb-io-beeline-for-java-complete-guide-2/
Explains Honeycomb Beeline for Java, which provides high-cardinality observability and deep query capabilities to understand complex system behavior and debug distributed systems efficiently.

https://macronepal.com/blog/lumigo-for-serverless-in-java-complete-distributed-tracing-guide-2/
Explains Lumigo for Java serverless applications, offering automatic distributed tracing, log correlation, and error tracking to simplify debugging in cloud-native environments. (Lumigo Docs)

https://macronepal.com/blog/from-noise-to-signals-implementing-log-anomaly-detection-in-java-applications/
Explains how to detect anomalies in Java logs using behavioral patterns and machine learning techniques to separate meaningful incidents from noisy log data and improve incident response.

https://macronepal.com/blog/ai-powered-log-analysis-in-java-from-reactive-debugging-to-proactive-insights/
Explains AI-driven log analysis for Java applications, shifting from manual debugging to predictive insights that identify issues early and improve system reliability using intelligent log processing.

https://macronepal.com/blog/titliel-java-logging-best-practices/
Explains best practices for Java logging, focusing on structured logs, proper log levels, performance optimization, and ensuring logs are useful for debugging and observability systems.

https://macronepal.com/blog/seeking-a-loguru-for-java-the-quest-for-elegant-and-simple-logging/
Explains the search for simpler, more elegant logging frameworks in Java, comparing modern logging approaches that aim to reduce complexity while improving readability and developer experience.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper