I wanted to see how much of the integration work Nango could handle from a Java/Spring Boot application, so I built a small multi-tenant accounting backend using Java, Spring Boot, Nango, and QuickBooks Online Sandbox. A customer can connect their QuickBooks account, and the backend can then fetch their company information, customers, and invoices. I also tested the integration with a real QuickBooks Sandbox instead of relying only on mocked responses.
And I also wanted to see what the Java experience would look like. The Nango Java backend SDK documentation currently points developers to the REST API while the SDK is still marked as coming soon, so I built a small Java client using Spring's RestClient.
While building it, I ran into a few things I hadn't expected, like the end_user requirement for Connect Sessions, the QuickBooks OAuth scope, handling the realmId, Nango errors, and webhooks. In this article, I'll walk through what I built and what I learned along the way.
GitHub: https://github.com/abhishek-dhnma/nango-java-quickbooks-integration-demo
1. Prerequisites
Before starting, you'll need:
- Java 17+
- Spring Boot 3.3.4
- Maven
- A Nango Cloud account
- An Intuit Developer account
- A QuickBooks Online Sandbox company
The QuickBooks Sandbox lets us go through the real OAuth flow and make API requests without using a production QuickBooks account.
Once these are ready, we can start with the QuickBooks setup.
2. Create a QuickBooks Developer App
First, create an application in the Intuit Developer portal. This gives us the OAuth credentials and permissions that Nango will use when connecting to QuickBooks.
From Sign In > My Hub > App dashboard, create a new app and select QuickBooks Online and Payments.
For this project, I used the com.intuit.quickbooks.accounting scope because the backend needs access to accounting data such as company information, customers, and invoices.
Once the app is created, open Keys & credentials > Development and copy the Client ID and Client Secret. We'll configure these credentials in Nango in the next section.
We're using Development credentials because the project connects to a QuickBooks Sandbox rather than a production company.
3. Create a QuickBooks Sandbox Company
Next, create a QuickBooks Online Sandbox company from the Intuit Developer portal. The Sandbox gives us a test company that we can use for the OAuth flow and API requests without connecting a production QuickBooks account.
If you already have a Sandbox company, you can skip this step.
We also need the QuickBooks realmId, which identifies the connected company. In the Sandbox, you can find it under Settings > Profile > Subscription and Billing > Company ID. The Company ID is the realmId used by the QuickBooks API.
Note: Nango can retrieve the realmId automatically during the connection flow, so it can also be left blank when configuring the Sandbox connection in Nango. I used the value returned in the Nango connection metadata when building the Spring Boot integration.
Once the Sandbox is ready, we can configure the QuickBooks integration in Nango.
4. Configure QuickBooks in Nango
Nango already provides a QuickBooks Sandbox integration, so we can configure it without implementing the provider OAuth flow ourselves.
In Nango, open the QuickBooks Sandbox integration and add the Client ID and Client Secret from the Intuit Developer app. Use quickbooks-sandbox as the provider configuration key and the com.intuit.quickbooks.accounting scope for this project.
Nango also provides an OAuth callback URL. Copy it into the Redirect URIs section of the Intuit Developer app under Keys & credentials > Development.
At this point, the Nango provider configuration is ready. We won't create a separate test connection from the Nango dashboard; the actual QuickBooks connection will be created later through the Connect Session from Spring Boot.
Next, we'll start the Spring Boot application and create a Nango Connect Session for a tenant.
5. Create a Nango Connect Session from Spring Boot
With the QuickBooks integration configured in Nango, clone the project from GitHub and open the project directory.
Create a .env file from .env.example and add your Nango credentials:
NANGO_BASE_URL=https://api.nango.dev
NANGO_SECRET_KEY=your_nango_secret_key
NANGO_INTEGRATION_KEY=quickbooks-sandbox
NANGO_WEBHOOK_SIGNING_KEY=your_nango_webhook_signing_key
In Nango, Environment settings > API keys and copy the Secret Key for your environment. Add it to .env as NANGO_SECRET_KEY.
Then start the application:
.\run.ps1
Once the application is running, create a Connect Session for the tenant:
curl -X POST \
http://localhost:8080/api/tenants/tenant-1/accounting/connect
Note 1: The tenant-1 value in the URL is an application-level tenant ID. It isn't generated by Nango. For this demo, tenant-1 is a sample tenant defined by the application and used to identify the customer whose QuickBooks account we're connecting. In a real application, this ID would normally come from the authenticated user's or organization's context rather than being hardcoded in the URL.
Note 2: Windows PowerShell: If curl is mapped to Invoke-WebRequest on your system, use curl.exe instead.
One small addition I'd make
The project also includes a small frontend called LedgerFlow that uses these Spring Boot endpoints to display the connected QuickBooks company, customers, and invoices. It provides a simple tenant selector and QuickBooks connection flow, so the backend integration can also be shown through a UI.
The endpoint creates a Nango Connect Session and returns a connect_link that the tenant can open to start the QuickBooks authorization flow. The controller passes the tenant ID to the accounting service, which creates the Connect Session:
public NangoConnectSessionResponse initiateAccountingConnection(
String tenantId) {
NangoEndUser endUser = new NangoEndUser(tenantId);
Map<String, String> tags = Map.of(
"organization_id", tenantId
);
return nangoClient.createConnectSession(
List.of(integrationKey),
endUser,
tags
);
}
}
The tenantId is passed to Nango as the end_user.id. I also send it as the organization_id tag. This tag is used later by the Nango auth webhook to associate the newly created Nango connection with the correct tenant.
The NangoClient keeps the Nango HTTP calls outside the controller and uses Spring's RestClient:
public NangoConnectSessionResponse createConnectSession(
List<String> allowedIntegrations,
NangoEndUser endUser,
Map<String, String> tags) {
NangoConnectSessionRequest request =
new NangoConnectSessionRequest(
allowedIntegrations,
endUser,
tags,
null
);
return restClient.post()
.uri("/connect/sessions")
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
.body(NangoConnectSessionResponse.class);
}
The request sent to Nango now contains the allowed integration, the end user, and the tenant tag:
{
"allowed_integrations": ["quickbooks-sandbox"],
"end_user": {
"id": "tenant-1"
},
"tags": {
"organization_id": "tenant-1"
}
}
Nango returns a Connect Session containing a connect_link:
{
"data": {
"token": "<redacted>",
"connect_link": "https://connect.nango.dev/?session_token=<redacted>"
}
}
One thing I discovered while building this was that end_user is required when creating the Connect Session. My initial request without it returned a 400 response, so I added the tenant ID as the end_user.id.
The Connect Session only starts the authorization flow. After the tenant completes QuickBooks authorization, Nango creates the actual connection and sends an auth webhook to the application. The webhook uses the organization_id tag to associate the new connection_id with the tenant.
6. Complete the QuickBooks OAuth Connection
The Connect Session response contains a connect_link. Copy this URL and open it in a browser to start the connection flow. Nango opens its Connect UI, where you can continue with the QuickBooks Sandbox authorization flow. Sign in to the Sandbox account and authorize the application.
After the authorization is completed, Nango creates the QuickBooks connection and shows a success screen.
At this point, the QuickBooks Sandbox account is connected through Nango. The application can use this connection to make authenticated QuickBooks API requests through the Nango Proxy.
One important detail is that the Connect Session only starts the authorization flow. After the tenant completes the QuickBooks authorization, Nango sends an auth webhook containing the new connection_id. The application uses the organization_id tag from the Connect Session to identify the tenant and stores the connection ID in TenantRepository. The demo uses an in-memory implementation, while a production application would normally persist this mapping in a database.
7. Resolve the Nango Connection and realmId
Once the QuickBooks connection has been created and mapped to the tenant, the application can resolve the Nango connection and retrieve the QuickBooks realmId.
The Spring Boot service first looks up the Nango connection_id for the tenant:
private String resolveConnectionId(String tenantId) {
return tenantRepository.findConnectionId(tenantId)
.orElseThrow(() ->
new IllegalStateException(
"No accounting connection found for tenant: "
+ tenantId));
}
Once the connection_id is resolved, the application asks Nango for the connection metadata and reads the QuickBooks realmId:
private String resolveRealmId(String connectionId) {
return realmIdCache.computeIfAbsent(connectionId, connId -> {
NangoConnection connection =
nangoClient.getConnection(
connId,
integrationKey
);
if (connection.connectionConfig() != null
&& connection.connectionConfig().containsKey("realmId")) {
return String.valueOf(
connection.connectionConfig().get("realmId")
);
}
throw new IllegalStateException(
"Nango connection " + connId
+ " does not have a 'realmId' configured."
);
});
}
}
The important part is that the application does not hardcode the QuickBooks realmId. It resolves the tenant's Nango connection first, then reads the realmId from the connection metadata returned by Nango.
The resolved realmId is also kept in an in-memory cache, so the application does not need to retrieve the same connection metadata from Nango on every request.
With the tenant's Nango connection and QuickBooks realmId resolved, the application can now make its first QuickBooks API request through the Nango Proxy.
8. Making the First QuickBooks API Request
Now that the application can resolve the tenant's connection_id and realmId, it can make a request to QuickBooks through the Nango Proxy.
The application exposes:
GET /api/tenants/{tenantId}/accounting/company
For example:
curl http://localhost:8080/api/tenants/tenant-1/accounting/company
The service resolves the connection and realmId, then passes the QuickBooks request to the NangoClient:
public CompanyInfo getCompanyInfo(String tenantId) {
String connectionId = resolveConnectionId(tenantId);
String realmId = resolveRealmId(connectionId);
return nangoClient.proxyGet(
connectionId,
integrationKey,
"/v3/company/" + realmId
+ "/companyinfo/" + realmId
);
}
The NangoClient makes the actual request to the Nango Proxy:
public CompanyInfo proxyGet(
String connectionId,
String providerConfigKey,
String path) {
return restClient.get()
.uri(uriBuilder -> uriBuilder
.path("/proxy" + path)
.build())
.header("Connection-Id", connectionId)
.header("Provider-Config-Key", providerConfigKey)
.retrieve()
.body(CompanyInfo.class);
}
The application doesn't send QuickBooks credentials with this request. It sends the Nango connection_id and provider configuration key, and Nango uses the credentials associated with that connection when forwarding the request to QuickBooks.
The important part here is the two headers:
Connection-Id: <nango-connection-id>
Provider-Config-Key: quickbooks-sandbox
They tell Nango which connection and provider configuration to use. Nango then makes the authenticated request to the connected QuickBooks account.
The QuickBooks endpoint itself is:
GET /v3/company/{realmId}/companyinfo/{realmId}
A successful response from the QuickBooks Sandbox looks like:
{
"CompanyName": "Sandbox Company US c56a",
"LegalName": "Sandbox Company US c56a",
"Country": "US",
"FiscalYearStartMonth": "January"
}
This response came from the QuickBooks Sandbox through Nango; it isn't a mocked response.
This made the integration boundary much clearer: Spring Boot decides which tenant is requesting data and which QuickBooks resource to retrieve, while Nango handles the authenticated connection to QuickBooks.
9. Reading Customers and Invoices
Once the company request is working, the same connection can be used to retrieve customers and invoices. The application exposes:
GET /api/tenants/{tenantId}/accounting/customers
GET /api/tenants/{tenantId}/accounting/invoices
For example:
curl "http://localhost:8080/api/tenants/tenant-1/accounting/customers?limit=2"
curl "http://localhost:8080/api/tenants/tenant-1/accounting/invoices?limit=2"
For customers, the service sends a QuickBooks query through the Nango Proxy:
SELECT * FROM Customer MAXRESULTS {limit}
A response from the Sandbox looks like:
[
{
"Id": "8",
"DisplayName": "0969 Ocean View Road",
"GivenName": "Sasha",
"FamilyName": "Tillou",
"CompanyName": "Freeman Sporting Goods",
"Active": true,
"Balance": 477.5
}
]
The invoice endpoint follows the same approach:
SELECT * FROM Invoice MAXRESULTS {limit}
For example:
[
{
"Id": "130",
"DocNumber": "1037",
"TxnDate": "2026-09-06",
"DueDate": "2026-10-06",
"TotalAmt": 362.07,
"Balance": 362.07,
"CustomerRef": {
"value": "24",
"name": "Sonnenschein Family Store"
}
}
]
Both endpoints use the same tenant-to-connection mapping and realmId resolution described in the previous sections. The client only needs to provide the application-level tenantId; the backend finds the corresponding Nango connection_id, resolves the QuickBooks realmId, and reuses the connection for the request. There is no separate OAuth flow for customers and invoices.
At this point, the integration is doing what I originally set out to build: once a tenant's QuickBooks connection is established, the backend can use that connection to retrieve different types of accounting data.
10. Handling Errors from Nango and QuickBooks
Getting the first successful response is only part of an integration. A connection can be missing, QuickBooks can reject a request, or the API can rate-limit the application. I didn't want those errors to leak directly out of the Nango client, so I handle Nango's responses in the client layer and convert them into application-level exceptions.
Nango can return different status codes depending on where the failure happened:
-
404: the requested Nango connection doesn't exist. -
424: Nango could not complete the request because of an upstream provider error. -
429: a rate limit was reached.
For 429, the response can also include a Retry-After header, which gives the application information about when it can try again.
I handle these responses in the Nango client instead of checking status codes inside every service:
if (status == 404) {
throw new NangoConnectionNotFoundException(
"Nango connection not found"
);
}
if (status == 424) {
throw new NangoActionException(
error.error().message(),
error.error().upstream()
);
}
if (status == 429) {
String retryAfter =
response.getHeaders()
.getFirst("Retry-After");
throw new NangoRateLimitException(
"Nango rate limit exceeded",
retryAfter
);
}
}
For 424, I preserve the upstream error information so the application can expose a more useful failure reason. I then use Spring's @RestControllerAdvice to convert these exceptions into ProblemDetail responses:
@ExceptionHandler(NangoConnectionNotFoundException.class)
public ProblemDetail handleConnectionNotFound(
NangoConnectionNotFoundException ex) {
ProblemDetail problem =
ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND,
ex.getMessage()
);
problem.setTitle("Accounting Connection Not Found");
return problem;
}
This keeps the Nango-specific error handling inside the integration layer while giving the frontend a consistent API response.
Why this matters
The company, customer, and invoice endpoints all use the same Nango client. Keeping the error handling there means the same behaviour is applied across the accounting endpoints instead of repeating try/catch logic in each controller.
QuickBooks OAuth can also fail
QuickBooks also has its own OAuth token lifecycle, including refresh-token rotation and invalid or revoked refresh tokens. Nango has documented these cases in its QuickBooks OAuth write-up. My Spring Boot application doesn't implement the QuickBooks token refresh lifecycle itself; Nango handles that provider authentication lifecycle.
There is also a trade-off to using an integration layer: Nango becomes another runtime dependency between the application and the provider. For a product with only one or two integrations, implementing the provider connection directly may be reasonable. The value becomes more apparent as the number of integrations and connected accounts grows.
Next, we'll look at another part of the integration that isn't part of the normal request flow: Nango webhooks.
11. Adding Webhooks
The API requests so far are initiated by the application. Webhooks cover the other side of the integration: when Nango sends an event back to the application.
I added a webhook endpoint:
POST /api/webhooks/nango
he endpoint verifies Nango's HMAC-SHA256 signature before processing the payload. Because the signature is calculated from the raw request body, I added WebhookContentCachingFilter to keep the original body available during verification.
The implementation is split into three classes:
-
NangoWebhookController: receives the webhook and handles the event. -
NangoWebhookVerifier: verifies the HMAC-SHA256 signature. -
NangoWebhookPayload: represents the incoming event.
For an auth creation event, the webhook uses the organization_id tag that was added to the Connect Session to identify the tenant. It then stores the new Nango connectionId in TenantRepository.
12. Testing
The test suite covers the parts of the integration that can be tested without a live QuickBooks connection. MockWebServer is used to mock Nango API responses, while MockMvc covers the Spring Boot endpoints. The tests cover Connect Session request serialization, Nango Proxy headers, Nango 404, 424, and 429 responses, webhook signature verification, and tenant accounting endpoints.
Run the tests with:
mvn test
The current suite contains 12 tests, all passing. I used the QuickBooks Sandbox separately for live verification. This keeps the automated tests deterministic while still letting me verify the integration against the actual provider.
13. What Nango Handles vs. What Spring Boot Handles
After building the integration, the responsibilities became fairly clear.
14. What I Learned
Building the integration exposed a few things that are easy to miss when looking at OAuth only from the API documentation. Connection mapping, provider-specific fields such as QuickBooks realmId, error handling, and webhooks all become important once the happy path is working.
I also found that the Java SDK wasn't required to get started. The current Nango Java backend documentation points developers to the REST API, so I built a small Java client using Spring's RestClient. This made the integration work well with the rest of the Spring Boot application and gave me a better understanding of what Nango is actually abstracting from the application.
15. A Few Things I'd Improve
The project currently focuses on the main integration flow: connecting QuickBooks, mapping tenants to Nango connections, retrieving connection details, making requests through Nango, handling errors, and testing the integration.
If this were going into a larger application, I'd replace the in-memory tenant-to-connection mapping with persistent storage and extend the connection lifecycle to handle tenants disconnecting and reconnecting their QuickBooks accounts. I'd also add background syncing and more provider-specific handling for cases such as pagination and rate limits.
For this project, I kept the scope smaller so I could focus on understanding the integration itself rather than building a complete accounting product.
16. Conclusion
Building this project gave me a much better idea of what it actually takes to integrate a service like QuickBooks. Making the API call was not the hard part. Most of the work was around the connection itself: setting up OAuth, connecting the right account to the right tenant, dealing with QuickBooks-specific details like realmId, handling errors and rate limits, and working with webhooks.
Building it in Java also helped me understand what Nango takes care of and what still belongs in the application. I didn't have to build the QuickBooks authentication and token handling myself. Nango manages the provider connection and lets the application make authenticated requests through its Proxy. My Spring Boot application still handles the tenants, connection mapping, business logic, API responses, and application-level errors.
I also wanted to make sure this worked with a real provider, not just mocked responses. I tested the complete flow with a QuickBooks Sandbox—from creating the Connect Session and completing OAuth to retrieving company information, customers, and invoices. Going through that process gave me a much clearer understanding of where Nango fits into a Java/Spring Boot application.
17. Useful Nango References
A few Nango resources I used while building and testing the integration:
- QuickBooks OAuth: refresh tokens and invalid_grant
- Why is OAuth still hard in 2026?
- QuickBooks Sandbox setup
- Nango Java backend SDK
These are useful if you want to go deeper into the provider-specific OAuth behaviour or the Nango API used in this project.










Top comments (0)