-
Notifications
You must be signed in to change notification settings - Fork 165
Home
Welcome to the NodeJS oauth-jsclient wiki!
Welcome to the Intuit OAuth Node.js Client FAQ! This guide answers common questions and provides solutions to frequently encountered issues.
- Getting Started
- Authentication & Authorization
- Token Management
- Error Handling
- Making API Calls
- Troubleshooting
- Best Practices
- Migration & Upgrades
- Advanced Topics
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.
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
npm install intuit-oauth --save- Go to Intuit Developer Portal
- Create or select your app
- Navigate to the "Keys & OAuth" tab
- Copy your Client ID and Client Secret
-
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!
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);
}
});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
]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');
}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 IDYou'll need this for all API calls to QuickBooks.
- Access Token: 1 hour (3600 seconds)
- Refresh Token: 100 days (8,726,400 seconds)
Always check token validity before making API calls!
if (oauthClient.isAccessTokenValid()) {
console.log('Token is valid');
} else {
console.log('Token expired - need to refresh');
}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
}
}Yes! This is useful when tokens are stored separately:
const authResponse = await oauthClient.refreshUsingToken('YOUR_REFRESH_TOKEN');
const newToken = authResponse.getToken();DO:
- ✅ Encrypt tokens before storing
- ✅ Store in a secure database
- ✅ Use environment variables for sensitive data
- ✅ Implement proper access controls
- ✅ Store the
realmIdwith 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
// 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);After 100 days (or 24 hours after refreshing), refresh tokens expire. When this happens:
- The refresh attempt will fail with
invalid_grant - You must redirect the user to re-authorize
- User will need to grant permissions again
try {
await oauthClient.revoke();
console.log('Tokens revoked successfully');
// Clear stored tokens
} catch (error) {
console.error('Revoke failed:', error);
}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
}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);
}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);
}
}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);
});
}
}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.
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()
});
}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);
}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.
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);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']
};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);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
}
});Cause: No URI provided to createToken()
Solution:
// Wrong
await oauthClient.createToken();
// Correct
await oauthClient.createToken(req.url);Common Causes:
- Authorization code already used (codes are single-use)
- Authorization code expired (10 minute limit)
- Redirect URI mismatch
- 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
Cause: Wrong Client ID or Client Secret
Solution:
- Verify credentials in Intuit Developer Portal
- Check for extra spaces or line breaks
- Ensure you're using correct environment (sandbox vs production)
- Regenerate keys if necessary
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);Solutions:
- Check if token expired:
oauthClient.isAccessTokenValid() - Refresh the token if expired
- Verify
realmIdis correct - Check token is properly loaded:
oauthClient.getToken()
QuickBooks Limits:
- 500 requests per minute per realm
- 5,000 requests per minute per app
Solutions:
- Implement request queuing
- Add delays between requests
- Use batch operations when possible
- Cache frequently accessed data
Cause: OAuth flow should happen on server-side
Solution: Never store credentials in browser code:
Browser → Your Server → OAuth Client → QuickBooks API
Checklist:
- ✅ Using production environment?
- ✅ Firewall allows outbound HTTPS?
- ✅ Correct base URL for production?
- ✅ Valid SSL certificates?
- ✅ No proxy blocking requests?
// Good: Encrypted storage
const encryptedToken = encrypt(JSON.stringify(token));
await db.saveToken(userId, encryptedToken);
// Bad: Plain text storage
await db.saveToken(userId, token);// 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 });// 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;
}
}// 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);// 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'
});// 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();
});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);
}Breaking Changes:
- Minimum Node.js version: 10
- Updated error handling (errors now include more details)
- Axios replaced Popsicle for HTTP requests
Migration Steps:
- Update Node.js to version 10 or higher
- Update error handling to use new error properties
- Test thoroughly in sandbox environment
Issue Fixed: Authorization header bug in makeApiCall
No code changes needed - just update:
npm install intuit-oauth@latestFor 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'
});try {
const isValid = await oauthClient.validateIdToken();
console.log('ID token valid:', isValid);
} catch (error) {
console.error('ID token validation failed:', error);
}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);
}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.logQuickBooks 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
}
});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)
});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);
});Found an issue or have a question not covered here?
Last Updated: December 2025
Library Version: 4.2.2+