Back to blog
AccountingGuides & Tutorials

How to Integrate with the QuickBooks API

Learn how to integrate the QuickBooks API into a FastAPI app for seamless accounting automation. This step-by-step guide covers developer account setup, OAuth 2.0 authentication, fetching financial data, creating invoices, handling errors, and implementing security best practices.

Vivek Singh

Vivek Singh

24 min read
How to Integrate with the QuickBooks API

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:

Developer account webpage

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:

App dashboard

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:

App overview

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:

Redirect URI settings

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:

Server check screen

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:

Sandbox option

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:

QuickBooks consent screen

Select the sandbox company from the drop-down and press next:

Auth successful screen

The token exchange was successful.

When you navigate to http://localhost:8000/, you can see the company name you selected:

Updated home page

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:

Customer endpoint

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:

Swagger UI window

In the Swagger UI window, click on the POST /invoices/create endpoint, and then click Try it out:

Create invoices endpoint

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:

POST response

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:

Transaction list

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.

Ready to get started?
Talk to an expert
Trusted by fast-moving product & engineering teams
Nmbrs
Benefex
Principal Group
Invoice2go by BILL
Trengo
MessageMedia
Lever
Ponto | Isabel Group
Apideck Blog

Insights, guides, and updates from Apideck

Discover company news, API insights, and expert blog posts. Explore practical integration guides and tech articles to make the most of Apideck's platform.

Build vs Buy Accounting Integrations
Unified APIGuides & TutorialsAccounting

Build vs Buy Accounting Integrations

Struggling with accounting API integrations? Learn why building your own can drain engineering resources, delay product development, and introduce compliance risks and why unified APIs are emerging as the superior alternative.

Kateryna Poryvay

3 min read
Understanding the security landscape of MCP
AIIndustry insights

Understanding the security landscape of MCP

This article dives deep into the current state of MCP security in 2025, covering real-world vulnerabilities like prompt injection, tool poisoning, and token exposure. You’ll learn about the biggest threats facing MCP implementations today, including exploits seen in GitHub, Supabase, and others—plus what the new OAuth 2.0-based security spec is doing to address them.

Saurabh Rai

9 min read
API Based RAG using Apideck’s Filestorage API, LangChain, Ollama, and Streamlit
AIFile StorageGuides & Tutorials

API Based RAG using Apideck’s Filestorage API, LangChain, Ollama, and Streamlit

This article walks through building a Retrieval-Augmented Generation (RAG) pipeline that goes beyond static vector stores. Instead of pre-indexed documents, it uses API-based retrieval to access live files from Box via Apideck’s unified file storage API. You'll see how to authenticate users securely with Vault, fetch and download files, and summarize them in real-time with a local LLM using LangChain and Ollama. Includes full code examples, project setup, and a working Streamlit UI to tie it all together.

Saurabh Rai

12 min read