# How to Integrate Bolt.new with QuickBooks Online

- Tool: Bolt.new
- Difficulty: Intermediate
- Time required: 45 minutes
- Last updated: April 2026

## TL;DR

Integrate Bolt.new with QuickBooks Online by creating a developer app at developer.intuit.com, implementing OAuth 2.0 through a Next.js API route (deployment required for the callback URL), and calling the QuickBooks REST API to read customers, create invoices, and query financial reports. Store your client ID and secret in your .env file and never expose them in client-side code.

## Building QuickBooks Accounting Features into Bolt.new Apps

QuickBooks Online has a comprehensive REST API that lets you read and write all the data your accounting team works with every day — customers, invoices, payments, expenses, vendors, and financial reports like profit and loss. Connecting a Bolt.new app to this API lets you build custom dashboards that surface accounting data in a context relevant to your specific business, automate invoice creation from your app's own records, and sync customer data between QuickBooks and your other systems.

The integration requires OAuth 2.0, which means your users authenticate through Intuit's login page rather than entering a username and password into your app. This is the right approach for security, but it introduces a practical constraint: the OAuth callback URL — where Intuit redirects users after they authorize access — must be a publicly accessible HTTPS URL. Bolt's browser-based WebContainer cannot receive incoming connections, so you must deploy to Netlify or Vercel before testing the login flow. Outbound API calls to read and write QuickBooks data work fine in development once you have a token.

QuickBooks Online enforces a rate limit of 500 requests per minute per realm (company). For most dashboard and reporting use cases this is generous, but batch operations that loop through many records should include delays between requests. The API uses a minor version system — always include the minorversion query parameter to ensure consistent behavior as Intuit evolves the API.

## Before you start

- A QuickBooks Online account (sandbox available free at developer.intuit.com — no paid subscription needed for testing)
- A developer app registered at developer.intuit.com with OAuth 2.0 credentials (Client ID and Client Secret)
- A deployed Bolt.new app on Netlify or Vercel with an HTTPS URL (required for the OAuth callback)
- Node.js 20 and a Next.js project (Bolt can generate this — prompt 'Create a Next.js app')
- The intuit-oauth npm package or manual OAuth 2.0 implementation for token management

## Step-by-step guide

### 1. Create a QuickBooks developer app and configure OAuth 2.0

Before writing any code, you need to register your application with Intuit's developer platform. Go to developer.intuit.com and sign in with your Intuit account (or create a free one — no QuickBooks subscription required for the sandbox environment). Click 'Create an app', select 'QuickBooks Online and Payments', give your app a name, and click 'Create app'.

In your app's dashboard, navigate to the Keys & credentials section. You will see two environments: Sandbox and Production. Start with Sandbox — it comes with pre-populated test data. Copy the Client ID and Client Secret for the Sandbox environment.

Now configure the redirect URI. This is the URL where Intuit sends users back to your app after they authorize access. In the Redirect URIs section, add two URIs: your deployed app's URL (e.g., https://your-app.netlify.app/api/quickbooks/callback) for production testing, and http://localhost:3000/api/quickbooks/callback for local development. You can add both and QuickBooks will accept either.

The scopes you need depend on your use case. For reading and writing invoices and customers, request: com.intuit.quickbooks.accounting. For payment processing, add com.intuit.quickbooks.payment. Start with just the accounting scope and add more later if needed. Write down the exact scope string — you will need it in your authorization URL.

```
// .env.local
QUICKBOOKS_CLIENT_ID=your_sandbox_client_id
QUICKBOOKS_CLIENT_SECRET=your_sandbox_client_secret
QUICKBOOKS_REDIRECT_URI=https://your-app.netlify.app/api/quickbooks/callback
QUICKBOOKS_ENVIRONMENT=sandbox
```

**Expected result:** Your developer app appears in the developer.intuit.com dashboard with Client ID, Client Secret, and at least one configured redirect URI.

### 2. Build the OAuth 2.0 authorization and callback API routes

The QuickBooks OAuth 2.0 flow has two legs. First, your app redirects the user to Intuit's authorization page where they log in and grant access. Second, Intuit redirects back to your callback URL with an authorization code, which your server exchanges for access and refresh tokens. Both legs must happen server-side to keep your client secret secure.

Create two Next.js API routes: one at app/api/quickbooks/authorize/route.ts that constructs the authorization URL and redirects the user, and one at app/api/quickbooks/callback/route.ts that handles the redirect from Intuit. The callback route receives the authorization code as a query parameter and exchanges it for tokens by making a POST request to Intuit's token endpoint.

The access token expires after one hour. The refresh token is valid for 100 days for Sandbox and 100 days for Production. Your app needs to store both tokens (along with the expiry time and the realmId, which identifies the QuickBooks company) and implement token refresh logic before making API calls. For a production app, store tokens in your database. For a prototype, storing in a server-side session or encrypted cookie is sufficient.

Important: the OAuth callback CANNOT be tested in Bolt's WebContainer preview because the WebContainer cannot receive incoming HTTP requests from Intuit's servers. Deploy to Netlify or Vercel first, add the deployed URL as a redirect URI in your Intuit developer app, and test the auth flow on the deployed site.

```
// app/api/quickbooks/authorize/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  const clientId = process.env.QUICKBOOKS_CLIENT_ID;
  const redirectUri = process.env.QUICKBOOKS_REDIRECT_URI;
  const scope = 'com.intuit.quickbooks.accounting';
  const state = Math.random().toString(36).substring(2);

  const authUrl = new URL('https://appcenter.intuit.com/connect/oauth2');
  authUrl.searchParams.set('client_id', clientId!);
  authUrl.searchParams.set('redirect_uri', redirectUri!);
  authUrl.searchParams.set('response_type', 'code');
  authUrl.searchParams.set('scope', scope);
  authUrl.searchParams.set('state', state);

  return NextResponse.redirect(authUrl.toString());
}

// app/api/quickbooks/callback/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const code = searchParams.get('code');
  const realmId = searchParams.get('realmId');

  if (!code || !realmId) {
    return NextResponse.json({ error: 'Missing code or realmId' }, { status: 400 });
  }

  const clientId = process.env.QUICKBOOKS_CLIENT_ID!;
  const clientSecret = process.env.QUICKBOOKS_CLIENT_SECRET!;
  const redirectUri = process.env.QUICKBOOKS_REDIRECT_URI!;
  const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');

  const tokenResponse = await fetch('https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer', {
    method: 'POST',
    headers: {
      'Authorization': `Basic ${credentials}`,
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: redirectUri,
    }).toString(),
  });

  const tokens = await tokenResponse.json();
  // In production, store tokens and realmId in your database
  // For now, return them to confirm the flow works
  return NextResponse.json({ success: true, realmId, expiresIn: tokens.expires_in });
}
```

**Expected result:** Visiting /api/quickbooks/authorize redirects to Intuit's login page. After logging in and authorizing, you are redirected back to your callback URL and receive a JSON response confirming the token exchange succeeded.

### 3. Build API routes for reading customers and invoices

With tokens in hand, you can now call the QuickBooks REST API. All QuickBooks API calls follow the same URL pattern: https://quickbooks.api.intuit.com/v3/company/{realmId}/{resource}. For Sandbox, use https://sandbox-quickbooks.api.intuit.com/v3/company/{realmId}/{resource}. Always include the minorversion=70 query parameter to use the current API version and get consistent field names.

The QuickBooks API uses a SQL-like query language called the QuickBooks Query Language (QBQL) for listing and filtering records. To fetch all customers, you POST or GET a query like SELECT * FROM Customer WHERE Active = true MAXRESULTS 100. To fetch open invoices, use SELECT * FROM Invoice WHERE Balance > '0.00'.

Create server-side API routes in your Next.js app that accept requests from your React components, retrieve the stored QuickBooks tokens, check if the access token has expired (access tokens last one hour), refresh if needed, and then make the QuickBooks API call. This pattern keeps credentials server-side and out of the browser.

The response format is consistent: each resource comes back as a JSON object with a QueryResponse wrapper containing the resource name as a key (e.g., QueryResponse.Customer for customer queries, QueryResponse.Invoice for invoice queries) and TotalCount indicating the total number of matching records.

```
// app/api/quickbooks/invoices/route.ts
import { NextResponse } from 'next/server';

// In production, retrieve these from your database/session
const REALM_ID = process.env.QUICKBOOKS_REALM_ID;
const ACCESS_TOKEN = process.env.QUICKBOOKS_ACCESS_TOKEN; // For demo only
const IS_SANDBOX = process.env.QUICKBOOKS_ENVIRONMENT === 'sandbox';

const QB_BASE_URL = IS_SANDBOX
  ? `https://sandbox-quickbooks.api.intuit.com/v3/company/${REALM_ID}`
  : `https://quickbooks.api.intuit.com/v3/company/${REALM_ID}`;

export async function GET() {
  const query = "SELECT * FROM Invoice WHERE Balance > '0.00' MAXRESULTS 100";

  const response = await fetch(
    `${QB_BASE_URL}/query?query=${encodeURIComponent(query)}&minorversion=70`,
    {
      headers: {
        'Authorization': `Bearer ${ACCESS_TOKEN}`,
        'Accept': 'application/json',
      },
    }
  );

  if (!response.ok) {
    const error = await response.json();
    return NextResponse.json({ error }, { status: response.status });
  }

  const data = await response.json();
  const invoices = data.QueryResponse?.Invoice ?? [];

  return NextResponse.json({
    invoices,
    total: data.QueryResponse?.totalCount ?? 0,
  });
}
```

**Expected result:** Calling /api/quickbooks/invoices returns a JSON array of open invoices from your QuickBooks sandbox with fields like CustomerRef, TotalAmt, Balance, and DueDate.

### 4. Create invoices via the QuickBooks API

Creating an invoice in QuickBooks via the API requires a POST request to the invoice endpoint with a JSON body that includes at minimum: the customer reference (CustomerRef with the QuickBooks customer ID), at least one line item (Line array), and a currency code. The QuickBooks API is strict about required fields — missing any required field returns a 400 error with a descriptive error code in the response body.

Each line item in the Line array needs a DetailType of SalesItemLineDetail (for product/service items), an Amount (the total for that line), and a SalesItemLineDetail object with ItemRef (the QuickBooks item ID) and Qty and UnitPrice. If you do not have an ItemRef, you can use DetailType: SubTotalLineDetail for simple amount lines, but using actual QuickBooks items is preferred for proper P&L categorization.

The most common creation error is using a CustomerRef.value that does not exist in QuickBooks. Always fetch the customer list first and match by name or email before attempting invoice creation. Similarly, ItemRef values must correspond to actual items in your QuickBooks Items list — fetch them with SELECT * FROM Item WHERE Type = 'Service'.

Rate limiting note: the QuickBooks API allows 500 requests per minute per realm. For bulk invoice creation (e.g., generating 100 invoices from a CSV upload), add a 120ms delay between requests or batch process in groups with pauses to stay safely under the limit.

```
// app/api/quickbooks/invoices/create/route.ts
import { NextRequest, NextResponse } from 'next/server';

const REALM_ID = process.env.QUICKBOOKS_REALM_ID;
const ACCESS_TOKEN = process.env.QUICKBOOKS_ACCESS_TOKEN;
const IS_SANDBOX = process.env.QUICKBOOKS_ENVIRONMENT === 'sandbox';

const QB_BASE_URL = IS_SANDBOX
  ? `https://sandbox-quickbooks.api.intuit.com/v3/company/${REALM_ID}`
  : `https://quickbooks.api.intuit.com/v3/company/${REALM_ID}`;

interface LineItem {
  description: string;
  quantity: number;
  unitPrice: number;
  itemId: string; // QuickBooks Item ID
}

export async function POST(request: NextRequest) {
  const { customerId, lineItems, dueDate } = await request.json() as {
    customerId: string;
    lineItems: LineItem[];
    dueDate: string; // YYYY-MM-DD
  };

  const invoiceBody = {
    CustomerRef: { value: customerId },
    DueDate: dueDate,
    Line: lineItems.map((item) => ({
      DetailType: 'SalesItemLineDetail',
      Amount: item.quantity * item.unitPrice,
      Description: item.description,
      SalesItemLineDetail: {
        ItemRef: { value: item.itemId },
        Qty: item.quantity,
        UnitPrice: item.unitPrice,
      },
    })),
  };

  const response = await fetch(`${QB_BASE_URL}/invoice?minorversion=70`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${ACCESS_TOKEN}`,
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
    body: JSON.stringify(invoiceBody),
  });

  if (!response.ok) {
    const error = await response.json();
    return NextResponse.json(
      { error: error.Fault?.Error?.[0]?.Message ?? 'Invoice creation failed' },
      { status: response.status }
    );
  }

  const data = await response.json();
  return NextResponse.json({
    invoiceId: data.Invoice.Id,
    docNumber: data.Invoice.DocNumber,
    total: data.Invoice.TotalAmt,
  });
}
```

**Expected result:** Submitting a POST request to /api/quickbooks/invoices/create with valid customer ID, line items, and due date creates a new invoice in QuickBooks sandbox and returns the invoice ID and doc number.

## Best practices

- Always test with QuickBooks Sandbox first — it comes with free test data and costs nothing. Only switch to Production credentials when you are confident the integration works correctly.
- Store OAuth tokens in your database alongside their expiry timestamps, not in environment variables — tokens expire and must be refreshed, and environment variables cannot be updated at runtime.
- Never expose your QuickBooks Client Secret or access token in client-side React components. All QuickBooks API calls must go through server-side Next.js API routes.
- Include minorversion=70 on all API calls to pin to a stable API version and avoid breaking changes when Intuit releases updates to the API contract.
- Implement token refresh proactively: check if the access token expires within the next 5 minutes before making any API call, and refresh it if so, rather than waiting for a 401 error.
- Handle the QuickBooks 500 requests-per-minute rate limit in batch operations by adding a 120ms delay between requests, which keeps throughput at about 490 requests per minute safely under the limit.
- Log the QuickBooks realmId alongside your own user/company identifier so you can map between your users and their QuickBooks companies when supporting multiple connected accounts.

## Use cases

### Client invoice dashboard

Build a custom dashboard that pulls all open invoices from QuickBooks and displays them grouped by client with aging indicators (current, 30 days overdue, 60+ days). Sales reps can see at a glance which clients need follow-up without logging into QuickBooks itself.

Prompt example:

```
Create a Next.js app with a QuickBooks Online integration. Build an invoice dashboard that fetches all open invoices from the QuickBooks API, groups them by customer name, shows the amount due and due date, and highlights invoices that are more than 30 days past due in red.
```

### Automated invoice creation from order data

When a customer completes an order in your app, automatically create a corresponding invoice in QuickBooks with the correct line items, quantities, and prices. This eliminates manual data entry and ensures accounting records stay in sync with sales.

Prompt example:

```
Add a QuickBooks integration to my e-commerce app. When an order is marked as complete, call the QuickBooks API to create an invoice for that customer with the order line items. Map product names and prices from our order data to QuickBooks invoice line items.
```

### Profit and loss report viewer

Surface a simplified P&L report from QuickBooks in your internal business app, showing revenue, cost of goods, and net income for a selected date range. Finance team members who don't have QuickBooks access can view key metrics without needing a seat.

Prompt example:

```
Build a financial reporting page that fetches the Profit and Loss report from QuickBooks Online API for a user-selected date range and displays total income, total expenses, and net income. Use a clean card layout with month-over-month comparison.
```

## Troubleshooting

### OAuth redirect goes to Intuit login but after authorizing shows 'Invalid redirect_uri' error

Cause: The redirect URI your code sends in the authorization request does not exactly match one of the URIs registered in your developer.intuit.com app. QuickBooks performs a strict string comparison including protocol, domain, path, and trailing slashes.

Solution: In your developer.intuit.com app, go to Keys & credentials and check the exact URIs listed under Redirect URIs. Copy one verbatim into your QUICKBOOKS_REDIRECT_URI environment variable. Even a missing trailing slash will cause this error. Also verify you are using the Sandbox credentials for sandbox testing and Production credentials for production.

### API calls return 401 Unauthorized even immediately after completing the OAuth flow

Cause: The access token was not stored correctly after the callback, the Bearer token format is wrong, or you are using Sandbox tokens against the Production API endpoint (or vice versa).

Solution: Verify the Authorization header format is exactly 'Bearer {access_token}' with a capital B and a space. Check that QUICKBOOKS_ENVIRONMENT is set to 'sandbox' and you are calling the sandbox-quickbooks.api.intuit.com URL. Log the raw token value on the server to confirm it is not undefined or truncated.

```
// Verify token format before API call:
console.log('Token prefix:', accessToken?.substring(0, 20)); // Should start with 'eyJ'
console.log('Token length:', accessToken?.length); // Should be ~800-1200 chars
```

### OAuth callback never fires — browser just shows a 404 after authorizing on Intuit's page

Cause: The OAuth flow is being tested in Bolt's WebContainer preview. Intuit redirects to your callback URL after authorization, but the WebContainer cannot receive incoming HTTP requests, so the redirect lands nowhere.

Solution: Deploy your app to Netlify or Vercel first. Add the deployed HTTPS URL (e.g., https://your-app.netlify.app/api/quickbooks/callback) as a redirect URI in your developer.intuit.com app. Set QUICKBOOKS_REDIRECT_URI to the deployed URL in your hosting platform's environment variables. Test the full OAuth flow on the deployed site, not in the Bolt preview.

### Invoice creation returns 400 with error 'Object Not Found: Something you're trying to use has been made inactive'

Cause: The CustomerRef.value or ItemRef.value in your invoice creation request refers to a QuickBooks entity that is either inactive or does not exist in the connected company.

Solution: Fetch the current customer list with 'SELECT * FROM Customer WHERE Active = true' and item list with 'SELECT * FROM Item WHERE Active = true' to get valid IDs. Never hardcode QuickBooks entity IDs — they differ between Sandbox companies and between different QuickBooks accounts.

## Frequently asked questions

### Can I test the QuickBooks integration in Bolt's preview before deploying?

Partially. Outbound API calls to read data from QuickBooks (invoice lists, customer data, reports) work fine in the Bolt preview once you have a valid access token. However, the OAuth 2.0 login flow — where Intuit redirects back to your app with an authorization code — requires a publicly accessible callback URL that the WebContainer cannot provide. Deploy to Netlify or Vercel first to complete the auth flow, then use the obtained token to test API calls.

### Does Bolt.new have a native QuickBooks integration?

No. QuickBooks is not one of Bolt's native connectors. You build the integration yourself using Next.js API routes that call the QuickBooks REST API. Bolt's AI can generate most of the boilerplate code when you describe what you need, but you will need to register your own developer app at developer.intuit.com.

### How do I handle QuickBooks token expiry in production?

QuickBooks access tokens expire after one hour. Before every API call, check if the token's expiry time (stored alongside the token) is within the next 5 minutes, and if so, call the token endpoint with grant_type: 'refresh_token' to get a new access token. Refresh tokens last 100 days; if the refresh token expires too, the user must re-authorize through the OAuth flow. Store the updated access token and its new expiry time in your database immediately after refreshing.

### What QuickBooks API plan do I need to access financial reports?

Reports like Profit and Loss, Balance Sheet, and Cash Flow are available on all QuickBooks Online plan tiers (Simple Start, Essentials, Plus, Advanced) through the Reports API endpoint at /reports/{reportName}. No special developer plan is required beyond a registered developer app. Note that some report types are only available on higher tiers of QuickBooks Online — for example, class and department tracking is only on Plus and Advanced.

### Can I connect multiple QuickBooks companies to one Bolt app?

Yes. Each QuickBooks company has a unique realmId returned during the OAuth callback. Store each user's tokens and realmId pair in your database associated with your app's user account. When making API calls, look up the realmId for the current user and include it in the API URL. This multi-tenant pattern is standard for apps that serve multiple businesses.

---

Source: https://www.rapidevelopers.com/bolt-ai-integrations/quickbooks
© RapidDev — https://www.rapidevelopers.com/bolt-ai-integrations/quickbooks
