This Spring Boot app authenticates with QuickBooks via OAuth 2.0 (Java SDK), fetches Customers/Items via the Accounting API, and calculates Sales Tax using the Sales Tax GraphQL API.
- Complete Sales Tax Operations (UI demo)
- ✅ Calculate: real-time tax calculations for transactions (GraphQL)
- ✅ Customer/Item retrieval via Accounting API
- ✅ Input builder: select customer/item, shipping ZIPs, units, and unit value
- GraphQL Integration
- Uses
indirectTaxCalculateSaleTransactionTaxmutation - Variables prepared dynamically from form input
- Uses
- OAuth 2.0 (Java SDK)
- Discovery-based environment handling
- Secure redirect with configurable URI
- Diagnostics:
GET /debug/configreturns non-sensitive runtime config
- Java 17 or higher
- Gradle 7.0 or higher
- QuickBooks Developer account and a QuickBooks Online company
- ngrok (for local development)
/— Home page/connect— Initiate OAuth flow/callback— OAuth callback/calculate_tax— Run Sales Tax GraphQL calculation/debug/config— Non-sensitive runtime config
Runtime
- Spring Boot Web (spring-boot-starter-web)
- Spring Boot Thymeleaf (spring-boot-starter-thymeleaf)
- Intuit QuickBooks SDKs:
- ipp-v3-java-devkit (6.7.0)
- oauth2-platform-api (6.7.0)
- ipp-v3-java-data (6.7.0)
- Jackson Databind (JSON)
- Spring Session Core (for session management)
Testing
- Spring Boot Starter Test (JUnit 5/Jupiter, AssertJ, Mockito)
- Spring Test (MockMvc, MockRestServiceServer)
SampleApp-SalesTax-Java/
├── src/main/java/com/quickbooks/demo/
│ ├── Application.java
│ ├── config/
│ ├── controller/
│ └── service/
├── src/main/resources/
│ ├── application.yml
│ ├── graphql/
│ │ ├── sales_tax.graphql
│ │ └── graphql_variables.json
│ ├── static/
│ └── templates/
└── README.md
com.intuit.quickbooks.accounting— access accounting data (customers, items)indirect-tax.tax-calculation.quickbooks— Sales Tax GraphQL access
- QuickBooks Automated Sales Tax (AST) is nexus-driven. Tax is charged only when your company has nexus (registration) in the ship-to state.
- The API accepts any US ZIP, but without nexus for that destination you will typically get $0 tax (and in some environments you may see an error).
- For non-zero tax: enable Sales Tax in QBO, add nexus for destination states, use a taxable item, and provide a valid US ship-to address/ZIP in a nexus state.
AST is US-only and supported in production. To get non-zero tax values:
- Enable Sales Tax in QBO
- Taxes → Sales tax → Set up sales tax
- Add nexus (registration) for destination states
- Taxes → Sales tax → Sales tax settings → Add state
- Pick the state, set effective date, filing frequency, agency
- Create a taxable item
- Sales → Products and services → New → mark “Is taxable”
-
Ensure a customer exists with a US address
-
Re-authenticate if necessary (scopes already include
indirect-tax.tax-calculation.quickbooks)
Testing note: The ship-to ZIP must be in a state where you have nexus. Example: if you added nexus for CA, test with 94043 (Mountain View, CA); for UT, test with 84041 (Layton, UT).
- Clone the repository
git clone <repository-url>
cd SampleApp-SalesTax-Java- Start ngrok
ngrok http 8080- Configure your QuickBooks app
- Visit the Intuit Developer Portal (
https://developer.intuit.com/app/developer/myapps) - Create an app (or use an existing one)
- Enable the scopes above
- Add your redirect URI (e.g.,
https://your-ngrok-url.ngrok-free.app/callback)
- Configure application settings
Option A: Use environment variables
export QB_CLIENT_ID=your_client_id
export QB_CLIENT_SECRET=your_client_secret
export QB_REDIRECT_URI=https://your-ngrok-url.ngrok-free.app/callback
export QB_ENVIRONMENT=production # or sandbox
export QB_GRAPHQL_URL=https://qb.api.intuit.com/graphql
export QB_BASE_URL=https://quickbooks.api.intuit.com
export QB_MINOR_VERSION=75
export QB_SCOPES="com.intuit.quickbooks.accounting,indirect-tax.tax-calculation.quickbooks"Option B: Use application.yml (create at src/main/resources/application.yml)
server:
port: 8080
spring:
application:
name: quickbooks-sdk-demo
thymeleaf:
cache: false
quickbooks:
# INSTRUCTIONS: Copy this block into application.yml and replace with your credentials
client-id: ${QB_CLIENT_ID:your_quickbooks_client_id_here}
client-secret: ${QB_CLIENT_SECRET:your_quickbooks_client_secret_here}
environment: ${QB_ENVIRONMENT:production} # Use 'sandbox' for development, 'production' for live
redirect-uri: ${QB_REDIRECT_URI:https://your-ngrok-url.ngrok-free.app/callback}
base-url: ${QB_BASE_URL:https://quickbooks.api.intuit.com} # If using sandbox, set to https://sandbox-quickbooks.api.intuit.com
graphql-url: ${QB_GRAPHQL_URL:https://qb.api.intuit.com/graphql}
minor-version: ${QB_MINOR_VERSION:75}
deep-link-template: ${QB_DEEP_LINK_TEMPLATE:https://app.qbo.intuit.com/app/invoice?txnId=%s} # sandbox: https://app.sandbox.qbo.intuit.com/app/invoice?txnId=%s
scopes:
- com.intuit.quickbooks.accounting
- project-management.project
logging:
level:
com.quickbooks.demo: DEBUG
org.springframework.graphql: DEBUGTip: Use the config template safely
- Copy the template to your local config file:
- macOS/Linux:
cp src/main/resources/application.yml.template src/main/resources/application.yml - Windows (PowerShell):
Copy-Item src/main\resources\application.yml.template src/main\resources\application.yml
- macOS/Linux:
- Edit
application.ymlwith your real credentials and URLs. src/main/resources/application.ymlis in.gitignore, so your secrets won’t be committed. Keep the.templatefile in git as a reference only.
- Run the application
./gradlew bootRun- Visit
http://localhost:8080 - Click "Connect to QuickBooks" to authenticate (redirects to
/connect) - After auth, the home page loads Customers and Items from your QBO company
- Enter ship-from/to ZIPs, number of units, and unit value (or extend to full free-form addresses)
- Click "Calculate Tax" to execute the Sales Tax GraphQL mutation and view the result
- Optional: visit
/debug/configto verify environment, endpoints, and scopes
- Insufficient scope (403)
- Ensure both scopes are configured and granted during auth
- If you change scopes, re-authenticate to obtain a new token
- Invalid client or redirect URI
- Verify Client ID/Secret
- Ensure
redirect-uriexactly matches the Developer Portal configuration
- Invalid
redirect_uriquery parameter
- The QuickBooks OAuth page shows: "The redirect_uri query parameter value is invalid".
- Fix by making the values match EXACTLY (scheme, host, path) between:
- Your Intuit Developer Portal app: Keys & OAuth → Redirect URIs
- Your app config:
src/main/resources/application.yml→quickbooks.redirect-uri
- Tips:
- Use HTTPS (required for production); ngrok URL must match the registered value.
- Include the path (typically
/callback). No trailing slashes unless registered that way. - After changing the redirect URI in either place, restart the app and redo the OAuth flow.
- Environment mismatch
- Ensure
environmentmatches the company you connect to (sandbox vs production) - Also update the URLs to match the environment:
- production: base-url=https://quickbooks.api.intuit.com, graphql-url=https://qb.api.intuit.com/graphql
- sandbox: base-url=https://sandbox-quickbooks.api.intuit.com, graphql-url=https://qb-sandbox.api.intuit.com/graphql
- GraphQL limitations (e.g., error
-37109)
- Ensure Sales Tax is enabled for the connected company
- Use valid QuickBooks customer IDs and product/variant IDs
- Provide full free-form addresses if ZIP-only returns errors
- First-time "double connect" (only on initial login)
- Cause: If you load the UI on one host (e.g.,
http://localhost:8080) but your OAuthredirect-uriis on another (e.g.,https://<ngrok>.ngrok-free.app/callback), the OAuth callback writes tokens to a different session (ngrok domain), while your visible page is on localhost. The page won’t see the token until you reconnect on the same host. - Symptoms: First connect appears successful in logs, but the home page still shows "Connect to QuickBooks". A second click works, and future connects are fine.
- Fixes:
- Open the app using the same host as your
redirect-uri(visithttps://<ngrok>.ngrok-free.app/instead ofhttp://localhost:8080). - Or change
quickbooks.redirect-urito match the host you actually use, and update the same value in the Intuit Developer Portal.
- Open the app using the same host as your
- Optional hardening (behind proxies):
- In
application.yml, consider:
- In
server:
forward-headers-strategy: native
servlet:
session:
cookie:
same-site: Lax
secure: true- Store a random OAuth
statein the session on/connectand validate it in/callback. - Remove
prompt=consentinQuickBooksOAuthService#getAuthorizationUrlif you don’t need forced re-consent.
This project is licensed under the Apache License 2.0 — see LICENSE.
For support, visit the Intuit Developer Community (https://help.developer.intuit.com/s/) or open an issue in this repository.

