QuickBooks is one of the most popular accounting software, especially among small and medium-sized businesses. Its API lets you integrate QuickBooks functionality into your software so users don't have to switch between systems. Automating data transfer between systems reduces the need for manual data entry and helps ensure data consistency.
Common use cases of the QuickBooks API include the following:
- Bidirectional syncing of invoices and payments between QuickBooks and other business software
- Automating payroll or expense tracking for HR platforms
- Generating financial reports using real-time accounting data
In this article, you'll learn how to integrate the QuickBooks API into a FastAPI app to fetch financial data. The article will also cover environment management, making API requests, error handling, and security best practices to ensure the reliability of your app.
You can find the code repository for this tutorial on GitHub.
Setting Up a Developer Account
To access the QuickBooks API, you need to set up a QuickBooks developer account.
Navigate to the QuickBooks Developer website and click Sign Up in the top-right corner of the page:
Complete the registration form, proceed to the verification step, and enter the validation code sent to your mobile number. Upon successful verification, you'll be redirected to the QuickBooks Developer account dashboard.
In the QuickBooks Developer account dashboard, click on the My Hub menu at the top right and select App Dashboard from the drop-down:
To create your first app, click on the + icon in the Apps section. Enter the name of your app and select permissions. Since this is only a demonstration, you can select the accounting and payments permissions. In a production environment, only request the exact permissions your app requires to reduce risk.
After you click Done, a confirmation screen will appear. Click on Show credentials to grab the Client ID and Client Secret values. You will need these to authenticate your app with QuickBooks.
Next, you need to add a redirect URl so that the QuickBooks auth server knows the response is going to a trusted URI. To do this, click on the Open App button to see the App Overview page for your newly created app:
Click on the settings section in the left navigation pane, click on the Redirect URIs tab, and create a new URI entry by clicking Add.
Input http://localhost:8000/callback
in the text field and click Save:
Setting Up a Backend with FastAPI
To connect your app to QuickBooks, you'll need a working backend application that can handle user authentication with OAuth 2.0, which you'll set up later in this tutorial.
The backend redirects users to the QuickBooks authorization page, securely stores the access tokens received after authorization, and uses them to make authenticated API calls. In this tutorial, you'll create your backend using FastAPI.
To set up a FastAPI server, you need to have Python 3.11 or above installed on your system.
Run the following command in the terminal to start a new project:
mkdir quickbooks-app && cd quickbooks-app
python3 -m venv env
This command creates a new project directory named quickbooks-app
and sets up a virtual environment inside it to isolate the project dependencies from the system.
Run the following command to activate the virtual environment:
source env/bin/activate
env\Scripts\activate # On Windows
Next, install the required libraries for the project:
python -m pip install "fastapi[standard]" python-dotenv intuit-oauth tenacity
The command above installs the following:
- fastapi for building backend endpoints and handling authentication redirects
- [python-dotenv](python-dotenv · PyPI) for building the environment variables in your code
- intuit-oauth, a Python SDK by Intuit, for simplifying the authentication process for QuickBooks apps
- tenacity for implementing retry logic in API endpoints
Once all the dependencies are installed, create a new file named .env
and paste your app credentials into it:
CLIENT_ID=your_app_client_id
CLIENT_SECRET=your_app_client_secret
REDIRECT_URI=http://localhost:8000/callback
ENVIRONMENT=sandbox
You will use the configuration above to make OAuth client calls from the FastAPI server.
To create server routes, create a new file named main.py
and populate it with the following code:
from fastapi import FastAPI, Request
from dotenv import load_dotenv
import requests
import json
base_url = "https://sandbox-quickbooks.api.intuit.com/"
current_company = None
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
}
load_dotenv()
app = FastAPI(title="QuickBooks API")
# Dictionary to store OAuth sessions
# This can be replaced with a more persistent storage solution like a database
oauth_session = {}
def init_db():
global oauth_session
try:
with open("oauth_session.json", "r") as file:
oauth_session = json.load(file)
except FileNotFoundError:
oauth_session = {}
with open("oauth_session.json", "w") as file:
json.dump(oauth_session, file)
init_db()
@app.get("/")
def root():
"""
Root endpoint that returns a welcome message
"""
return {
"message": "Welcome to QuickBooks API",
"Company" : current_company if current_company else "No company authenticated"
}
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
The code above loads the .env
file in the project and initializes a new FastAPI server, with a simple GET request endpoint that returns a welcome message. The file also initializes the variables headers
, oauth_session
, and current_company
, which you will use when making API requests. The oauth_session
dictionary will act as a DB to store tokens.
The init_db
function verifies if the app is already authorized by checking the oauth_session.json
file for stored authentication details when the application starts.
Run this file to start the server:
python main.py
Now you can visit http://localhost:8000/
to confirm if the server is running properly:
Setting Up Authentication
QuickBooks uses the OAuth 2.0 standard to authorize apps and let users send API calls.
In an OAuth 2.0 flow, a client (in this case, your app) sends the user to the authorization server consent page (in this case, QuickBooks), where the user is asked to approve the permissions requested by the app. After the user grants consent, the server redirects back with an auth code. The client must make a server-to-server POST request to the authorization server's token endpoint to exchange this code for access and refresh tokens. The access tokens are short-lived, and they are checked on every API call. Refresh tokens stay on the client and are used to obtain new access tokens without login.
To keep the tokens secure, it's best to handle all API requests through a backend. This way, tokens are stored securely on the server and never exposed to the browser.
Let's see how to implement the OAuth 2.0 authentication flow using the QuickBooks API and sample data in the QuickBooks sandbox environment.
To access this sandbox, click on My Hub at the top right of your developer account dashboard and click on Sandboxes:
If you don't see a sandbox company, create a new one by clicking Add, selecting QuickBooks Online Plus SKU, and choosing your country. Click Create to generate a new sandbox environment.
You're now ready to authenticate your app with QuickBooks. To create an OAuth 2.0 flow in your FastAPI app, you have to make changes to the main.py
file. First, add the following imports:
from fastapi.responses import RedirectResponse
from intuitlib.client import AuthClient
from intuitlib.enums import Scopes
import os
You're importing RedirectResponse
to redirect when the authorization URL is obtained, while AuthClient
creates a new client that takes app configuration and generates the authorization URL.
To send an auth request, you need an AuthClient
object with your client ID, client secret, redirect URI, and environment. Add the following code in the main.py
file:
def get_auth_client():
"""
Create and return an AuthClient instance using environment variables
"""
return AuthClient(
os.getenv("CLIENT_ID"),
os.getenv("CLIENT_SECRET"),
os.getenv("REDIRECT_URI"),
os.getenv("ENVIRONMENT", "sandbox")
)
Next, you need to create an endpoint that gets the authorization URL and redirects to the consent screen where you approve your app to access QuickBooks resources. Populate your server file with the following code:
@app.get("/auth")
def auth():
"""
Endpoint to redirect to QuickBooks authorization URL
"""
scopes = [
Scopes.ACCOUNTING,
]
try:
auth_client = get_auth_client()
auth_url = auth_client.get_authorization_url(scopes)
return RedirectResponse(auth_url, status_code=303)
except Exception as e:
return {"error": str(e)}
This code defines a /auth
route handler that sends a request to the QuickBooks server to get an authorization URL. The get_authorization_url
takes a scopes list as an argument, which tells the QuickBooks server what type of resources are intended to be used. Lastly, the application redirects to the consent screen.
Now, you need to create a redirect URL where the user will be redirected after authorization. Add the following code to the server file:
@app.get("/callback")
def callback(request: Request):
"""
Callback endpoint to handle QuickBooks authorization response
"""
try:
code = request.query_params.get("code")
state = request.query_params.get("state")
realm_id = request.query_params.get("realmId")
if not code or not state:
return {"error": "Missing code or state parameter"}
auth_client = get_auth_client()
auth_client.get_bearer_token(code, realm_id=realm_id)
oauth_session = {
"state": state,
"access_token": auth_client.access_token,
"refresh_token": auth_client.refresh_token,
"realm_id": realm_id,
"token_expiry": auth_client.expires_in
}
with open("oauth_session.json", "w") as file:
json.dump(oauth_session, file)
return {
"message": "Authorization successful"
}
except Exception as e:
return {"error": str(e)}
This code creates a /callback
route handler that takes the Request
object sent by the QuickBooks server and extracts code
, state
, and realm_id
from it. The handler then calls the get_auth_client
function to initialize auth_client
and uses its get_bearer_token
method to exchange code
for access_token
and refresh_token
. These tokens are then stored in the dictionary storage.
Next, you need to create a new function that takes the current state, makes an API call, and sets the current_company
variable to the name of the sandbox company. Add the following code in the same file:
def set_current_company():
headers.update({
"Authorization": f"Bearer {oauth_session.get('access_token')}"
})
realm_id = oauth_session.get('realm_id')
try:
response = requests.get(
f"{base_url}v3/company/{realm_id}/companyinfo/{realm_id}",
headers=headers
)
if response.status_code == 200:
return response.json()["CompanyInfo"]["CompanyName"]
else:
return None
except Exception as e:
print(f"Error setting current company: {str(e)}")
This code updates the headers
to include the access_token
. A GET request is sent to the /v3/company/<realmid>/companyinfo/<realmid>
endpoint, which returns a CompanyInfo object. If the request is successful, the code extracts and returns the company's name from the CompanyName field in the response.
Lastly, make the following changes in your root endpoint to verify if the authorization is working or not:
@app.get("/")
def root():
"""
Root endpoint that returns a welcome message
"""
if oauth_session: # new
global current_company # new
current_company = set_current_company() # new
return {
"message": "Welcome to QuickBooks API",
"Company" : current_company if current_company else "No company authenticated"
}
If the oauth_session
value was set successfully, this endpoint will call the set_current_company
function and set the global variable current_company
to the name obtained from the function.
Test this flow by running the following command in your terminal:
python main.py
With the server running, navigate to http://localhost:8000/auth
in your browser:
Select the sandbox company from the drop-down and press next:
The token exchange was successful.
When you navigate to http://localhost:8000/
, you can see the company name you selected:
This confirms that the authorization was indeed successful and the current_company
value was initialized.
Fetch Customers
To get customers from QuickBooks, you need to send a GET request to /v2/company/<realmid>/query/?query=select * from customer
. You can implement this functionality by adding the following code to your main.py
file:
@app.get("/customers")
def get_customers():
"""
Endpoint to fetch customers from QuickBooks
"""
if not oauth_session:
return {"error": "Not authenticated"}
headers.update({
"Authorization": f"Bearer {oauth_session.get('access_token')}"
})
realm_id = oauth_session.get('realm_id')
params = {
"query": "Select * from Customer",
}
try:
response = requests.get(
f"{base_url}v3/company/{realm_id}/query",
headers=headers,
params=params
)
if response.status_code == 200:
print("Response:", response.status_code, "Request successful")
return response.json()["QueryResponse"].get("Customer", [])
else:
print("Response:", response.status_code, "Request failed")
return {"error": "Failed to fetch customers", "status_code": response.status_code}
except Exception as e:
print(f"Error fetching customers: {str(e)}")
return {"error": str(e)}
This code creates a route for /customers
, which first checks if the user is authenticated. If they are, the function adds the authorization token to the headers by accessing it from the oauth_session
. To access all the customers, the API URL takes a query
params and executes the query on the QuickBooks server. It makes the API calls by passing the headers
and params
data to the /v3/company/<realmid>/query
endpoint and accessing the Customer value from the JSON response.
The code also adds error handling in the form of the try/catch
block, which catches any exception thrown by the request. The /customers
route also logs the status of the request for debugging and monitoring purposes.
To test this code, run the server and navigate to the http://localhost:8000/customers
endpoint. You will see the following output:
Handling Data Models and Common Endpoints
The QuickBooks API works with nested data, and this data needs to be carefully modeled to ensure API requests conform to QuickBooks' schema expectations. For customers, nested data could include their shipping address and billing address. Invoices and payments present a complex nesting of data, with each line item containing item information, which further includes data about item detail. Similarly, each line item in the payments object has multiple child attributes, which may become complex to handle.
Pydantic models help manage this complexity. Pydantic is a Python library for defining and validating structured data. It ensures the data exchanged with QuickBooks is correctly formatted.
Let's now see how to define nested data models using Pydantic and how to use them to create and validate data for common QuickBooks endpoints, such as creating invoices. You'll also learn how to set up a route handler to fetch all transactions in your company.
Create a new file named models.py
and populate it with the following code:
from pydantic import BaseModel
from typing import List
class ItemRef(BaseModel):
name: str
value: str
class SalesItemLineDetail(BaseModel):
ItemRef: ItemRef
class LineItem(BaseModel):
DetailType: str
Amount: float
SalesItemLineDetail: SalesItemLineDetail
class CustomerRef(BaseModel):
value: str
class InvoiceModel(BaseModel):
Line: List[LineItem]
CustomerRef: CustomerRef
To create an invoice item, two fields are required: Line
and CustomerRef
. The Line
field is a list of LineItem
objects, where each LineItem
includes details such as the amount and type of detail. Within each LineItem
, there's a nested SalesItemLineDetail
object, which further contains an ItemRef
object holding the item's name
and value
. This structure allows for a clear and organized representation of each item included in the invoice.
Next, you need to create an endpoint that sends a POST request to the QuickBooks API with InvoiceData
and returns the created invoice. Open your main.py
file and add the following import at the top:
from models import InvoiceModel
The InvoiceModel
will be used to validate incoming JSON data in your endpoint.
To create the /invoices/create
route, append your file with the following code:
@app.post("/invoices/create")
def create_invoice(invoice: InvoiceModel):
"""
Endpoint to create a new invoice in QuickBooks
"""
if not oauth_session:
return {"error": "Not authenticated"}
headers.update({
"Authorization": f"Bearer {oauth_session.get('access_token')}"
})
realm_id = oauth_session.get('realm_id')
try:
response = requests.post(
f"{base_url}v3/company/{realm_id}/invoice",
headers=headers,
json=invoice.model_dump()
)
if response.status_code == 200:
return response.json()
else:
return {"error": "Failed to create invoice", "status_code": response.status_code}
except Exception as e:
print(f"Error creating invoice: {str(e)}")
return {"error": str(e)}
The above route first checks if the user is authenticated; if it is, it adds the auth token to the headers and sends the POST request to the /v3/company/<realmid>/invoice
API endpoint. The InvoiceModel
data received is used as the request body to the /invoices/create
route. If the request sends a 200 status code, the invoice was successfully created, and the route returns the created invoice.
Since this is a POST request, you cannot directly send POST data from the browser. FastAPI provides a built-in API testing feature called Swagger UI, which lets you send POST requests and test your endpoint from the browser.
To test this code, make sure your server is running, then navigate to http://localhost:8000/docs
. This opens the Swagger UI window, which lists all the endpoints in your app:
In the Swagger UI window, click on the POST /invoices/create
endpoint, and then click Try it out:
Edit the default JSON object text box to include the necessary nested data that conforms to the InvoiceModel
you created earlier:
{
"Line": [
{
"DetailType": "SalesItemLineDetail",
"Amount": 100.0,
"SalesItemLineDetail": {
"ItemRef": {
"name": "Services",
"value": "1"
}
}
}
],
"CustomerRef": {
"value": "1"
}
}
Click on Execute for the Swagger UI to send the POST request to the /invoices/create
endpoint. Scroll down to see the response returned by the endpoint:
The response was successful, and an invoice object was returned as JSON.
Let's now see how to set up a route that handles the /transactions
endpoint. This route will return all the transactions in your sandbox company.
Append the following code to the main.py
file:
@app.get("/transactions")
def get_transactions():
"""
Endpoint to fetch transactions from QuickBooks
"""
if not oauth_session:
return {"error": "Not authenticated"}
headers.update({
"Authorization": f"Bearer {oauth_session.get('access_token')}"
})
realm_id = oauth_session.get('realm_id')
try:
response = requests.get(
f"{base_url}v3/company/{realm_id}/reports/TransactionList",
headers=headers,
)
if response.status_code == 200:
return response.json()
else:
return {"error": "Failed to fetch transactions", "status_code": response.status_code}
except Exception as e:
print(f"Error fetching transactions: {str(e)}")
return {"error": str(e)}
This code sends a GET request to /v3/company/<realmid>/TransactionList
to obtain all the transactions in the company. If the request was successful, the list of transactions is returned as JSON.
Test this endpoint by navigating to http://localhost:8000/transactions
:
You will see that all the transactions in your sandbox company have been returned.
Handling Errors and Rate Limits
When working with APIs, error handling and rate limit management prevent app crashes or API lockouts.
APIs can throw various errors like 401 Unauthorized, 429 ResourceExhausted, or 500 Internal Server Error. An app making API calls should be able to handle all these errors. In case of any failures, the app should implement a retry logic with a delay to prevent overwhelming the system and allow time for temporary issues to resolve.
One common retry logic is exponential backoff, which waits progressively longer after each failure. Spacing retries reduces load on the server and gives it time to recover.
Logging is another great strategy to triage issues and pinpoint the exact source of bugs easily. It cuts the mean time to resolution by a significant amount.
To implement these strategies in your code, import the following modules into the main.py
file:
from tenacity import (
retry,
stop_after_attempt,
wait_exponential,
RetryError,
before_sleep_log,
)
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
This code imports several functions from tenacity to implement exponential backoff retry logic. It also imports a logging library and creates a logger variable, which will log the status of your API requests.
Let's create a retrying logic for the /customers
endpoint. Add the following code to the main.py
file just above the get_customers
route:
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=5),
before_sleep=before_sleep_log(logger, logging.INFO),
)
def fetch_customers_requests(realm_id: str, headers: dict, params: dict = None):
"""
Fetch customers from QuickBooks with retry logic
"""
logger.info(f"Fetching customers for realm_id: {realm_id} with params: {params}")
response = requests.get(
f"{base_url}v3/company/{realm_id}/query", headers=headers, params=params
)
if response.status_code == 200:
print("Response:", response.status_code, "Request successful")
return response.json()["QueryResponse"].get("Customer", [])
else:
print("Response:", response.status_code, "Request failed")
raise Exception(f"Failed to fetch customers: {response.status_code}")
In this code, fetch_customers_requests
calls the QuickBooks API to get all the customers in your company. This function is decorated with a retry
decorator from tenacity to handle errors automatically when calling the API. This decorator is configured to retry the request up to three times. Between each attempt, it waits for a certain amount of time, starting from one second and increasing up to a maximum of five seconds. Between each retry, the code logs the attempt using before_sleep_log
.
Next, modify the /customers
route to use this function with retry logic:
@app.get("/customers")
def get_customers():
if not oauth_session:
return {"error": "Not authenticated"}
headers.update({"Authorization": f"Bearer {oauth_session.get('access_token')}"})
realm_id = oauth_session.get("realm_id")
params = {
"query": "Select * from Customer",
}
try:
customers = fetch_customers_requests(realm_id, headers, params)
return customers
except RetryError as e:
print(f"Retry failed: {str(e)}")
return {"error": "Failed to fetch customers after retries", "details": str(e)}
except Exception as e:
print(f"Error fetching customers: {str(e)}")
return {"error": str(e)}
This code calls the fetch_customers_requests
function to send the API request. The /customers
route prepares the request parameters and calls the function.
To test this retry behavior, simulate a failed request by altering the authorization header:
# In /customer route handler remove 'n' from Authorization
headers.update({"Authorizatio": f"Bearer {oauth_session.get('access_token')}"})
Now, start your server and navigate to http://127.0.0.1:8000/customers
. You will see the following logs in the terminal:
INFO:main:Fetching customers for realm_id: 9341455125556714 with params: {'query': 'Select * from Customer'}
Response: 401 Request failed
INFO:main:Retrying main.fetch_customers_requests in 1.0 seconds as it raised Exception: Failed to fetch customers: 401.
INFO:main:Fetching customers for realm_id: 9341455125556714 with params: {'query': 'Select * from Customer'}
Response: 401 Request failed
INFO:main:Retrying main.fetch_customers_requests in 2.0 seconds as it raised Exception: Failed to fetch customers: 401.
INFO:main:Fetching customers for realm_id: 9341455125556714 with params: {'query': 'Select * from Customer'}
Response: 401 Request failed
Retry failed: RetryError[<Future at 0x109697750 state=finished raised Exception>]
As shown in the logs, the function attempts the request three times with increasing wait times. After the third failure, it raises a RetryError
, signaling that the retries were exhausted.
Security Best Practices
Whenever you work with sensitive financial data, keep the following security best practices in mind to prevent data breaches, unauthorized access, and violations of compliance.
Environment variables must be stored securely on the server; they shouldn't be hard-coded. Use the .env
file of a secrets manager service like AWS or Pulumi ESC to keep credentials separate from code and reduce the risk of accidentally pushing secrets to version control.
Tokens provide full access to company financial data, so they must be encrypted before storage. Use strong encryption algorithms, such as AES-256, and ensure encryption keys are stored securely.
SOC 2 compliance shows that your app follows enterprise-grade security, confidentiality, and privacy. Compliance includes a data audit trail, token management, adding granular permissions like RBAC (role-based access control), and automated data lifecycle management that handles data from creation to secure destruction without manual intervention.
Conclusion
In this article, you learned how to integrate the QuickBooks API into your application and access financial data from your app efficiently and securely.
If QuickBooks is one of several accounting solutions you need to integrate with, consider Apideck's unified accounting API. It supports multiple accounting platforms, including QuickBooks, with a single integration. Apideck maintains SOC 2 compliance and offers real-time data with no caching layer, ensuring customers receive data without any caching delay.
You can try out Apideck for free.
Ready to get started?
Scale your integration strategy and deliver the integrations your customers need in record time.