The Embedded Finance Index is live: 6,292 companies and 1,383 providers, mapped across the stack. Explore the Index →

QuickBooks app review check / Requirements guide

The QuickBooks app security review, item by item

What each requirement asks, why reviewers care, how to check it yourself, and how to fix it in common stacks. Or run the free check on your app.

This is our plain-language reading of Intuit's published requirements, not official guidance. Requirements change, so confirm the current list in Intuit's developer documentation before you submit.

Server & transport

HTTPS enforced on every pageAutomated

All pages of the app must be served over HTTPS.

Every page, including marketing pages on the same host, should only be reachable over HTTPS. Plain HTTP should redirect permanently.

Check it yourself

curl -sI http://app.example.com | grep -iE "^(HTTP|location)"
# expect: HTTP/1.1 301 … and location: https://…

Fix it

nginx
server {
  listen 80;
  server_name app.example.com;
  return 301 https://$host$request_uri;
}
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Express
app.set('trust proxy', 1);
app.use((req, res, next) =>
  req.secure ? next() : res.redirect(301, `https://${req.headers.host}${req.url}`));

TLS 1.1 minimum, TLS 1.2+ recommendedAutomated

SSL must support TLS 1.1 or higher; TLS 1.2 with AES-256 and SHA-256 is recommended.

TLS 1.0 is below the minimum, and TLS 1.1 is deprecated everywhere else. Most managed platforms (Vercel, Cloudflare, AWS ALB with a modern policy) already refuse both.

Check it yourself

openssl s_client -connect app.example.com:443 -tls1_1 </dev/null 2>&1 | grep -E "Protocol|alert"
# a handshake failure here is what you want

Fix it

nginx
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
AWS ALB
Use the security policy ELBSecurityPolicy-TLS13-1-2-2021-06 (or newer) on the HTTPS listener.

Caching disabled on sensitive pagesAutomated

Use Cache-Control no-cache and no-store (not private) on SSL and sensitive pages.

`private` only stops shared caches; the browser can still store the page on disk. Anything showing QuickBooks data, tokens or account settings needs `no-store`.

Check it yourself

curl -sI -H "Cookie: session=…" https://app.example.com/dashboard | grep -i cache-control
# expect: no-store (and no-cache)

Fix it

Express
app.use(['/app', '/oauth'], (req, res, next) => {
  res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
  res.set('Pragma', 'no-cache');
  next();
});
Next.js
// next.config.js
headers: async () => [{
  source: '/app/:path*',
  headers: [{ key: 'Cache-Control', value: 'no-store, no-cache, must-revalidate' }],
}]
Django
from django.views.decorators.cache import never_cache

@never_cache
def dashboard(request): ...

TRACE and unused HTTP methods disabledAutomated

The web server must disable TRACE and other HTTP methods that are not used.

TRACE echoes the request back, including headers, which enables cross-site tracing. Turn off any method your app does not serve.

Check it yourself

curl -si -X TRACE https://app.example.com/ | head -1
# expect 405 or 501, not 200

Fix it

nginx
if ($request_method !~ ^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)$) {
  return 405;
}
Apache
TraceEnable off

Patches current; no credential or QuickBooks data loggingCode patterns

OS, web server and app server patches must be current. Never log user credentials or QuickBooks data.

Logs are often shipped to third-party services and kept for a long time. A logged refresh token is a working credential to a customer’s books.

Fix it

Node
// log a fingerprint, never the token
logger.info({ realm: hash(realmId), tokenTail: token.refresh_token.slice(-4) }, 'token refreshed');

Attack vulnerabilities

Cross-Site Request Forgery protectionCode patterns

State-changing requests (including the OAuth flow) must be protected against CSRF.

Intuit’s sample code passes a fixed `state` value. Shipping that means anyone can make a victim’s browser connect the attacker’s QuickBooks company to the victim’s account.

Fix it

Node (intuit-oauth)
// /connect
req.session.oauthState = crypto.randomBytes(32).toString('hex');
res.redirect(oauthClient.authorizeUri({ scope, state: req.session.oauthState }));

// /callback
if (!req.query.state || req.query.state !== req.session.oauthState) return res.redirect('/error');
delete req.session.oauthState;

Cross-Site Scripting (reflected & stored)Code patterns

Validate input and encode output to prevent reflected and stored XSS.

Customer names, memos and line descriptions come from QuickBooks users and can contain markup. Escape them like any other user input.

SQL and XML injectionCode patterns

Use parameterized queries and safe XML parsing.

This applies to your database and also to QuickBooks query strings. A customer name like O’Brien breaks an unescaped `WHERE DisplayName = '…'` query.

Authentication, session management, access controlSign-off

Authentication, session management and functional-level access control must be robust.

The most common real-world issue is an insecure direct object reference: changing a realmId or record id in a request shows another customer’s data. Check that ownership is enforced on every route.

Validated forwards and redirectsAutomated

Redirect logic must be validated so it cannot be abused as an open redirect.

Login and OAuth flows often take a `next` or `returnTo` parameter. Only allow relative paths or an allow-list of hosts.

Cookies & sessions

OAuth token management

Tokens never exposed or sharedCode patterns

Intuit OAuth tokens and customer-identifying information must not be exposed in the app or shared with other parties.

Tokens belong on the server. The browser should only ever hold an opaque session cookie, and a disconnect should revoke the tokens with Intuit.

Refresh token and realmId encrypted at restCode patterns

Refresh token and realmId must be encrypted (AES preferred, or 3DES) and stored in persistent storage.

The refresh token lasts for months and the realmId identifies the company. Both need encrypting before they reach your database or cache.

Fix it

Node (AES-256-GCM)
import crypto from 'node:crypto';
const key = Buffer.from(process.env.TOKEN_ENCRYPTION_KEY, 'base64'); // 32 bytes

export function encrypt(plain) {
  const iv = crypto.randomBytes(12);
  const c = crypto.createCipheriv('aes-256-gcm', key, iv);
  const data = Buffer.concat([c.update(plain, 'utf8'), c.final()]);
  return Buffer.concat([iv, c.getAuthTag(), data]).toString('base64');
}

export function decrypt(blob) {
  const buf = Buffer.from(blob, 'base64');
  const d = crypto.createDecipheriv('aes-256-gcm', key, buf.subarray(0, 12));
  d.setAuthTag(buf.subarray(12, 28));
  return Buffer.concat([d.update(buf.subarray(28)), d.final()]).toString('utf8');
}
Python (cryptography)
import os, base64
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

key = base64.b64decode(os.environ['TOKEN_ENCRYPTION_KEY'])

def encrypt(plain: str) -> str:
    nonce = os.urandom(12)
    return base64.b64encode(nonce + AESGCM(key).encrypt(nonce, plain.encode(), None)).decode()
Rails
class QuickbooksConnection < ApplicationRecord
  encrypts :refresh_token
  encrypts :realm_id, deterministic: true
end

Encryption key kept in separate configurationCode patterns

The AES key must be stored in a separate configuration file, not inline in code.

A key committed next to the ciphertext protects nothing. Load it from an environment variable, a config file outside the repo, or a secrets manager, and plan for rotation.

Token-endpoint redirect rule

Token-bearing endpoints respond with 302, not HTMLAutomated

Endpoints receiving tokens or sensitive data in URL parameters must not return HTML; they should 302 redirect to avoid Referer leaks.

Intuit sends the user back to your redirect URI with `code` and `realmId` in the query string. If that URL renders a page with any third-party asset (analytics, fonts, a CDN image), the browser can send the full URL in the Referer header.

Check it yourself

curl -si "https://app.example.com/oauth/callback?code=x&state=y&realmId=1" | grep -iE "^(HTTP|location|content-type)"
# expect: HTTP/1.1 302 and a location without code/realmId

Fix it

Express
app.get('/oauth/callback', async (req, res) => {
  try {
    await handleCallback(req);          // validate state, exchange code, store encrypted
    res.redirect(302, '/app/connected');
  } catch {
    res.redirect(302, '/app/connect-error'); // errors redirect too
  }
});

QuickBooks data usage

No third-party access to QuickBooks dataSign-off

The app must not give third parties access to customer QuickBooks data, via APIs or any other means.

This includes indirect sharing: sending invoices to an LLM API, piping payloads into product analytics, or error trackers capturing request bodies. Review every outbound integration that sits next to QuickBooks data.

Store QuickBooks data only for functional useSign-off

Do not export, save or store QuickBooks data for any purpose other than the functional use of the app.

Keep only what the feature needs, set retention periods, and delete data when a customer disconnects.

Building on QuickBooks

Last updated

This tool is provided as is, without warranty of any kind. It is a self-assessment aid and not legal, compliance or security advice. Results do not guarantee that an app meets Intuit's requirements or will be approved. You are responsible for your app's security and for confirming requirements against Intuit's official documentation. Only scan systems you own or are authorized to test.

Not affiliated with, endorsed by or sponsored by Intuit Inc. Intuit and QuickBooks are trademarks of Intuit Inc., used here only to describe what the tool checks against.

Launch accounting integrations in weeks, not months

Get started for free