Skip to main content
RapidDev - Software Development Agency
bubble-integrationsBubble API Connector

QuickBooks

Connect Bubble to QuickBooks Online's IDS REST API using OAuth 2.0. Two Bubble-specific gotchas kill most first attempts: every API call requires the realmId in the URL path, and QuickBooks refresh tokens rotate on each use — storing the old token after a refresh permanently locks you out, forcing users to re-authorize from scratch.

What you'll learn

  • How to set up OAuth 2.0 Authorization Code flow for QuickBooks in Bubble with a callback page
  • How to capture and store the realmId from the OAuth callback URL — required for every API call
  • How to configure the Bubble API Connector with a Private Authorization header for QuickBooks
  • How to query invoices using QuickBooks IDS Query Language (IQL) from a Bubble Repeating Group
  • How to build a token refresh Backend Workflow that atomically saves rotating refresh tokens
  • How to switch safely between QuickBooks sandbox and production environments
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate20 min read3–5 hoursFinance & AccountingLast updated July 2026RapidDev Engineering Team
TL;DR

Connect Bubble to QuickBooks Online's IDS REST API using OAuth 2.0. Two Bubble-specific gotchas kill most first attempts: every API call requires the realmId in the URL path, and QuickBooks refresh tokens rotate on each use — storing the old token after a refresh permanently locks you out, forcing users to re-authorize from scratch.

Quick facts about this guide
FactValue
ToolQuickBooks
CategoryFinance & Accounting
MethodBubble API Connector
DifficultyIntermediate
Time required3–5 hours
Last updatedJuly 2026

QuickBooks + Bubble: two things that will break your first implementation

QuickBooks Online has an excellent REST API through the Intuit Developer Services (IDS) platform. It supports invoices, customers, expenses, payments, bills — everything a small business accounting integration could need. The Bubble integration is very achievable, but there are two architectural details that are not obvious from the QuickBooks docs and will break a naive first implementation.

**The realmId requirement.** Every single API call to the QuickBooks API must include the company identifier — called `realmId` — directly in the URL path: `/v3/company/{realmId}/invoice`. The realmId is delivered once, in the OAuth callback URL as a query parameter, alongside the authorization code. If you miss it during the callback flow, you will get generic 404 errors on every data call and no helpful error message pointing to the missing realmId. You need to store it in the User's database record immediately during the OAuth flow.

**Rotating refresh tokens.** QuickBooks access tokens expire in 1 hour. To stay logged in, you exchange your refresh token for a new access token — a standard OAuth pattern. But QuickBooks also issues a new refresh token with each exchange and immediately invalidates the old one. This means: if your Backend Workflow successfully refreshes the access token but fails to save the new refresh token to the database before the workflow continues, the old refresh token is gone forever and the user must re-authorize from scratch. You need an atomic workflow step that saves both tokens before doing anything else.

This tutorial covers both problems with Bubble-specific solutions, plus the IDS Query Language (IQL) syntax for fetching invoices and the sandbox/production environment separation.

Integration method

Bubble API Connector

REST calls to QuickBooks IDS API with Authorization: Bearer [access_token] as a Private header. OAuth 2.0 Authorization Code flow requires a Bubble Backend Workflow on a paid plan for token exchange and rotation-safe refresh.

Prerequisites

  • A Bubble account on the Starter plan or higher (Backend Workflows required for OAuth token exchange and rotation-safe refresh — free plan cannot complete this integration)
  • A QuickBooks Online account (free trial works); paid subscription from ~$30/month required for production use
  • An Intuit Developer account at developer.intuit.com — free, takes 5 minutes to create
  • A QuickBooks sandbox company created in the Intuit Developer portal for testing
  • API Connector plugin installed in your Bubble app (Plugins → Add plugins → search 'API Connector' → Install)

Step-by-step guide

1

Step 1: Create an Intuit Developer app and configure OAuth credentials

Go to developer.intuit.com and log in with your Intuit account (or create one — it's free). Click **Dashboard** in the top navigation, then click **Create an app**. Select **QuickBooks Online and Payments** as the platform. Give your app a name (e.g. 'My Bubble Integration') and click Create app. Once created, you will land on the app's Keys & credentials page. You will see two sets of keys: **Sandbox** and **Production**. Start with Sandbox — copy the **Client ID** and **Client Secret** for the sandbox environment. You will need these for the OAuth flow. In the same Keys & credentials section, scroll down to **Redirect URIs** and click Add URI. Enter your Bubble app's callback page URL. You will create this page in the next step, so the URL will be something like `https://your-app.bubbleapps.io/qb-callback`. Click Save. Back in your Bubble app's Data tab, add these fields to the User data type: `qb_access_token` (text), `qb_refresh_token` (text), `qb_realm_id` (text), `qb_token_issued_at` (date). Then go to the **Privacy** tab, click on User, and add a rule: 'This User is Current User' — check all the new qb_ fields as readable only by the current user. This prevents QuickBooks tokens from leaking to other users.

user_data_fields.json
1// Bubble User data type — new fields to add
2{
3 "qb_access_token": "text", // Store encrypted, Privacy rule: owner only
4 "qb_refresh_token": "text", // Same privacy rule
5 "qb_realm_id": "text", // Company identifier, required in every API URL
6 "qb_token_issued_at": "date" // For checking 55-minute expiry window
7}
8
9// Privacy rule (Data tab → Privacy → User):
10// When: This User is Current User
11// Read: qb_access_token, qb_refresh_token, qb_realm_id, qb_token_issued_at

Pro tip: Use the Sandbox environment for all development and testing. QuickBooks creates a pre-populated sandbox company with sample invoices, customers, and transactions — perfect for testing without touching real business data. Sandbox and production use different Client IDs, different secrets, and different base URLs.

Expected result: An Intuit Developer app exists with sandbox Client ID and Client Secret copied. The redirect URI points to your /qb-callback Bubble page URL. The User data type has four new qb_ fields with Privacy rules restricting access to the token owner.

2

Step 2: Build the OAuth authorization URL and 'Connect QuickBooks' button

The OAuth 2.0 Authorization Code flow starts by redirecting the user to Intuit's authorization page with the correct parameters. You will build this as a Bubble workflow on a 'Connect QuickBooks' button. In your Bubble page editor, add a Button element with the text 'Connect QuickBooks'. Click on the button to add a workflow. Add the action **Navigate → Open an external URL**. The URL is the QuickBooks OAuth authorization endpoint with parameters that you construct dynamically. The authorization URL format: `https://appcenter.intuit.com/connect/oauth2?client_id=[CLIENT_ID]&redirect_uri=[REDIRECT_URI]&response_type=code&scope=com.intuit.quickbooks.accounting&state=[STATE]` In Bubble's 'Open an external URL' action, build this URL as a dynamic expression: - Base: `https://appcenter.intuit.com/connect/oauth2` - `?client_id=` + your Intuit Client ID (put this directly in the URL or store in an App Data field) - `&redirect_uri=` + your callback page URL (URL-encoded) - `&response_type=code` - `&scope=com.intuit.quickbooks.accounting` - `&state=` + a unique value (can use Current User's Unique ID) to prevent CSRF Set the option 'Open in a new tab' to No — you want the same tab to land on the Intuit authorization page, then redirect back to your /qb-callback page after the user grants access. Make sure the redirect_uri exactly matches what you entered in the Intuit Developer portal, including trailing slashes or their absence. A mismatch causes an 'Invalid redirect_uri' error from Intuit before any authorization happens.

oauth_url_construction.json
1// OAuth Authorization URL (construct in Bubble workflow)
2https://appcenter.intuit.com/connect/oauth2
3 ?client_id=ABcDEfGH123456789xyz
4 &redirect_uri=https%3A%2F%2Fyour-app.bubbleapps.io%2Fqb-callback
5 &response_type=code
6 &scope=com.intuit.quickbooks.accounting
7 &state=<Current User's Unique ID>
8
9// After user authorizes, Intuit redirects to:
10https://your-app.bubbleapps.io/qb-callback
11 ?code=Q0IxMDEwMzYxMTIwNjM0
12 &state=<same_state_value>
13 &realmId=9341452234219068 CAPTURE THIS IMMEDIATELY

Pro tip: Store your Intuit Client ID in a Bubble App Data object (create a type called 'Settings' with a text field 'qb_client_id') rather than hard-coding it in the URL string. This makes switching between sandbox and production Client IDs much simpler.

Expected result: Your Bubble app has a 'Connect QuickBooks' button. Clicking it redirects the user to the Intuit authorization page where they can log in with their QuickBooks credentials and grant access to your app. After granting, they are redirected to /qb-callback.

3

Step 3: Create the /qb-callback page to capture code, realmId, and exchange for tokens

Create a new page in your Bubble app named `qb-callback`. This page handles the OAuth redirect from Intuit. When the page loads, the URL contains two critical parameters: `code` (the authorization code to exchange for tokens) and `realmId` (the QuickBooks company identifier you need to store immediately). Add a workflow to the qb-callback page: **When page is loaded → Run a Backend Workflow**. Inside the Backend Workflow (which requires a paid Bubble plan), you will: 1. Extract the `code` URL parameter using Bubble's 'Get data from page URL' → Parameter name: `code` 2. Extract the `realmId` URL parameter using 'Get data from page URL' → Parameter name: `realmId` 3. POST to the QuickBooks token endpoint to exchange the code for tokens 4. Save all results to the User record atomically The token exchange POST: - URL: `https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer` - Method: POST - Headers: `Authorization: Basic [base64(clientId:clientSecret)]`, `Content-Type: application/x-www-form-urlencoded` - Body (form-encoded): `grant_type=authorization_code&code=[code]&redirect_uri=[your_redirect_uri]` The response contains `access_token`, `refresh_token`, `token_type`, and `expires_in`. In the same Backend Workflow step, immediately save all four values plus the extracted `realmId` to Current User's record. Save `qb_token_issued_at` as the current date/time. Do NOT proceed to any other action before this save completes — the tokens are only valid once and must be persisted immediately. After saving, navigate the user to your main dashboard page: add 'Navigate to page → [dashboard page]' as the final workflow step.

token_exchange.json
1// Token exchange POST request (configure in API Connector or Backend Workflow)
2{
3 "method": "POST",
4 "url": "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
5 "headers": {
6 "Authorization": "Basic <base64(clientId:clientSecret)>", // Private
7 "Content-Type": "application/x-www-form-urlencoded",
8 "Accept": "application/json"
9 },
10 "body": "grant_type=authorization_code&code=<code_from_url>&redirect_uri=<your_redirect_uri>"
11}
12
13// Successful response:
14{
15 "access_token": "eyJlbmMiOiJBMTI4Q0...",
16 "expires_in": 3600,
17 "refresh_token": "AB11686661598blahblah",
18 "refresh_token_expires_in": 8726400,
19 "token_type": "bearer"
20}
21
22// Store immediately in User record:
23// qb_access_token = response.access_token
24// qb_refresh_token = response.refresh_token
25// qb_realm_id = realmId from URL parameter ← Do NOT forget this
26// qb_token_issued_at = Current date/time

Pro tip: The base64 encoding of clientId:clientSecret for the Authorization header must NOT include the 'Basic ' prefix in the encoded string itself — the header value is 'Basic ' + base64(clientId + ':' + clientSecret). In Bubble, you can pre-compute this base64 string and store it as an app-level constant since the clientId and clientSecret don't change.

Expected result: The /qb-callback page, when loaded, extracts the code and realmId from URL parameters, exchanges the code for tokens via the token endpoint, and saves access_token, refresh_token, realm_id, and token_issued_at to the User record. The user is then redirected to the dashboard.

4

Step 4: Configure the Bubble API Connector for QuickBooks data calls

Now configure the API Connector for the actual data endpoints. In the Bubble editor, go to **Plugins → API Connector**. Click **Add another API** and name it 'QuickBooks'. Set the base URL for sandbox: `https://sandbox-quickbooks.api.intuit.com` (switch to `https://quickbooks.api.intuit.com` for production). In the **Shared headers** section, add the `Authorization` header with value `Bearer [access_token]` where `access_token` is a parameter. Check the **Private** checkbox — this prevents the token from appearing in browser requests. Add a second shared header: `Accept: application/json` (required by QuickBooks IDS to return JSON instead of XML). Add a shared header: `Content-Type: application/json` for POST calls. Now add your first API call: click **Add a call**, name it 'Get Company Info'. Set method to GET and path to `/v3/company/[realm_id]/companyinfo/[realm_id]`. The `realm_id` parameter will be supplied at call time from the User's stored `qb_realm_id` field. Set **Use as: Data**. For the **Initialize call**, you need real values. In the test parameter fields: enter a real access_token from your test user's stored value, and enter the sandbox realmId you connected earlier. Click **Initialize call**. You should receive a 200 response with company name, address, and country. Bubble will auto-detect these fields. If you see a 401, your access_token has expired — run the token refresh workflow (built in Step 6) to get a fresh token first. If you see a 404, double-check the realm_id value matches what was stored during the OAuth callback.

api_connector_quickbooks.json
1// API Connector — QuickBooks group configuration
2{
3 "api_name": "QuickBooks",
4 "base_url": "https://sandbox-quickbooks.api.intuit.com",
5 "shared_headers": [
6 {
7 "name": "Authorization",
8 "value": "Bearer <access_token>",
9 "private": true
10 },
11 {
12 "name": "Accept",
13 "value": "application/json"
14 },
15 {
16 "name": "Content-Type",
17 "value": "application/json"
18 }
19 ]
20}
21
22// Initialize Call — GET Company Info:
23{
24 "method": "GET",
25 "path": "/v3/company/<realm_id>/companyinfo/<realm_id>",
26 "use_as": "Data"
27}
28
29// Successful response shape:
30{
31 "CompanyInfo": {
32 "CompanyName": "Sandbox Company_US_2",
33 "Country": "US",
34 "SyncToken": "5",
35 "Id": "1"
36 },
37 "time": "2026-07-10T08:00:00.000-07:00"
38}

Pro tip: Set both the sandbox and production base URLs as Bubble App Data constants named 'qb_base_url'. In your API Connector calls, reference this app data value for the base URL. When you are ready to go to production, update that one constant instead of editing every API call.

Expected result: The QuickBooks API Connector group is configured with a Private Authorization header and Accept: application/json. The 'Get Company Info' Initialize Call returns a 200 with company name and details. The call is initialized and fields are auto-detected.

5

Step 5: Add invoice query and create calls using IDS Query Language

Add the invoice query call. In your QuickBooks API Connector group, click **Add a call**, name it 'Query Invoices'. Set method to GET and path to `/v3/company/[realm_id]/query`. Add a query parameter named `query` with a default value of `SELECT * FROM Invoice MAXRESULTS 50 STARTPOSITION 1`. The `realm_id` and `query` parameters will be passed from your Bubble workflows. QuickBooks uses IDS Query Language (IQL), which is SQL-like with important differences: - Entity names are **singular PascalCase**: Invoice, Customer, Payment, Bill — not invoices or customers - Pagination uses **MAXRESULTS** (not LIMIT) and **STARTPOSITION** (1-based, not 0-based) - Filtering uses standard WHERE syntax: `WHERE DueDate < '2026-01-01'` or `WHERE Balance > '0.00'` - Sort: `ORDERBY MetaData.CreateTime DESC` Initialize this call with your test values to get Bubble to detect the response shape. The response contains a `QueryResponse` object with an `Invoice` array. For creating invoices, add another call named 'Create Invoice'. Set method to POST, path to `/v3/company/[realm_id]/invoice`. The JSON body needs at minimum: `Line` (array of line items with DetailType, Amount, SalesItemLineDetail), and `CustomerRef` (Id of the customer). Set **Use as: Action** for this call since it creates data rather than returning it for display. In your Bubble Repeating Group, set Type of content to 'QuickBooks — Query Invoices — QueryResponse Invoice' (the auto-detected type from the Initialize Call). Set Data source to 'Get data from external API → QuickBooks - Query Invoices' with `realm_id` set to Current User's qb_realm_id and your desired IQL query string.

invoice_calls.json
1// GET Invoice Query — API call configuration
2{
3 "method": "GET",
4 "path": "/v3/company/<realm_id>/query",
5 "params": {
6 "query": "SELECT * FROM Invoice WHERE Balance > '0.00' ORDERBY MetaData.CreateTime DESC MAXRESULTS 50 STARTPOSITION 1"
7 },
8 "use_as": "Data"
9}
10
11// Sample Invoice response shape:
12{
13 "QueryResponse": {
14 "Invoice": [
15 {
16 "Id": "1",
17 "DocNumber": "1001",
18 "TotalAmt": 150.00,
19 "Balance": 150.00,
20 "DueDate": "2026-08-01",
21 "CustomerRef": { "name": "Amy's Bird Sanctuary", "value": "1" },
22 "EmailStatus": "Delivered"
23 }
24 ],
25 "startPosition": 1,
26 "maxResults": 50,
27 "totalCount": 12
28 }
29}
30
31// POST Create Invoice — body structure
32{
33 "Line": [
34 {
35 "DetailType": "SalesItemLineDetail",
36 "Amount": 150.00,
37 "SalesItemLineDetail": {
38 "ItemRef": { "value": "1", "name": "Services" }
39 }
40 }
41 ],
42 "CustomerRef": { "value": "<customer_id>" }
43}

Pro tip: To build pagination in Bubble: store the current STARTPOSITION as a custom state on your page (default: 1). Each time the user clicks 'Load more', add 50 to the state value and update the IQL query's STARTPOSITION parameter. Check if `totalCount` equals the current list length to hide the 'Load more' button when all invoices are loaded.

Expected result: A 'Query Invoices' API call is initialized and returns invoice data with amount, due date, and customer name fields auto-detected. A 'Create Invoice' action call is configured. A Repeating Group on your page displays the invoice list filtered to open balances.

6

Step 6: Build the token refresh Backend Workflow with atomic token save

This step is the most critical for long-term reliability. QuickBooks access tokens expire after 1 hour. When they expire, every API call returns 401 until refreshed. QuickBooks refresh tokens rotate — each use of a refresh token generates a new refresh token and invalidates the old one simultaneously. If you refresh and fail to save the new refresh_token before your workflow continues, the user must re-authorize from scratch. In Bubble's **Backend Workflows** section (Settings → API → 'Expose as a public API' must be enabled, and this requires a paid plan), create a new API Workflow named 'Refresh QuickBooks Token'. This workflow takes no parameters — it runs on behalf of the Current User. The workflow steps: 1. **Check if refresh is needed**: Add a condition — only run if Current User's qb_token_issued_at is more than 55 minutes ago (5 minutes buffer before the 1-hour expiry). 2. **Call the token refresh endpoint**: POST to `https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer` with body `grant_type=refresh_token&refresh_token=[Current User's qb_refresh_token]`. The Authorization header must be the same base64(clientId:clientSecret) as Step 3. 3. **Save both tokens atomically — this is the critical step**: In the same workflow action that processes the token response, immediately set Current User's `qb_access_token` to the response `access_token` AND `qb_refresh_token` to the response `refresh_token` AND `qb_token_issued_at` to Current date/time. Do NOT split this into two workflow steps — if the second step fails after the first succeeds, you lose the new refresh_token permanently. 4. **Call this workflow at the start of any workflow that touches QuickBooks**: Add 'Run Backend Workflow → Refresh QuickBooks Token' as the first action in any workflow that makes a QuickBooks API call. Bubble will wait for it to complete before continuing. RapidDev's team has built Bubble apps with complex OAuth token flows like this one. If you want a free architecture review before going live, visit rapidevelopers.com/contact.

token_refresh_workflow.json
1// Token Refresh POST request
2{
3 "method": "POST",
4 "url": "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
5 "headers": {
6 "Authorization": "Basic <base64(clientId:clientSecret)>",
7 "Content-Type": "application/x-www-form-urlencoded",
8 "Accept": "application/json"
9 },
10 "body": "grant_type=refresh_token&refresh_token=<Current User's qb_refresh_token>"
11}
12
13// Refresh response — SAME shape as initial token exchange:
14{
15 "access_token": "eyJlbmMiOiJBMTI4Q0...", // NEW access token
16 "expires_in": 3600,
17 "refresh_token": "AB11686661598newtoken", // NEW refresh token — save this NOW
18 "refresh_token_expires_in": 8726400,
19 "token_type": "bearer"
20}
21
22// Atomic save — BOTH tokens in one Make Changes to Current User step:
23// qb_access_token = API response.access_token
24// qb_refresh_token = API response.refresh_token ← CRITICAL: save the new one
25// qb_token_issued_at = Current date/time

Pro tip: If a user's refresh token expires (100 days of non-use) or gets invalidated due to a failed atomic save, they will see 400 errors from the refresh endpoint. Build a graceful error handler: if the refresh workflow returns an error, show a 'Please reconnect your QuickBooks account' message and trigger the OAuth flow from Step 2 again.

Expected result: A 'Refresh QuickBooks Token' Backend Workflow exists that checks token age, calls the QuickBooks refresh endpoint, and atomically saves both the new access_token and new refresh_token to the User record. Any workflow touching QuickBooks calls this first.

Common use cases

Invoice management portal for SMB clients

Build a Bubble app that lets business owners view their QuickBooks invoices, filter by status (paid, unpaid, overdue), and see customer details — all without logging into QuickBooks directly. Use the IDS query language to filter: SELECT * FROM Invoice WHERE DueDate < '2026-01-01' MAXRESULTS 50.

Bubble Prompt

Copy this prompt to try it in Bubble

Automated invoice creation from Bubble forms

When a user completes a project or sale in your Bubble app, automatically POST a new invoice to QuickBooks with the customer, line items, and due date. This eliminates manual double-entry between your Bubble CRM and QuickBooks accounting.

Bubble Prompt

Copy this prompt to try it in Bubble

Accounting dashboard with live QuickBooks data

Display revenue totals, outstanding receivables, and expense summaries pulled from QuickBooks in a Bubble dashboard. Query the CompanyInfo endpoint for business details, aggregate Invoice data for revenue metrics, and show Expense data for spending trends.

Bubble Prompt

Copy this prompt to try it in Bubble

Troubleshooting

Every QuickBooks API call returns 404 even with a valid access token

Cause: The realmId is missing or wrong in the URL path. Every QuickBooks IDS API call must include the company identifier in the URL: /v3/company/{realmId}/invoice. If you missed capturing the realmId from the OAuth callback URL or stored the wrong value, every call returns 404.

Solution: Check Current User's qb_realm_id field in Bubble's App Data viewer. If it is blank, you need to run through the OAuth flow again and ensure the /qb-callback page is correctly extracting the realmId URL parameter ('Get data from page URL' → parameter name: realmId). The realmId is a numeric string like '9341452234219068'. If you connected to a sandbox company, the realmId from the sandbox does not work against the production base URL and vice versa.

Users are getting logged out of QuickBooks randomly and need to re-authorize

Cause: The refresh token rotation is failing — a successful token refresh is not atomically saving the new refresh_token. The old refresh_token gets invalidated when the exchange succeeds, but if the save step fails, both the old and new tokens are effectively lost.

Solution: Audit your 'Refresh QuickBooks Token' Backend Workflow to ensure the 'Make changes to current user' step saves BOTH qb_access_token AND qb_refresh_token in the same action step, not in two separate steps. If the workflow has two separate 'Make changes' steps for the two tokens, combine them into one. Also check Bubble's Workflow Logs (Logs tab) for any failed workflow runs during the token refresh window to identify where the failure is occurring.

QuickBooks query returns 'InternalServerError' or empty results with IQL query

Cause: IDS Query Language has strict syntax requirements. Entity names must be singular PascalCase (Invoice, not invoices), pagination uses MAXRESULTS not LIMIT, and STARTPOSITION is 1-based not 0-based. A typo in any of these causes query rejection.

Solution: Verify your IQL query syntax. Correct examples: 'SELECT * FROM Invoice MAXRESULTS 50 STARTPOSITION 1' or 'SELECT * FROM Customer WHERE Active = true MAXRESULTS 100 STARTPOSITION 1'. Test your IQL query in the Intuit API Explorer at developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities before putting it in Bubble. Also verify that the Accept: application/json header is present — without it, QuickBooks returns XML which Bubble cannot parse.

typescript
1// Common IQL mistakes and corrections:
2// WRONG: SELECT * FROM Invoices LIMIT 50 OFFSET 0
3// RIGHT: SELECT * FROM Invoice MAXRESULTS 50 STARTPOSITION 1
4
5// WRONG: SELECT * FROM invoice WHERE balance > 0
6// RIGHT: SELECT * FROM Invoice WHERE Balance > '0.00'
7
8// WRONG: ORDER BY Date DESC
9// RIGHT: ORDERBY MetaData.CreateTime DESC

'Workflow API is not enabled' when trying to create Backend Workflows

Cause: Backend Workflows require a paid Bubble plan. The free plan does not support this feature, which is essential for OAuth token exchange and rotation-safe refresh.

Solution: Upgrade to the Bubble Starter plan (minimum ~$32/month) to enable Backend Workflows. Go to Settings → API in your Bubble editor. If the option 'This app exposes a Workflow API' appears but is greyed out, your current plan does not include it. Backend Workflows are required for the OAuth callback processing and token refresh — this integration cannot be completed on the free plan.

There was an issue setting up your call — Initialize Call fails with 401

Cause: The Initialize Call needs a real, currently-valid access token in the test parameter field. If the token you entered has expired (QuickBooks tokens last 1 hour), the call returns 401 and Bubble shows the generic initialization error.

Solution: Use a fresh access token for the Initialize Call. Run your token refresh workflow against your test user account to get a new token, then copy it from the User's qb_access_token field in Bubble's App Data viewer. Paste it as the test value for the access_token parameter in the Initialize Call. You only need to do this once — after the call is initialized, Bubble stores the response schema and doesn't need to re-initialize unless the response shape changes.

Best practices

  • Save BOTH access_token and refresh_token atomically in a single 'Make changes to current user' step after every token exchange — splitting this into two steps risks losing the new refresh_token if the second step fails, permanently locking the user out.
  • Store the realmId in the User record during the OAuth callback and never rely on re-fetching it — it appears only once in the callback URL and is required in the URL path of every single QuickBooks API call.
  • Add Privacy Rules immediately for all qb_ fields in the User data type (Data tab → Privacy → User → 'This User is Current User') to prevent token fields from being exposed to other app users through data queries.
  • Use the QuickBooks sandbox environment and sandbox company for all development and testing — sandbox and production use completely separate credentials, base URLs, and company data; never test against production accounting data.
  • Implement token age checking at the start of every workflow that calls QuickBooks: compare qb_token_issued_at to current time and trigger the refresh workflow if older than 55 minutes, giving a 5-minute buffer before the 1-hour expiry.
  • Cache the realmId in an App Data object in addition to the User record — if you are building a single-QuickBooks-company integration rather than a multi-user integration, this avoids the overhead of reading the User record on every API call.
  • Use Bubble's Workflow Logs (Logs tab → API calls) to monitor QuickBooks API call success rates and WU consumption — QuickBooks rate limits are per minute, and heavy polling (e.g. refreshing invoice lists on every page load) can approach limits for busy apps.
  • Add a 'Reconnect QuickBooks' button to your app's settings page that triggers the OAuth flow from Step 2 — users whose refresh tokens have expired (after 100 days of inactivity) need a way to re-authorize without contacting support.

Alternatives

Frequently asked questions

Do I need a paid QuickBooks subscription to use the API?

You need a QuickBooks Online subscription for production use (from ~$30/month). However, the Intuit Developer sandbox is free and includes a pre-populated test company with sample invoices and customers — ideal for building and testing your Bubble integration before going live. Create your Intuit Developer account at developer.intuit.com and use the sandbox indefinitely for development.

Why does my QuickBooks refresh token keep getting invalidated?

QuickBooks refresh tokens rotate on every use — each successful exchange issues a new refresh token and immediately invalidates the old one. If your Backend Workflow successfully refreshes the access token but fails to save the new refresh_token (e.g. due to a workflow error after the exchange), the old token is gone and cannot be recovered. The fix is an atomic save: both access_token and refresh_token must be saved in the same 'Make changes to user' step, before any other workflow action runs.

What happens if my app has multiple users each connected to different QuickBooks companies?

Each user's realmId, access_token, and refresh_token are stored separately in their User record. When a user's workflow calls QuickBooks, the API call uses that user's stored credentials. This pattern naturally supports multi-user, multi-company scenarios — each user's token lifecycle is independent. The QuickBooks rate limits (verify current limits at developer.intuit.com) also apply per company, so heavy usage by one user doesn't affect others.

Can I switch between the QuickBooks sandbox and production environments?

Yes, but they are completely separate environments with different credentials and different base URLs. Sandbox base URL: https://sandbox-quickbooks.api.intuit.com. Production base URL: https://quickbooks.api.intuit.com. You also need a separate Client ID and Client Secret for production vs sandbox (both found in your Intuit Developer app's Keys & credentials page). When switching to production, update your API Connector base URL, use production credentials, and have users re-authorize against the production OAuth flow.

Why can't I use the free Bubble plan for this integration?

The OAuth 2.0 callback flow requires a Backend Workflow (API Workflow) to process the token exchange when the user returns from Intuit's authorization page. Backend Workflows are a paid-plan feature on Bubble — they are not available on the free plan. Additionally, the rotation-safe token refresh uses Backend Workflows on a schedule. The minimum required plan is Bubble Starter (~$32/month).

What is IDS Query Language and how is it different from SQL?

IDS Query Language (IQL) is QuickBooks' SQL-like query syntax for the /query endpoint. The key differences from standard SQL: entity names are singular PascalCase (Invoice not invoices), pagination uses MAXRESULTS and STARTPOSITION (1-based) instead of LIMIT and OFFSET, and sorting uses ORDERBY (one word). String values in WHERE clauses use single quotes. Test queries in the Intuit API Explorer at developer.intuit.com before putting them in Bubble to avoid 400 errors from syntax mistakes.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Integrations are where projects stall

We wire Bubble integrations like this one every week — auth, webhooks, edge cases included. Fixed-price builds from $13K, delivered in 6–10 weeks.

Talk to an integration engineer

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.