Skip to content
rajeshgupta723 edited this page Dec 15, 2025 · 3 revisions

Welcome to the NodeJS oauth-jsclient wiki!

Intuit OAuth Node.js Client - Frequently Asked Questions (FAQ)

Welcome to the Intuit OAuth Node.js Client FAQ! This guide answers common questions and provides solutions to frequently encountered issues.

Table of Contents

  1. Getting Started
  2. Authentication & Authorization
  3. Token Management
  4. Error Handling
  5. Making API Calls
  6. Troubleshooting
  7. Best Practices
  8. Migration & Upgrades
  9. Advanced Topics

Getting Started

What is this library?

The intuit-oauth library is a Node.js client that simplifies OAuth 2.0 and OpenID Connect authentication with Intuit services (QuickBooks Online). It handles token management, API calls, error handling, and automatic retries.

What version of Node.js do I need?

The library requires Node.js 10 or higher. Different versions are available for older Node.js versions:

  • Node 10+: intuit-oauth@4.x.x
  • Node 8-9: intuit-oauth@3.x.x
  • Node 7: intuit-oauth@2.x.x
  • Node 6: intuit-oauth@1.x.x

How do I install it?

npm install intuit-oauth --save

Where do I get my Client ID and Client Secret?

  1. Go to Intuit Developer Portal
  2. Create or select your app
  3. Navigate to the "Keys & OAuth" tab
  4. Copy your Client ID and Client Secret

What's the difference between sandbox and production?

  • Sandbox: Test environment with test companies and data. Use environment: 'sandbox'
  • Production: Live environment with real customer data. Use environment: 'production'

Always test thoroughly in sandbox before deploying to production!


Authentication & Authorization

How do I implement the OAuth flow?

The OAuth flow has two main steps:

Step 1: Redirect user to authorize

const OAuthClient = require('intuit-oauth');

const oauthClient = new OAuthClient({
  clientId: 'YOUR_CLIENT_ID',
  clientSecret: 'YOUR_CLIENT_SECRET',
  environment: 'sandbox',
  redirectUri: 'http://localhost:8000/callback'
});

// Generate authorization URL
const authUri = oauthClient.authorizeUri({
  scope: [OAuthClient.scopes.Accounting, OAuthClient.scopes.OpenId],
  state: 'random-state-string'
});

// Redirect user to authUri
res.redirect(authUri);

Step 2: Handle callback and exchange code for tokens

app.get('/callback', async (req, res) => {
  try {
    const authResponse = await oauthClient.createToken(req.url);
    const token = authResponse.getToken();
    // Store token securely
    console.log('Access token:', token.access_token);
  } catch (error) {
    console.error('Token exchange failed:', error);
  }
});

What scopes should I use?

Common scopes:

  • OAuthClient.scopes.Accounting - QuickBooks Accounting API
  • OAuthClient.scopes.Payment - QuickBooks Payments API
  • OAuthClient.scopes.Payroll - QuickBooks Payroll (beta)
  • OAuthClient.scopes.OpenId - User identity
  • OAuthClient.scopes.Email - User email
  • OAuthClient.scopes.Profile - User profile
  • OAuthClient.scopes.Phone - User phone
  • OAuthClient.scopes.Address - User address

You can request multiple scopes:

scope: [
  OAuthClient.scopes.Accounting,
  OAuthClient.scopes.OpenId,
  OAuthClient.scopes.Email
]

What is the state parameter and why is it important?

The state parameter is a CSRF protection mechanism:

  • Generate a random string when creating the authorization URL
  • Store it in session
  • Verify it matches when handling the callback
  • Prevents cross-site request forgery attacks
// Generate state
const state = oauthClient.state.create(oauthClient.state.secretSync());

// Store in session
req.session.state = state;

// Create authorization URL
const authUri = oauthClient.authorizeUri({ scope: scopes, state });

// Verify on callback
if (req.query.state !== req.session.state) {
  throw new Error('State mismatch - possible CSRF attack');
}

How do I get the realmId (Company ID)?

The realmId is returned in the callback URL and automatically stored in the token:

const authResponse = await oauthClient.createToken(req.url);
const token = authResponse.getToken();
console.log('RealmId:', token.realmId); // QuickBooks company ID

You'll need this for all API calls to QuickBooks.


Token Management

How long do tokens last?

  • Access Token: 1 hour (3600 seconds)
  • Refresh Token: 100 days (8,726,400 seconds)

Always check token validity before making API calls!

How do I check if my access token is valid?

if (oauthClient.isAccessTokenValid()) {
  console.log('Token is valid');
} else {
  console.log('Token expired - need to refresh');
}

How do I refresh an expired token?

if (!oauthClient.isAccessTokenValid()) {
  try {
    const authResponse = await oauthClient.refresh();
    const newToken = authResponse.getToken();
    // Store updated token
    console.log('New access token:', newToken.access_token);
  } catch (error) {
    console.error('Refresh failed:', error);
    // Redirect user to re-authorize
  }
}

Can I refresh using a specific refresh token?

Yes! This is useful when tokens are stored separately:

const authResponse = await oauthClient.refreshUsingToken('YOUR_REFRESH_TOKEN');
const newToken = authResponse.getToken();

How should I store tokens?

DO:

  • ✅ Encrypt tokens before storing
  • ✅ Store in a secure database
  • ✅ Use environment variables for sensitive data
  • ✅ Implement proper access controls
  • ✅ Store the realmId with the token

DON'T:

  • ❌ Store in plain text
  • ❌ Store in cookies without encryption
  • ❌ Store in client-side code
  • ❌ Share tokens between users
  • ❌ Log tokens in production

How do I restore a token from storage?

// Retrieve from database
const storedToken = await db.getToken(userId);

// Create client with stored token
const oauthClient = new OAuthClient({
  clientId: 'YOUR_CLIENT_ID',
  clientSecret: 'YOUR_CLIENT_SECRET',
  environment: 'sandbox',
  redirectUri: 'http://localhost:8000/callback',
  token: storedToken
});

// Or set token after creation
oauthClient.setToken(storedToken);

What happens when refresh tokens expire?

After 100 days (or 24 hours after refreshing), refresh tokens expire. When this happens:

  1. The refresh attempt will fail with invalid_grant
  2. You must redirect the user to re-authorize
  3. User will need to grant permissions again

How do I revoke tokens?

try {
  await oauthClient.revoke();
  console.log('Tokens revoked successfully');
  // Clear stored tokens
} catch (error) {
  console.error('Revoke failed:', error);
}

Error Handling

Does createToken return errors or throw them?

createToken() always throws errors, never returns them.

Correct:

try {
  const authResponse = await oauthClient.createToken(callbackUrl);
  const token = authResponse.getToken();
  // Success - use token
} catch (error) {
  // Error thrown - handle it
  console.error('Error:', error.error);
}

Wrong assumption:

// This is WRONG - errors are not returned
const authResponse = await oauthClient.createToken(callbackUrl);
if (authResponse.error) { // This will never happen
  // Error handling here won't work
}

What error information is available?

All errors include:

  • error.error - Error code (e.g., "invalid_grant")
  • error.error_description - Human-readable description
  • error.intuit_tid - Transaction ID for support
  • error.authResponse - Full response object
  • error.message - Error message
  • error.code - HTTP status code
try {
  await oauthClient.createToken(callbackUrl);
} catch (error) {
  console.log('Error code:', error.error);
  console.log('Description:', error.error_description);
  console.log('Transaction ID:', error.intuit_tid);
  console.log('HTTP Status:', error.code);
}

How do I handle specific OAuth errors?

try {
  const authResponse = await oauthClient.createToken(callbackUrl);
} catch (error) {
  switch (error.error) {
    case 'invalid_grant':
      // Authorization code expired/used - redirect to re-authorize
      res.redirect('/reauthorize');
      break;
    
    case 'invalid_client':
      // Wrong client ID/secret - check configuration
      console.error('Invalid credentials');
      break;
    
    case 'invalid_request':
      // Malformed request - check parameters
      console.error('Invalid request parameters');
      break;
    
    default:
      console.error('OAuth error:', error.error);
  }
}

What are QuickBooks Fault objects?

QuickBooks API returns Fault objects for validation errors:

try {
  await oauthClient.makeApiCall({
    url: `/v3/company/${realmId}/customer`,
    method: 'POST',
    body: JSON.stringify(customerData)
  });
} catch (error) {
  if (error.fault) {
    console.log('Fault Type:', error.fault.type);
    console.log('Number of errors:', error.fault.errors.length);
    
    error.fault.errors.forEach(err => {
      console.log('Message:', err.message);
      console.log('Detail:', err.detail);
      console.log('Code:', err.code);
    });
  }
}

What HTTP status codes are supported?

The library handles all common HTTP status codes:

  • 400: Bad Request (includes Fault objects)
  • 401: Unauthorized (invalid/expired token)
  • 403: Forbidden (insufficient permissions)
  • 404: Not Found
  • 429: Rate Limited
  • 500: Internal Server Error
  • 502: Bad Gateway
  • 503: Service Unavailable
  • 504: Gateway Timeout

All status codes throw appropriate errors with detailed information.

How do I use Transaction IDs for support?

When contacting QuickBooks support, provide the intuit_tid:

try {
  await oauthClient.makeApiCall(params);
} catch (error) {
  console.log('Please provide this Transaction ID to support:', error.intuit_tid);
  // Log this for your support team
  logger.error('API error', { 
    intuit_tid: error.intuit_tid,
    error: error.error,
    timestamp: new Date()
  });
}

Making API Calls

How do I make API calls to QuickBooks?

const token = oauthClient.getToken();
const realmId = token.realmId;

try {
  const response = await oauthClient.makeApiCall({
    url: `https://sandbox-quickbooks.api.intuit.com/v3/company/${realmId}/companyinfo/${realmId}`,
    method: 'GET',
    headers: {
      'Content-Type': 'application/json'
    }
  });
  
  console.log('Company info:', response.json);
} catch (error) {
  console.error('API call failed:', error);
}

Can I use relative URLs?

Yes! The library automatically prepends the correct base URL:

// Both work the same:
// Absolute URL
url: 'https://sandbox-quickbooks.api.intuit.com/v3/company/123/item'

// Relative URL (recommended)
url: '/v3/company/123/item'

// Without leading slash also works
url: 'v3/company/123/item'

The library uses the correct base URL based on your environment setting.

How do I make POST/PUT requests?

const customerData = {
  DisplayName: 'John Doe',
  PrimaryEmailAddr: { Address: 'john@example.com' }
};

const response = await oauthClient.makeApiCall({
  url: `/v3/company/${realmId}/customer`,
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(customerData)
});

console.log('Created customer:', response.json);

How does automatic retry work?

The library automatically retries failed requests:

Default Configuration:

  • Max retries: 3
  • Delay: 1s, 2s, 4s (exponential backoff)
  • Retryable status codes: 408, 429, 500, 502, 503, 504
  • Retryable errors: ECONNRESET, ETIMEDOUT, ECONNREFUSED

Customize retry behavior:

OAuthClient.retryConfig = {
  maxRetries: 5,
  retryDelay: 2000, // 2 seconds
  retryableStatusCodes: [408, 429, 500, 502, 503, 504],
  retryableErrors: ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED']
};

How do I handle PDF responses?

For PDF downloads (invoices, reports):

const response = await oauthClient.makeApiCall({
  url: `/v3/company/${realmId}/invoice/${invoiceId}/pdf`,
  method: 'GET',
  headers: {
    'Accept': 'application/pdf'
  },
  responseType: 'arraybuffer'
});

// Save to file
const fs = require('fs');
fs.writeFileSync('invoice.pdf', response.json);

How do I query with filters?

Use the params option for query parameters:

const response = await oauthClient.makeApiCall({
  url: `/v3/company/${realmId}/query`,
  method: 'GET',
  params: {
    query: "SELECT * FROM Customer WHERE DisplayName = 'John Doe'",
    minorversion: 59
  }
});

Troubleshooting

"Provide the Uri" error

Cause: No URI provided to createToken()

Solution:

// Wrong
await oauthClient.createToken();

// Correct
await oauthClient.createToken(req.url);

"invalid_grant" error

Common Causes:

  1. Authorization code already used (codes are single-use)
  2. Authorization code expired (10 minute limit)
  3. Redirect URI mismatch
  4. Token refresh failed (refresh token expired)

Solutions:

  • For initial authorization: Redirect user to authorize again
  • For refresh: User must re-authorize (refresh token expired)
  • Check redirect URI matches exactly

"invalid_client" error

Cause: Wrong Client ID or Client Secret

Solution:

  1. Verify credentials in Intuit Developer Portal
  2. Check for extra spaces or line breaks
  3. Ensure you're using correct environment (sandbox vs production)
  4. Regenerate keys if necessary

API calls fail after token refresh

Cause: Old token still in use

Solution: Always update stored token after refresh:

const authResponse = await oauthClient.refresh();
const newToken = authResponse.getToken();

// Update in database
await db.updateToken(userId, newToken);

// Update in client
oauthClient.setToken(newToken);

"Token invalid" or "Unauthorized" errors

Solutions:

  1. Check if token expired: oauthClient.isAccessTokenValid()
  2. Refresh the token if expired
  3. Verify realmId is correct
  4. Check token is properly loaded: oauthClient.getToken()

Rate limiting (429 errors)

QuickBooks Limits:

  • 500 requests per minute per realm
  • 5,000 requests per minute per app

Solutions:

  1. Implement request queuing
  2. Add delays between requests
  3. Use batch operations when possible
  4. Cache frequently accessed data

CORS errors in browser

Cause: OAuth flow should happen on server-side

Solution: Never store credentials in browser code:

Browser → Your Server → OAuth Client → QuickBooks API

Connection errors in production

Checklist:

  1. ✅ Using production environment?
  2. ✅ Firewall allows outbound HTTPS?
  3. ✅ Correct base URL for production?
  4. ✅ Valid SSL certificates?
  5. ✅ No proxy blocking requests?

Best Practices

Token Storage

// Good: Encrypted storage
const encryptedToken = encrypt(JSON.stringify(token));
await db.saveToken(userId, encryptedToken);

// Bad: Plain text storage
await db.saveToken(userId, token);

Error Logging

// Good: Structured logging with context
try {
  await oauthClient.makeApiCall(params);
} catch (error) {
  logger.error('QuickBooks API error', {
    error: error.error,
    intuit_tid: error.intuit_tid,
    user_id: userId,
    realm_id: realmId,
    timestamp: new Date()
  });
  // User-friendly message
  res.status(500).json({ error: 'Unable to process request' });
}

// Bad: Exposing error details to users
res.status(500).json({ error: error.error_description });

Token Refresh Strategy

// Good: Check before API call
async function makeQuickBooksCall(params) {
  if (!oauthClient.isAccessTokenValid()) {
    await oauthClient.refresh();
    // Update stored token
    await updateStoredToken();
  }
  
  return oauthClient.makeApiCall(params);
}

// Better: Automatic refresh on 401
async function makeQuickBooksCallWithRetry(params) {
  try {
    return await oauthClient.makeApiCall(params);
  } catch (error) {
    if (error.code === '401' && !retried) {
      await oauthClient.refresh();
      await updateStoredToken();
      return makeQuickBooksCallWithRetry(params, true);
    }
    throw error;
  }
}

Multi-tenant Applications

// Good: Separate client per user/company
function getOAuthClient(userId) {
  const userToken = await db.getToken(userId);
  
  return new OAuthClient({
    clientId: process.env.CLIENT_ID,
    clientSecret: process.env.CLIENT_SECRET,
    environment: process.env.ENVIRONMENT,
    redirectUri: process.env.REDIRECT_URI,
    token: userToken
  });
}

// Use it
const client = await getOAuthClient(req.user.id);
await client.makeApiCall(params);

Environment Configuration

// Good: Environment variables
require('dotenv').config();

const oauthClient = new OAuthClient({
  clientId: process.env.INTUIT_CLIENT_ID,
  clientSecret: process.env.INTUIT_CLIENT_SECRET,
  environment: process.env.INTUIT_ENVIRONMENT || 'sandbox',
  redirectUri: process.env.INTUIT_REDIRECT_URI,
  logging: process.env.NODE_ENV !== 'production'
});

Testing

// Good: Mock for testing
jest.mock('intuit-oauth');

test('handles token refresh', async () => {
  const mockRefresh = jest.fn().mockResolvedValue({
    getToken: () => ({ access_token: 'new_token' })
  });
  
  OAuthClient.prototype.refresh = mockRefresh;
  
  // Test your code
  await refreshUserToken(userId);
  
  expect(mockRefresh).toHaveBeenCalled();
});

Migration & Upgrades

Migrating from OAuth 1.0 to OAuth 2.0

The library supports OAuth 1.0 to 2.0 migration:

const migrationParams = {
  oauth_consumer_key: 'YOUR_OAUTH1_CONSUMER_KEY',
  oauth_consumer_secret: 'YOUR_OAUTH1_CONSUMER_SECRET',
  oauth_signature_method: 'HMAC-SHA1',
  oauth_timestamp: Math.round(Date.now() / 1000),
  oauth_nonce: 'nonce',
  oauth_version: '1.0',
  access_token: 'YOUR_OAUTH1_ACCESS_TOKEN',
  access_secret: 'YOUR_OAUTH1_ACCESS_SECRET',
  scope: [OAuthClient.scopes.Accounting]
};

try {
  const response = await oauthClient.migrate(migrationParams);
  const newToken = response.token;
  // Store OAuth 2.0 token
} catch (error) {
  console.error('Migration failed:', error);
}

Upgrading to v4.x

Breaking Changes:

  • Minimum Node.js version: 10
  • Updated error handling (errors now include more details)
  • Axios replaced Popsicle for HTTP requests

Migration Steps:

  1. Update Node.js to version 10 or higher
  2. Update error handling to use new error properties
  3. Test thoroughly in sandbox environment

Upgrading from 4.2.1 to 4.2.2

Issue Fixed: Authorization header bug in makeApiCall

No code changes needed - just update:

npm install intuit-oauth@latest

Advanced Topics

Custom Authorization URLs

For special cases requiring custom OAuth endpoints:

oauthClient.setAuthorizeURLs({
  authorizeEndpoint: 'https://custom.authorize.endpoint',
  tokenEndpoint: 'https://custom.token.endpoint',
  revokeEndpoint: 'https://custom.revoke.endpoint',
  userInfoEndpoint: 'https://custom.userinfo.endpoint'
});

OpenID Connect - ID Token Validation

try {
  const isValid = await oauthClient.validateIdToken();
  console.log('ID token valid:', isValid);
} catch (error) {
  console.error('ID token validation failed:', error);
}

Getting User Information

try {
  const userInfoResponse = await oauthClient.getUserInfo();
  const userInfo = userInfoResponse.json;
  
  console.log('Email:', userInfo.email);
  console.log('Name:', userInfo.givenName, userInfo.familyName);
} catch (error) {
  console.error('Failed to get user info:', error);
}

Logging Configuration

Enable detailed logging for debugging:

const oauthClient = new OAuthClient({
  clientId: 'YOUR_CLIENT_ID',
  clientSecret: 'YOUR_CLIENT_SECRET',
  environment: 'sandbox',
  redirectUri: 'http://localhost:8000/callback',
  logging: true  // Enables Winston logging
});

// Logs are saved to: ./logs/oAuthClient-log.log

Working with Minor Versions

QuickBooks API uses minor versions for incremental updates:

const response = await oauthClient.makeApiCall({
  url: `/v3/company/${realmId}/customer/${customerId}`,
  method: 'GET',
  params: {
    minorversion: 65  // Use specific API version
  }
});

Batch Operations

For bulk operations, use the batch endpoint:

const batchRequest = {
  BatchItemRequest: [
    {
      bId: 'bid1',
      operation: 'create',
      Customer: { DisplayName: 'Customer 1' }
    },
    {
      bId: 'bid2', 
      operation: 'create',
      Customer: { DisplayName: 'Customer 2' }
    }
  ]
};

const response = await oauthClient.makeApiCall({
  url: `/v3/company/${realmId}/batch`,
  method: 'POST',
  body: JSON.stringify(batchRequest)
});

Webhook Verification

Verify QuickBooks webhooks:

const crypto = require('crypto');

function verifyWebhook(payload, signature, webhookToken) {
  const hash = crypto
    .createHmac('sha256', webhookToken)
    .update(payload)
    .digest('base64');
    
  return hash === signature;
}

app.post('/webhooks', (req, res) => {
  const signature = req.headers['intuit-signature'];
  const isValid = verifyWebhook(
    JSON.stringify(req.body),
    signature,
    process.env.WEBHOOK_TOKEN
  );
  
  if (isValid) {
    // Process webhook
    console.log('Valid webhook received');
  }
  
  res.sendStatus(200);
});

Additional Resources

Official Documentation

Sample Code

Getting Help

Testing Tools


Contributing to This FAQ

Found an issue or have a question not covered here?


Last Updated: December 2025
Library Version: 4.2.2+

Clone this wiki locally