Integrating QuickBooks Online with Lovable uses Edge Functions to proxy the Intuit REST API with OAuth2 authentication. Store your Client ID, Client Secret, and refresh token in Cloud Secrets, then create Edge Functions to manage token refresh and proxy API calls for customers, invoices, and payments. QuickBooks has no native Lovable connector, but a well-structured Edge Function layer gives you access to full accounting data for custom financial dashboards.
Why integrate QuickBooks Online with Lovable?
QuickBooks Online powers the accounting for over 7 million businesses in the US and Canada. If your Lovable app serves clients or customers who use QuickBooks — or if you run a business yourself and want custom reporting beyond what QuickBooks's built-in dashboard provides — the QuickBooks API unlocks a powerful data layer: live invoice status, customer payment history, expense categorization, and profit-and-loss reports, all pulled into a custom UI you fully control.
The most common use cases for QuickBooks + Lovable integrations are custom client portals (let clients see and pay their outstanding invoices in a branded interface), project profitability dashboards (combine QuickBooks financial data with your own project metrics), automated invoice generation (trigger invoice creation when a project milestone is reached in your app), and data export tools (transform QuickBooks JSON data into custom reports or formats your accountant needs).
QuickBooks uses OAuth2 for authentication, which involves a one-time authorization step where a QuickBooks account owner approves your app's access. This generates a long-lived refresh token (valid 100 days for live apps) that your Edge Functions use to get fresh access tokens as needed. The setup takes longer than a simple API key integration, but the resulting connection is stable and production-ready.
Integration method
QuickBooks Online has no native Lovable connector. All authenticated QuickBooks API calls run through Supabase Edge Functions using OAuth2. The initial OAuth2 flow generates a refresh token, which is stored in Cloud Secrets and used by Edge Functions to obtain fresh access tokens before each API call. This architecture gives your Lovable app access to customers, invoices, payments, expenses, and financial reports via the QuickBooks REST API.
Prerequisites
- A Lovable project with Cloud enabled
- An Intuit Developer account — sign up free at developer.intuit.com
- A QuickBooks Online company (sandbox companies are created automatically for developers)
- A QuickBooks Developer app created in the Intuit Developer Dashboard
- Basic understanding of OAuth2 authorization flows (authorize → code → tokens → refresh)
Step-by-step guide
Create a QuickBooks Developer app and configure OAuth2
Create a QuickBooks Developer app and configure OAuth2
Go to developer.intuit.com and sign in or create an Intuit Developer account. In the dashboard, click 'Create an app'. Select 'QuickBooks Online and Payments' as the platform. Give your app a name (e.g., 'My Lovable Dashboard') and click 'Create app'. In the app settings, navigate to the 'Keys & OAuth' section. You'll see your Client ID and Client Secret for both Sandbox and Production environments. Copy the Sandbox Client ID and Client Secret for development. Critically, you also need to configure your Redirect URI — this is the URL QuickBooks sends the authorization code to after the user approves access. For the OAuth2 callback, you'll create an Edge Function endpoint. Enter your Edge Function URL: https://[your-project].supabase.co/functions/v1/quickbooks-oauth (you'll create this function shortly). You can also add http://localhost:3000/auth/quickbooks/callback for local development if needed. In the 'Scopes' section, select the data your app needs. For accounting access (invoices, customers, payments), select 'com.intuit.quickbooks.accounting'. For payment processing, add 'com.intuit.quickbooks.payment'. Click 'Save' after configuring scopes.
Pro tip: QuickBooks Sandbox provides pre-populated test company data including sample customers, invoices, and transactions. This is much more useful than an empty sandbox — use the Sandbox credentials throughout all development and testing.
Expected result: Your QuickBooks app is created with Client ID, Client Secret, and Redirect URI configured. You have both Sandbox Client ID and Client Secret copied.
Store QuickBooks credentials and complete the OAuth2 flow
Store QuickBooks credentials and complete the OAuth2 flow
Open your Lovable project, click '+' → Cloud → Secrets, and add: QUICKBOOKS_CLIENT_ID (your Sandbox Client ID), QUICKBOOKS_CLIENT_SECRET (your Sandbox Client Secret), QUICKBOOKS_ENVIRONMENT with value 'sandbox'. The base URLs are https://sandbox-quickbooks.api.intuit.com for Sandbox and https://quickbooks.api.intuit.com for Production. QuickBooks OAuth2 is a standard three-leg flow: (1) build an authorization URL and redirect the user to it, (2) QuickBooks redirects back to your redirect URI with an authorization code, (3) exchange the code for access token and refresh token. For a single-user integration (your own QuickBooks account connected to your app), you complete this flow once and store the resulting refresh token. To get your initial refresh token: build the authorization URL manually using your Client ID, scopes, and redirect URI, then open it in your browser. It will look like: https://appcenter.intuit.com/connect/oauth2?client_id=YOUR_CLIENT_ID&response_type=code&scope=com.intuit.quickbooks.accounting&redirect_uri=YOUR_REDIRECT_URI&state=randomstate. After approving, QuickBooks redirects to your redirect URI with a code parameter. For the Sandbox flow, you can use Intuit's OAuth 2.0 Playground at developer.intuit.com/app/developer/playground to get a Sandbox refresh token without building the full redirect handler. Use the playground to exchange the authorization code for tokens, then save the refresh_token value as QUICKBOOKS_REFRESH_TOKEN in Cloud Secrets, and the realm_id (company ID) as QUICKBOOKS_REALM_ID.
Pro tip: QuickBooks refresh tokens are valid for 100 days in production and 100 days in sandbox. Store your refresh token in Supabase (not just Cloud Secrets) with a last_refreshed timestamp so you can proactively renew it before it expires and avoid losing access to a connected account.
Expected result: QUICKBOOKS_CLIENT_ID, QUICKBOOKS_CLIENT_SECRET, QUICKBOOKS_REFRESH_TOKEN, QUICKBOOKS_REALM_ID, and QUICKBOOKS_ENVIRONMENT are all set in Cloud Secrets.
Create the QuickBooks token refresh and API proxy Edge Function
Create the QuickBooks token refresh and API proxy Edge Function
QuickBooks access tokens expire after 1 hour. Every Edge Function that calls the QuickBooks API must first get a fresh access token by calling Intuit's token endpoint with your refresh token. This pattern — refresh then call — runs inside each Edge Function invocation, ensuring you always have a valid token. Create a single Edge Function that handles multiple QuickBooks API operations. It reads the refresh token from Deno.env (Cloud Secrets), calls the Intuit token endpoint to get a fresh access token, then proxies the requested operation. The function should support common actions: fetching customers, fetching invoices (with optional filters), creating invoices, and fetching payment records. The QuickBooks REST API uses a specific URL pattern: https://sandbox-quickbooks.api.intuit.com/v3/company/{realmId}/{entity} where realmId is your company ID and entity is 'invoice', 'customer', 'payment', etc. The API uses both REST-style endpoints and a SQL-like query language called the QuickBooks Query Language (QBQL) for filtering: SELECT * FROM Invoice WHERE Balance > 0. For RapidDev clients building multi-tenant QuickBooks integrations (where each of your customers connects their own QuickBooks company), the token management architecture is more complex — each user's refresh token needs to be stored encrypted in Supabase and retrieved per-user in the Edge Function. RapidDev's team can help architect this pattern.
Create a Supabase Edge Function at supabase/functions/quickbooks/index.ts. It should read QUICKBOOKS_CLIENT_ID, QUICKBOOKS_CLIENT_SECRET, QUICKBOOKS_REFRESH_TOKEN, QUICKBOOKS_REALM_ID, and QUICKBOOKS_ENVIRONMENT from Deno.env. First call Intuit's token endpoint (https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer) to get a fresh access token using the refresh token. Then support these actions: action='get-invoices' fetches open invoices using QBQL query SELECT * FROM Invoice WHERE Balance > 0; action='get-customers' fetches all customers; action='create-invoice' creates an invoice given customer_id, line items array, and due_date. Return structured JSON for each. Add CORS headers.
Paste this in Lovable chat
1// supabase/functions/quickbooks/index.ts2import { serve } from "https://deno.land/std@0.168.0/http/server.ts";34const IS_SANDBOX = Deno.env.get("QUICKBOOKS_ENVIRONMENT") !== "production";5const QB_BASE = IS_SANDBOX6 ? "https://sandbox-quickbooks.api.intuit.com"7 : "https://quickbooks.api.intuit.com";8const TOKEN_URL = "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer";9const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization" };1011async function getAccessToken(): Promise<string> {12 const clientId = Deno.env.get("QUICKBOOKS_CLIENT_ID") ?? "";13 const clientSecret = Deno.env.get("QUICKBOOKS_CLIENT_SECRET") ?? "";14 const refreshToken = Deno.env.get("QUICKBOOKS_REFRESH_TOKEN") ?? "";15 const credentials = btoa(`${clientId}:${clientSecret}`);16 const res = await fetch(TOKEN_URL, {17 method: "POST",18 headers: {19 Authorization: `Basic ${credentials}`,20 "Content-Type": "application/x-www-form-urlencoded",21 Accept: "application/json",22 },23 body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(refreshToken)}`,24 });25 const data = await res.json();26 if (!data.access_token) throw new Error(data.error_description ?? "Token refresh failed");27 return data.access_token;28}2930serve(async (req) => {31 if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });32 try {33 const { action, query, invoice } = await req.json();34 const token = await getAccessToken();35 const realmId = Deno.env.get("QUICKBOOKS_REALM_ID") ?? "";36 const headers = { Authorization: `Bearer ${token}`, Accept: "application/json", "Content-Type": "application/json" };3738 if (action === "get-invoices") {39 const sql = encodeURIComponent(query ?? "SELECT * FROM Invoice WHERE Balance > 0 MAXRESULTS 100");40 const res = await fetch(`${QB_BASE}/v3/company/${realmId}/query?query=${sql}&minorversion=65`, { headers });41 const data = await res.json();42 return new Response(JSON.stringify({ invoices: data.QueryResponse?.Invoice ?? [] }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });43 }4445 if (action === "get-customers") {46 const sql = encodeURIComponent("SELECT * FROM Customer WHERE Active = true MAXRESULTS 200");47 const res = await fetch(`${QB_BASE}/v3/company/${realmId}/query?query=${sql}&minorversion=65`, { headers });48 const data = await res.json();49 return new Response(JSON.stringify({ customers: data.QueryResponse?.Customer ?? [] }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });50 }5152 if (action === "create-invoice" && invoice) {53 const res = await fetch(`${QB_BASE}/v3/company/${realmId}/invoice?minorversion=65`, {54 method: "POST",55 headers,56 body: JSON.stringify(invoice),57 });58 const data = await res.json();59 return new Response(JSON.stringify({ invoice: data.Invoice }), { headers: { ...corsHeaders, "Content-Type": "application/json" } });60 }6162 return new Response(JSON.stringify({ error: "Unknown action" }), { status: 400, headers: corsHeaders });63 } catch (err) {64 return new Response(JSON.stringify({ error: err.message }), { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } });65 }66});Pro tip: Always pass minorversion=65 (the latest stable QuickBooks API minor version) in your API requests. Without it, QuickBooks defaults to an older schema that may not include newer fields and can cause unexpected empty responses for recently added features.
Expected result: The quickbooks Edge Function is deployed. Calling it with action='get-customers' returns your QuickBooks Sandbox company's customer list. Calling action='get-invoices' returns open invoices.
Build an invoice dashboard in Lovable
Build an invoice dashboard in Lovable
With the Edge Function in place, build the frontend invoice management view. Ask Lovable to create a dashboard page that fetches and displays open invoices, shows customer names, amounts due, and due dates, and provides action buttons for common operations. The invoice data from QuickBooks is rich — each invoice record includes the customer reference (CustomerRef), line items (Line array), balance due, total amount, due date, invoice number (DocNumber), and a link to the customer-facing invoice PDF (InvoiceLink). Display the most relevant fields in a table and add a Detail panel or modal for individual invoice information. For the invoice creation form, you'll need to look up customers from QuickBooks to populate a dropdown — call the get-customers action to populate the selector, then construct the invoice payload with the correct QuickBooks object structure. A minimal QuickBooks invoice object requires: CustomerRef (the customer's QuickBooks ID), Line array with at least one line item (each line needs Amount, DetailType='SalesItemLineDetail', and a SalesItemLineDetail with ItemRef and UnitPrice), and optionally DueDate. Refresh the invoice list after successful creation to show the new invoice immediately without a page reload.
Build an invoice management dashboard at /invoices. On load, call the quickbooks Edge Function with action='get-invoices' to fetch all invoices where Balance > 0. Display them in a table with columns: Invoice #, Customer Name, Amount, Balance Due, Due Date, and Status. Add a Create Invoice button that opens a form: customer dropdown (populated by action='get-customers'), description field, amount field, and due date. On submit, call the quickbooks Edge Function with action='create-invoice' and the proper QuickBooks invoice payload. Refresh the list after creation.
Paste this in Lovable chat
Pro tip: QuickBooks customer names are stored in the DisplayName field of the CustomerRef object. When displaying invoices, join the CustomerRef.name field (which QuickBooks includes inline) rather than making separate customer lookups.
Expected result: The invoice dashboard displays open invoices from QuickBooks Sandbox with customer names, amounts, and due dates. The Create Invoice form successfully creates a new invoice visible in both the Lovable dashboard and the QuickBooks Sandbox UI.
Handle OAuth2 token expiration and renewal
Handle OAuth2 token expiration and renewal
QuickBooks access tokens expire after 60 minutes and refresh tokens expire after 100 days. For a personal integration where you're the only QuickBooks user, the refresh token stored in Cloud Secrets handles access token renewal automatically on each Edge Function call. However, the refresh token itself eventually expires — at 100 days, your app will lose access until you re-authorize. For production resilience, store the refresh token in Supabase with a last_refreshed timestamp, and update it whenever the token endpoint returns a new refresh_token (Intuit sometimes rotates it). Check the expiry date before it lapses and send yourself an email reminder to re-authorize. For multi-user integrations where each customer connects their own QuickBooks account (OAuth2 multi-tenant pattern), you need a full OAuth2 redirect flow: a 'Connect QuickBooks' button that redirects to Intuit's authorization URL, a redirect handler Edge Function that receives the code and exchanges it for tokens, and secure per-user token storage in Supabase. This architecture ensures each user's tokens are separate and can be revoked independently.
Create a Supabase table called quickbooks_tokens with columns: id, user_id, access_token, refresh_token, realm_id, expires_at, refresh_expires_at, created_at, updated_at. Add RLS policies so users can only read their own row. Create an Edge Function at supabase/functions/quickbooks-oauth/index.ts that handles the OAuth2 callback: receives the authorization code and state from QuickBooks redirect, exchanges it for tokens using QUICKBOOKS_CLIENT_ID and QUICKBOOKS_CLIENT_SECRET from Deno.env, saves all token fields to the quickbooks_tokens table using service role key, and redirects the user to /dashboard on success.
Paste this in Lovable chat
Pro tip: Set a calendar reminder 90 days after your initial QuickBooks connection to re-authorize before the refresh token expires. Losing access to your accounting data mid-month due to an expired token is disruptive and avoidable with proactive renewal.
Expected result: Your token management system handles automatic access token refresh. The app continues working for up to 100 days without re-authorization. A re-auth reminder mechanism is in place before the refresh token expires.
Common use cases
Customer invoice portal with online payment
A freelancer or agency wants to give clients a branded portal where they can see all outstanding invoices, download PDFs, and pay online without logging into QuickBooks directly. The Edge Function fetches unpaid invoices from QuickBooks filtered by customer email, displays them in a clean interface, and when the client pays (via Stripe), calls the QuickBooks API to record the payment against the invoice.
Build a client invoice portal at /invoices. The Edge Function should fetch all open invoices from QuickBooks for a customer identified by their email address, return invoice number, amount due, due date, and line items. Display them in a table with a Pay Now button. When payment is made via Stripe, call another Edge Function to record the payment in QuickBooks using the Create Payment API, linking it to the invoice.
Copy this prompt to try it in Lovable
Automated invoice creation from project milestones
A project management tool wants to automatically create QuickBooks invoices when a project milestone is marked complete. An Edge Function triggered by a Supabase database webhook reads the milestone details, constructs a QuickBooks invoice with the appropriate service line items and customer data, creates it via the API, and saves the QuickBooks invoice ID back to the database for tracking.
When a project milestone is marked complete in the milestones table, trigger an Edge Function that: 1) fetches the project's customer record from QuickBooks using their email, 2) creates a QuickBooks invoice with the milestone name as the service description, the milestone price as the amount, and a due date 30 days from today, 3) saves the returned invoice ID and doc_number to the milestones table. Show a notification in the UI that the invoice was created.
Copy this prompt to try it in Lovable
Financial reporting dashboard with QuickBooks data
An e-commerce business owner wants a custom dashboard combining QuickBooks P&L data with their Shopify sales data in one view. An Edge Function fetches the QuickBooks Profit and Loss report for the current month, formats it as structured data, and the frontend renders it alongside other business metrics. The dashboard is refreshed daily and saved to Supabase for historical trend analysis.
Create a financial dashboard that pulls this month's Profit and Loss report from QuickBooks via an Edge Function. Use the QuickBooks Reports API endpoint /reports/ProfitAndLoss with date_macro=This Month. Return total income, total expenses, and net income. Display these three numbers prominently at the top of the /dashboard page with sparkline charts showing the last 6 months of data fetched from our Supabase financial_snapshots table.
Copy this prompt to try it in Lovable
Troubleshooting
Token refresh fails with 'AuthenticationFailed' or 'invalid_grant' error
Cause: The QUICKBOOKS_REFRESH_TOKEN in Cloud Secrets is expired (tokens last 100 days), was already used to get a new token and the old one invalidated, or was generated for a different app than the one specified by QUICKBOOKS_CLIENT_ID.
Solution: You need to re-authorize by going through the OAuth2 flow again. Use the Intuit OAuth 2.0 Playground at developer.intuit.com/app/developer/playground to get a new refresh token for your Sandbox company, or build a 'Reconnect QuickBooks' flow in your app that triggers the authorization redirect. Update QUICKBOOKS_REFRESH_TOKEN in Cloud Secrets with the new value.
QuickBooks API returns 401 even with a valid access token
Cause: The Authorization header format is incorrect, or the realm_id in the URL doesn't match the company the token was issued for. QuickBooks access tokens are realm-specific — a token for company A cannot access company B's data.
Solution: Verify QUICKBOOKS_REALM_ID matches the company ID that appears in your QuickBooks URL (the number after /app/companyName/ or from the token endpoint response's realmId field). Ensure the Authorization header is exactly 'Bearer {access_token}' — not 'Basic' and not with extra spaces.
Invoice creation fails with 'Business Validation Error: QBException object'
Cause: The invoice payload is missing required fields or references a customer or item ID that doesn't exist in the QuickBooks company. Common issues include missing CustomerRef, invalid Line item structure, or referencing an Item ID that was deleted in QuickBooks.
Solution: Check the error details returned in the Fault.Error array of QuickBooks's error response — each error includes a code, message, and detail field pinpointing the issue. Verify the CustomerRef.value is a valid QuickBooks customer ID from your get-customers call. Ensure each Line item has all required fields: Amount, DetailType='SalesItemLineDetail', and a valid SalesItemLineDetail.ItemRef.value.
1// Minimal valid invoice payload for QuickBooks2const invoicePayload = {3 CustomerRef: { value: "1" }, // Must be a valid customer ID4 Line: [{5 Amount: 100.00,6 DetailType: "SalesItemLineDetail",7 SalesItemLineDetail: {8 ItemRef: { value: "1", name: "Services" }, // Must be a valid item ID9 UnitPrice: 100.00,10 Qty: 111 }12 }]13};QuickBooks API QBQL query returns empty results even though invoices exist
Cause: The QBQL query syntax has an error, the filter condition doesn't match any records, or the minorversion parameter is missing causing an older schema to be used. QuickBooks Sandbox uses different data than Production — records that exist in your real account don't exist in Sandbox.
Solution: Start with the simplest possible query: SELECT * FROM Invoice MAXRESULTS 10 to confirm the connection works and data exists. If that works, add filters one at a time. Remember that Sandbox has pre-populated sample data, not your real invoices. Ensure minorversion=65 is included in the query URL.
Best practices
- Store the QuickBooks refresh token in Supabase with an expiry timestamp rather than only in Cloud Secrets, so you can proactively detect and handle token expiration before it breaks your app.
- Always include minorversion=65 in QuickBooks API requests to ensure consistent field availability and avoid deprecated schema behavior.
- Use the QuickBooks Query Language (QBQL) with MAXRESULTS to paginate large datasets rather than fetching all records at once — QuickBooks has a 1,000-record per query limit and large queries are slow.
- Cache QuickBooks data in Supabase for non-real-time use cases. Financial reports, customer lists, and invoice history change slowly — fetching them from Supabase is faster than re-calling QuickBooks on every page load.
- Use the Intuit OAuth 2.0 Playground during development to quickly generate Sandbox tokens without building the full redirect flow — this saves hours when you just need to test API calls.
- Test your integration against QuickBooks Sandbox before connecting to a real company account. Sandbox provides representative data and you can create and delete records freely without affecting real accounting data.
- Handle QuickBooks API rate limits gracefully — the API allows 500 requests per minute per realm. Add retry logic with exponential backoff for 429 responses rather than failing immediately.
Alternatives
Xero is preferred by businesses in the UK, Australia, and New Zealand, and uses OAuth2 PKCE for a slightly more secure authorization flow.
Stripe has a native Lovable connector and handles invoicing and payment collection with simpler setup if you don't need full accounting integration.
Plaid provides bank transaction data that can complement QuickBooks when you need to reconcile bank feeds with accounting records.
Frequently asked questions
Does QuickBooks have a sandbox for testing?
Yes. The Intuit Developer Dashboard automatically creates a Sandbox company when you sign up. This sandbox company has sample customers, invoices, and transactions you can use for development. Use the Sandbox Client ID and Client Secret from the developer dashboard and target the sandbox API URL (sandbox-quickbooks.api.intuit.com). Sandbox data is completely separate from any real QuickBooks company.
How long does it take to get QuickBooks Production access?
Intuit requires app review before you can connect real customer QuickBooks accounts. If you're building a personal tool for your own QuickBooks account, you can use your own credentials in production immediately. For apps where other users connect their QuickBooks accounts, you need to submit for Intuit's app review, which checks security, UX, and data handling practices. Review typically takes 5-15 business days.
Can I sync QuickBooks invoices to Supabase automatically?
Yes, in two ways. First, you can create a Supabase scheduled Edge Function (using pg_cron) that calls your quickbooks Edge Function every hour to sync new invoices. Second, QuickBooks Webhooks (part of their Change Data Capture feature) can notify your app when data changes — create a webhook receiver Edge Function to get real-time notifications of new invoices, payments, and customer changes.
What's the difference between QuickBooks Online and QuickBooks Desktop?
QuickBooks Online is the cloud-based subscription version with a REST API that this tutorial covers. QuickBooks Desktop is the older installed software version with a separate QBSDK (IPC-based, Windows only) that works nothing like the REST API. This tutorial is exclusively for QuickBooks Online. If your users run QuickBooks Desktop, a different integration approach (typically using a desktop connector) is required.
How do I handle multiple customers each connecting their own QuickBooks?
This requires a multi-tenant OAuth2 implementation. Each user clicks a 'Connect QuickBooks' button in your Lovable app that initiates the OAuth2 flow with a state parameter identifying their user ID. Your OAuth callback Edge Function exchanges the code for tokens and stores them in a quickbooks_tokens table keyed by user_id. Subsequent API calls fetch the user's tokens from Supabase rather than from Cloud Secrets. This architecture requires more setup but enables a true multi-user QuickBooks integration.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation