Skip to main content
RapidDev - Software Development Agency
replit-integrationsStandard API Integration

How to Integrate Replit with QuickBooks

To integrate Replit with QuickBooks Online, create an OAuth 2.0 app in the Intuit Developer Portal, store client credentials in Replit Secrets (lock icon πŸ”’), implement the authorization code flow in your Express or Flask server, and use the QuickBooks Online API to create invoices, record payments, and sync customer data. This is an Advanced integration requiring careful OAuth token management.

What you'll learn

  • How to register a QuickBooks OAuth 2.0 app and configure sandbox vs production environments
  • How to implement the full OAuth 2.0 authorization code flow in Express or Flask
  • How to securely store and refresh QuickBooks access tokens in Replit Secrets
  • How to create invoices, record payments, and query customers via the QuickBooks API
  • How to handle token expiry and use the sandbox environment for safe testing
Book a free consultation
4.9Clutch rating ⭐
600+Happy partners
17+Countries served
190+Team members
Advanced18 min read60 minutesPaymentMarch 2026RapidDev Engineering Team
TL;DR

To integrate Replit with QuickBooks Online, create an OAuth 2.0 app in the Intuit Developer Portal, store client credentials in Replit Secrets (lock icon πŸ”’), implement the authorization code flow in your Express or Flask server, and use the QuickBooks Online API to create invoices, record payments, and sync customer data. This is an Advanced integration requiring careful OAuth token management.

Why Connect Replit to QuickBooks Online?

QuickBooks Online is used by over 8 million businesses to manage their accounting, making it the most widely-used bookkeeping platform for the apps your customers are likely to use. Connecting your Replit app to QuickBooks enables you to automate invoice creation when orders are placed, sync payment status in real time, import customers from your app into QuickBooks, and generate financial reports without manual data entry.

The QuickBooks Online Accounting API is a full-featured REST API that covers the entire accounting object model: customers, invoices, payments, bills, accounts, and more. Authentication uses OAuth 2.0 with authorization codes, which means the business owner must authorize your app once through a Intuit consent screen, after which your server can operate autonomously using refresh tokens that last up to 100 days. This is a more complex setup than a simple API key, but it is the standard approach for any integration that touches a user's financial data.

Because QuickBooks OAuth requires a stable callback URL, this integration must be deployed before the OAuth flow can be completed. Replit's deployment options (Autoscale or Reserved VM) provide the persistent HTTPS URL needed for the redirect URI. Careful token management β€” including automatic refresh and secure storage β€” is essential, and all of that complexity is covered in the steps below.

Integration method

Standard API Integration

You connect Replit to QuickBooks Online by registering an OAuth 2.0 app in the Intuit Developer Portal, implementing the authorization code flow in your server-side code to obtain access and refresh tokens, storing all credentials in Replit Secrets, and calling the QuickBooks Online Accounting API. The OAuth flow requires a deployed Replit URL with a stable callback URI β€” development URLs will not work for production. Token refresh must be handled automatically since access tokens expire after one hour.

Prerequisites

  • A Replit account with a Node.js or Python project and a deployed URL (required for OAuth callback)
  • An Intuit Developer account at https://developer.intuit.com (free to create)
  • A QuickBooks Online sandbox company (automatically created with your developer account)
  • Basic understanding of OAuth 2.0 authorization code flow
  • Familiarity with Express.js or Flask for server-side request handling

Step-by-step guide

1

Create a QuickBooks OAuth 2.0 App in the Intuit Developer Portal

Go to https://developer.intuit.com and sign in. Click 'Dashboard' and then 'Create an app'. Choose 'QuickBooks Online and Payments' as the platform. Give your app a name like 'Replit Accounting Integration'. Once created, navigate to the 'Keys & OAuth' section. You will see two environments: Sandbox (for development and testing) and Production (for real user data). Start with Sandbox. Copy the 'Client ID' and 'Client Secret' from the Sandbox section. The most important configuration step is adding your Redirect URI. This must be the exact URL where Intuit will send the authorization code after a user grants access. For development, you can temporarily use your Replit development URL, but for production this must be your stable deployed URL: https://your-app.replit.app/auth/callback. Click 'Add URI' and enter your callback URL. You can add multiple URIs β€” add both the development and deployed URLs. Under 'Scopes', select the permissions your app needs. For invoice and customer management, select 'com.intuit.quickbooks.accounting'. For payment processing, also select 'com.intuit.quickbooks.payment'. Your app will only be able to access data within the selected scopes. QuickBooks requires apps to go through a production approval process before connecting to real company data. During development, always use the Sandbox environment β€” it comes pre-loaded with a test company and sample data.

Pro tip: Create your Sandbox app credentials and Production credentials separately in the portal. Never use Production credentials during development β€” mistakes on production data are irreversible.

Expected result: You have a Sandbox Client ID, Client Secret, and a registered redirect URI in the Intuit Developer Portal.

2

Store QuickBooks Credentials in Replit Secrets

Open your Replit project and click the lock icon πŸ”’ in the left sidebar to open the Secrets pane. Add the following secrets: - Key: QB_CLIENT_ID β€” Value: your Sandbox Client ID from Intuit Developer Portal - Key: QB_CLIENT_SECRET β€” Value: your Sandbox Client Secret - Key: QB_REDIRECT_URI β€” Value: https://your-app.replit.app/auth/callback (your deployed callback URL) - Key: QB_ENVIRONMENT β€” Value: sandbox (change to 'production' when ready to go live) After completing the OAuth flow (Step 3), you will also store: - Key: QB_ACCESS_TOKEN β€” Value: the access token obtained from the OAuth flow - Key: QB_REFRESH_TOKEN β€” Value: the refresh token for obtaining new access tokens - Key: QB_COMPANY_ID β€” Value: the realm ID / company ID from the OAuth callback The Client Secret and access/refresh tokens are the most sensitive values β€” they grant full access to the connected QuickBooks company's financial data. Replit encrypts all Secrets with AES-256 at rest. Never log these values or include them in error messages that might appear in public logs. For a production app where multiple users connect their own QuickBooks accounts, you would not store tokens in static Replit Secrets. Instead, store per-user access tokens and refresh tokens in an encrypted database column, and look them up by user ID when making API calls. The Secrets approach works for single-account server-side integrations.

Pro tip: After adding Secrets in Replit, you must redeploy your app for the production deployment to pick them up. Changes in the workspace Secrets editor take effect immediately on the next local Run, but deployed apps require a fresh deployment.

Expected result: QB_CLIENT_ID, QB_CLIENT_SECRET, QB_REDIRECT_URI, and QB_ENVIRONMENT are visible in the Replit Secrets pane.

3

Implement the OAuth 2.0 Authorization Flow

QuickBooks uses the standard OAuth 2.0 authorization code flow. Your server needs two routes: one to redirect the user to Intuit's authorization page, and one callback route that receives the authorization code and exchanges it for tokens. The flow works as follows: your user visits /auth/quickbooks, which redirects them to Intuit's authorization URL. Intuit shows a consent screen. The user approves, and Intuit redirects back to your /auth/callback URL with an authorization code. Your server exchanges the code for access and refresh tokens. You store these tokens and use them for all subsequent API calls. The node-quickbooks library simplifies token management, but you can also implement the OAuth flow using the intuit-oauth library or plain HTTP calls. The example below uses the intuit-oauth npm package which handles URL generation, token exchange, and token refresh. Critically, access tokens expire after 60 minutes. Refresh tokens expire after 100 days. Your app must detect expired tokens (401 responses) and automatically use the refresh token to obtain a new access token. Each refresh also returns a new refresh token β€” you must save the latest refresh token every time you refresh.

auth.js
1const express = require('express');
2const OAuthClient = require('intuit-oauth');
3
4const app = express();
5app.use(express.json());
6
7// Initialize Intuit OAuth client from Replit Secrets
8const oauthClient = new OAuthClient({
9 clientId: process.env.QB_CLIENT_ID,
10 clientSecret: process.env.QB_CLIENT_SECRET,
11 environment: process.env.QB_ENVIRONMENT || 'sandbox', // 'sandbox' or 'production'
12 redirectUri: process.env.QB_REDIRECT_URI
13});
14
15// In-memory token storage (use a database in production)
16let authToken = null;
17let companyId = null;
18
19// Load tokens from Replit Secrets if already authorized
20if (process.env.QB_ACCESS_TOKEN && process.env.QB_REFRESH_TOKEN) {
21 authToken = {
22 access_token: process.env.QB_ACCESS_TOKEN,
23 refresh_token: process.env.QB_REFRESH_TOKEN
24 };
25 companyId = process.env.QB_COMPANY_ID;
26 console.log('QuickBooks tokens loaded from Secrets');
27}
28
29// Step 1: Redirect user to QuickBooks authorization page
30app.get('/auth/quickbooks', (req, res) => {
31 const authUri = oauthClient.authorizeUri({
32 scope: [OAuthClient.scopes.Accounting, OAuthClient.scopes.OpenId],
33 state: 'replit-qb-integration'
34 });
35 res.redirect(authUri);
36});
37
38// Step 2: Handle callback β€” exchange code for tokens
39app.get('/auth/callback', async (req, res) => {
40 const parseRedirect = req.url;
41 try {
42 const authResponse = await oauthClient.createToken(parseRedirect);
43 authToken = authResponse.getJson();
44 companyId = req.query.realmId;
45
46 console.log('QuickBooks authorized! Company ID:', companyId);
47 console.log('Access token expires in 60 minutes');
48 console.log('Store these in Replit Secrets:');
49 console.log('QB_ACCESS_TOKEN:', authToken.access_token);
50 console.log('QB_REFRESH_TOKEN:', authToken.refresh_token);
51 console.log('QB_COMPANY_ID:', companyId);
52
53 res.send('QuickBooks connected successfully! Check your console for tokens to save to Replit Secrets.');
54 } catch (err) {
55 console.error('OAuth callback error:', err);
56 res.status(500).send('Authorization failed: ' + err.message);
57 }
58});
59
60// Refresh the access token when it expires
61async function refreshAccessToken() {
62 try {
63 oauthClient.setToken(authToken);
64 const authResponse = await oauthClient.refresh();
65 authToken = authResponse.getJson();
66 console.log('QuickBooks token refreshed successfully');
67 // In production, persist the new tokens to your database or update Secrets via API
68 return authToken;
69 } catch (err) {
70 console.error('Token refresh failed:', err);
71 throw err;
72 }
73}
74
75// Helper: make authenticated API call with auto-refresh
76async function qbApiCall(url, options = {}) {
77 if (!authToken) throw new Error('QuickBooks not authorized β€” visit /auth/quickbooks first');
78
79 const makeRequest = async (token) => {
80 const env = process.env.QB_ENVIRONMENT || 'sandbox';
81 const baseUrl = env === 'production'
82 ? 'https://quickbooks.api.intuit.com'
83 : 'https://sandbox-quickbooks.api.intuit.com';
84
85 return fetch(`${baseUrl}${url}`, {
86 ...options,
87 headers: {
88 'Authorization': `Bearer ${token.access_token}`,
89 'Accept': 'application/json',
90 'Content-Type': 'application/json',
91 ...options.headers
92 }
93 });
94 };
95
96 let response = await makeRequest(authToken);
97
98 // Auto-refresh on 401
99 if (response.status === 401) {
100 console.log('Token expired β€” refreshing...');
101 authToken = await refreshAccessToken();
102 response = await makeRequest(authToken);
103 }
104
105 if (!response.ok) {
106 const error = await response.text();
107 throw new Error(`QuickBooks API error ${response.status}: ${error}`);
108 }
109
110 return response.json();
111}
112
113module.exports = { oauthClient, qbApiCall, getCompanyId: () => companyId };
114
115app.listen(3000, '0.0.0.0', () => {
116 console.log('QuickBooks OAuth server running on port 3000');
117 console.log('Visit /auth/quickbooks to connect your QuickBooks account');
118});

Pro tip: After completing the OAuth flow for the first time, copy the printed access token, refresh token, and company ID into Replit Secrets. This way, after a server restart, the app loads valid tokens from Secrets without requiring re-authorization.

Expected result: Visiting /auth/quickbooks redirects to the Intuit consent screen. After approving, the callback logs tokens to the console and responds with a success message.

4

Create Invoices and Query Data via the QuickBooks API

With OAuth tokens in place, you can call the QuickBooks Online Accounting API. The API uses company-scoped URLs β€” every endpoint includes the company ID (realm ID) in the path. For the Sandbox environment, use https://sandbox-quickbooks.api.intuit.com/v3/company/{companyId}/. For production, use https://quickbooks.api.intuit.com/v3/company/{companyId}/. The most commonly used operations are creating invoices, recording payments, and querying customers. Invoice creation requires referencing existing Customer and Item records by their QuickBooks IDs. Use the query endpoint to find customers by email or name before creating an invoice. QuickBooks uses a query language called IDS Query (similar to SQL) for searching and filtering records. All query requests use POST to /v3/company/{companyId}/query with the query string as a URL parameter. This is a notable quirk compared to most REST APIs.

quickbooks_api.js
1const { qbApiCall, getCompanyId } = require('./auth');
2const express = require('express');
3const app = express();
4app.use(express.json());
5
6// Find a customer by email
7async function findCustomerByEmail(email) {
8 const companyId = getCompanyId();
9 const query = `SELECT * FROM Customer WHERE PrimaryEmailAddr = '${email}'`;
10 const result = await qbApiCall(
11 `/v3/company/${companyId}/query?query=${encodeURIComponent(query)}&minorversion=65`
12 );
13 const customers = result.QueryResponse?.Customer || [];
14 return customers.length > 0 ? customers[0] : null;
15}
16
17// Create a new customer
18async function createCustomer(displayName, email, phone = '') {
19 const companyId = getCompanyId();
20 const customer = {
21 DisplayName: displayName,
22 PrimaryEmailAddr: { Address: email },
23 PrimaryPhone: phone ? { FreeFormNumber: phone } : undefined
24 };
25 const result = await qbApiCall(
26 `/v3/company/${companyId}/customer?minorversion=65`,
27 { method: 'POST', body: JSON.stringify(customer) }
28 );
29 return result.Customer;
30}
31
32// Create an invoice for a customer
33async function createInvoice(customerId, lineItems, dueDate = '') {
34 const companyId = getCompanyId();
35 const lines = lineItems.map((item, index) => ({
36 DetailType: 'SalesItemLineDetail',
37 Amount: item.quantity * item.unitPrice,
38 SalesItemLineDetail: {
39 ItemRef: { value: item.itemId, name: item.itemName },
40 Qty: item.quantity,
41 UnitPrice: item.unitPrice
42 }
43 }));
44
45 const invoice = {
46 CustomerRef: { value: customerId },
47 Line: lines,
48 DueDate: dueDate || undefined
49 };
50
51 const result = await qbApiCall(
52 `/v3/company/${companyId}/invoice?minorversion=65`,
53 { method: 'POST', body: JSON.stringify(invoice) }
54 );
55 return result.Invoice;
56}
57
58// Record a payment against an invoice
59async function recordPayment(customerId, invoiceId, amount, paymentDate) {
60 const companyId = getCompanyId();
61 const payment = {
62 CustomerRef: { value: customerId },
63 TotalAmt: amount,
64 TxnDate: paymentDate,
65 Line: [{
66 Amount: amount,
67 LinkedTxn: [{ TxnId: invoiceId, TxnType: 'Invoice' }]
68 }]
69 };
70
71 const result = await qbApiCall(
72 `/v3/company/${companyId}/payment?minorversion=65`,
73 { method: 'POST', body: JSON.stringify(payment) }
74 );
75 return result.Payment;
76}
77
78// API endpoints
79app.post('/invoices', async (req, res) => {
80 const { customerEmail, customerName, lineItems, dueDate } = req.body;
81 try {
82 let customer = await findCustomerByEmail(customerEmail);
83 if (!customer) {
84 customer = await createCustomer(customerName, customerEmail);
85 console.log(`Created new QuickBooks customer: ${customer.Id}`);
86 }
87 const invoice = await createInvoice(customer.Id, lineItems, dueDate);
88 res.json({ success: true, invoiceId: invoice.Id, docNumber: invoice.DocNumber });
89 } catch (err) {
90 console.error('Invoice creation error:', err);
91 res.status(500).json({ error: err.message });
92 }
93});
94
95app.post('/payments', async (req, res) => {
96 const { customerId, invoiceId, amount, paymentDate } = req.body;
97 try {
98 const payment = await recordPayment(customerId, invoiceId, amount, paymentDate);
99 res.json({ success: true, paymentId: payment.Id });
100 } catch (err) {
101 res.status(500).json({ error: err.message });
102 }
103});
104
105app.listen(3000, '0.0.0.0', () => console.log('QuickBooks API server running'));

Pro tip: Always include minorversion=65 as a query parameter on QuickBooks API calls. This pins you to the latest stable API version and ensures consistent behavior. Without it, some fields may be missing in responses.

Expected result: POST /invoices creates a new invoice in your QuickBooks Sandbox company and returns the invoice ID and document number.

5

Handle Token Refresh and Deploy for Production

QuickBooks access tokens expire after exactly 60 minutes. Your production app must detect token expiry (HTTP 401 responses) and automatically use the refresh token to get a new access token. The auto-refresh pattern is already included in the auth.js example above, but you also need to persist new tokens when they are refreshed. For a single-account server-side integration, you can update Replit Secrets programmatically using the Replit API, or simply log the new refresh token and update it manually. For multi-user apps, store tokens per user in a database encrypted column. Refresh tokens themselves expire after 100 days. If a refresh token expires (which happens if the app is inactive for 100 days or if the user revokes access), the entire OAuth flow must be repeated β€” the user must re-authorize. Set up monitoring or an admin alert for this case. For deployment, click 'Deploy' in Replit and choose Autoscale if your app handles invoicing requests triggered by users, or Reserved VM if it runs background sync jobs. The deployed URL (https://your-app.replit.app) must be added as an authorized redirect URI in the Intuit Developer Portal. During the Sandbox phase, you can test with up to 25 connected sandbox companies. When you are ready for production, submit your app for review in the Intuit Developer Portal β€” this typically takes 1-2 business days.

qb_token_refresh.py
1# Python example for token refresh using requests-oauthlib
2import os
3import requests
4import base64
5from datetime import datetime, timedelta
6
7CLIENT_ID = os.environ["QB_CLIENT_ID"]
8CLIENT_SECRET = os.environ["QB_CLIENT_SECRET"]
9ENVIRONMENT = os.environ.get("QB_ENVIRONMENT", "sandbox")
10
11# Token storage (in production, use a database)
12access_token = os.environ.get("QB_ACCESS_TOKEN", "")
13refresh_token = os.environ.get("QB_REFRESH_TOKEN", "")
14company_id = os.environ.get("QB_COMPANY_ID", "")
15
16def refresh_access_token() -> dict:
17 """Exchange refresh token for new access token."""
18 global access_token, refresh_token
19
20 token_url = "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer"
21 credentials = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
22
23 response = requests.post(
24 token_url,
25 headers={
26 "Authorization": f"Basic {credentials}",
27 "Content-Type": "application/x-www-form-urlencoded"
28 },
29 data={
30 "grant_type": "refresh_token",
31 "refresh_token": refresh_token
32 }
33 )
34 response.raise_for_status()
35 tokens = response.json()
36
37 access_token = tokens["access_token"]
38 refresh_token = tokens["refresh_token"] # Save the new refresh token!
39 print(f"Tokens refreshed. New access token expires in {tokens['expires_in']}s")
40 print(f"Update QB_REFRESH_TOKEN in Replit Secrets to: {refresh_token[:20]}...")
41 return tokens
42
43def api_call(endpoint: str, method: str = "GET", body: dict = None) -> dict:
44 """Make an authenticated QuickBooks API call with auto-refresh."""
45 global access_token
46
47 base = "https://sandbox-quickbooks.api.intuit.com" if ENVIRONMENT == "sandbox" \
48 else "https://quickbooks.api.intuit.com"
49 url = f"{base}/v3/company/{company_id}{endpoint}"
50
51 def make_request(token):
52 headers = {
53 "Authorization": f"Bearer {token}",
54 "Accept": "application/json",
55 "Content-Type": "application/json"
56 }
57 if method == "POST" and body:
58 return requests.post(url, headers=headers, json=body, params={"minorversion": 65})
59 return requests.get(url, headers=headers, params={"minorversion": 65})
60
61 response = make_request(access_token)
62
63 if response.status_code == 401:
64 print("Token expired β€” refreshing...")
65 refresh_access_token()
66 response = make_request(access_token)
67
68 response.raise_for_status()
69 return response.json()
70
71# Example: get company info
72if __name__ == "__main__":
73 result = api_call("/companyinfo/" + company_id)
74 print("Company:", result["CompanyInfo"]["CompanyName"])

Pro tip: After every successful token refresh, update QB_REFRESH_TOKEN in Replit Secrets. If you let the refresh token expire by not using it for 100 days, you will need to re-run the full OAuth flow.

Expected result: Token refresh succeeds without user interaction, and the new tokens are printed to the console for updating in Replit Secrets.

Common use cases

Automated Invoice Generation on Order

When a customer completes an order in your Replit web app, automatically create a matching invoice in QuickBooks with the customer's details, line items, and amount due. This eliminates manual invoice creation and ensures your accounting records stay in sync with your app's transaction history.

Replit Prompt

Build an Express API endpoint that receives order data (customer name, email, items, total) and creates a QuickBooks invoice for the order using the QuickBooks Online API with OAuth 2.0 tokens stored in Replit Secrets.

Copy this prompt to try it in Replit

Payment Status Sync

When a payment is recorded in your app (via Stripe or another payment processor), automatically mark the corresponding QuickBooks invoice as paid and record the payment amount. This keeps accounts receivable accurate without requiring your accountant to manually reconcile payments.

Replit Prompt

Create a webhook handler in Flask that receives Stripe payment events and creates a matching QuickBooks Payment record linked to the customer's open invoice, using the QuickBooks companyId and tokens from Replit Secrets.

Copy this prompt to try it in Replit

Customer and Revenue Reporting

Query QuickBooks data from a Replit app to display financial dashboards for business owners β€” total revenue this month, outstanding invoices, top customers by revenue. The QuickBooks API supports both per-object queries and report endpoints for pre-built financial summaries.

Replit Prompt

Write a Python Flask endpoint that calls the QuickBooks Reports API to return a profit and loss summary for the current month, formatted as JSON for display in a React dashboard.

Copy this prompt to try it in Replit

Troubleshooting

OAuth callback returns error: 'invalid_grant' or 'invalid redirect_uri'

Cause: The redirect URI used in the authorization request does not exactly match one of the URIs registered in the Intuit Developer Portal. Even minor differences (trailing slash, http vs https, different port) cause this error.

Solution: Go to the Intuit Developer Portal, open your app, and verify the registered redirect URIs. Check that QB_REDIRECT_URI in Replit Secrets exactly matches what is registered. For local development, register http://localhost:3000/auth/callback. For deployed apps, register https://your-app.replit.app/auth/callback.

QuickBooks API returns HTTP 401 after previously working

Cause: The access token has expired (tokens are valid for exactly 60 minutes). All API calls after expiry will return 401 until the token is refreshed.

Solution: Implement automatic token refresh: detect 401 responses, call the token refresh endpoint with the refresh token, update the stored access token, and retry the original request. The auth.js example in Step 3 includes this pattern in the qbApiCall helper function.

typescript
1// Auto-refresh on 401
2if (response.status === 401) {
3 authToken = await refreshAccessToken();
4 response = await makeRequest(authToken); // Retry with new token
5}

API returns 'Application authentication failed' in Sandbox but credentials look correct

Cause: You may be using Production credentials (Client ID/Secret) while calling the Sandbox API URL, or vice versa. The Intuit Developer Portal has separate credentials for Sandbox and Production.

Solution: Verify that QB_ENVIRONMENT in Replit Secrets is set to 'sandbox' and that the Client ID and Secret you copied come from the Sandbox section of the portal (not Production). The Sandbox API base URL is https://sandbox-quickbooks.api.intuit.com. Production is https://quickbooks.api.intuit.com.

typescript
1const baseUrl = process.env.QB_ENVIRONMENT === 'production'
2 ? 'https://quickbooks.api.intuit.com'
3 : 'https://sandbox-quickbooks.api.intuit.com';

Invoice creation fails with 'CustomerRef' validation error

Cause: The customer ID provided in the CustomerRef field does not exist in the connected QuickBooks company. Customer IDs from one company cannot be used in another, and Sandbox and Production have different customer databases.

Solution: Query for the customer by email or name before creating an invoice. If the customer does not exist, create them first. Use the findCustomerByEmail function from Step 4 to look up the customer ID before constructing the invoice payload.

typescript
1// Always look up customer before creating invoice
2let customer = await findCustomerByEmail(email);
3if (!customer) {
4 customer = await createCustomer(name, email);
5}
6await createInvoice(customer.Id, lineItems);

Best practices

  • Always use the Sandbox environment (separate credentials and API URL) for all development and testing β€” Production credentials access real company financial data.
  • Store all QuickBooks credentials (Client ID, Client Secret, access token, refresh token, company ID) in Replit Secrets β€” never in source code or Git history.
  • Implement automatic token refresh: detect 401 responses and call the refresh endpoint to get a new access token, then retry the original request transparently.
  • Save the new refresh token every time you refresh the access token β€” each refresh call returns a new refresh token that replaces the old one.
  • Include minorversion=65 on all QuickBooks API calls to pin to the latest stable API behavior and receive all available response fields.
  • Use the QuickBooks query endpoint to look up customers by email before creating invoices β€” this prevents duplicate customer records and respects QuickBooks' unique display name requirement.
  • For production deployments, use Reserved VM rather than Autoscale if you are running background sync processes that must not be interrupted by cold starts.
  • Register your app with Intuit for production access review before launching β€” without approval, your app can only connect to 25 sandbox companies.

Alternatives

Frequently asked questions

How do I store QuickBooks API credentials in Replit?

Click the lock icon πŸ”’ in the Replit sidebar to open Secrets. Add QB_CLIENT_ID, QB_CLIENT_SECRET, QB_REDIRECT_URI, and QB_ENVIRONMENT ('sandbox' or 'production'). After completing the OAuth flow, also add QB_ACCESS_TOKEN, QB_REFRESH_TOKEN, and QB_COMPANY_ID. Access them in Node.js with process.env.QB_CLIENT_ID or in Python with os.environ['QB_CLIENT_ID'].

Why does QuickBooks need OAuth instead of just an API key?

QuickBooks uses OAuth 2.0 because it integrates with multiple users' financial accounts, and OAuth ensures users explicitly consent to your app accessing their data without sharing their password. For internal tools where you only connect your own QuickBooks account, you still complete the OAuth flow once, then store the resulting tokens in Replit Secrets for automated use.

How long do QuickBooks access tokens last?

QuickBooks access tokens expire after exactly 60 minutes. Refresh tokens last 100 days. Your app must detect 401 responses (expired token) and automatically call the token refresh endpoint to get a new access token. Each refresh also returns a new refresh token β€” you must save it. If the refresh token expires due to inactivity (100 days), the full OAuth flow must be repeated.

Can I test QuickBooks integration on Replit for free?

Yes. The Intuit Developer Portal provides a free Sandbox environment with a sample QuickBooks company pre-loaded with test data. Create a developer account at developer.intuit.com, create an app, and use the Sandbox credentials and API URL (https://sandbox-quickbooks.api.intuit.com) for all development. No payment is required for sandbox access.

What Replit deployment type should I use for QuickBooks integration?

Use Autoscale deployment for web apps that create invoices or record payments in response to user actions. Use Reserved VM if you are running background sync processes that continuously reconcile data between your app and QuickBooks, since Reserved VMs are always on with no cold-start delays. Both deployment types give you the stable HTTPS URL required for the OAuth redirect URI.

How do I connect to the live QuickBooks API (not sandbox)?

First, submit your app for production review in the Intuit Developer Portal β€” this is required before connecting to real company data and typically takes 1-2 business days. Once approved, update QB_CLIENT_ID and QB_CLIENT_SECRET in Replit Secrets to your Production credentials, update QB_ENVIRONMENT to 'production', and run the OAuth flow again to get production tokens. Update the API base URL to https://quickbooks.api.intuit.com.

RapidDev

Talk to an Expert

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

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation β€” no strings attached.

Book a free consultation

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.