using System.Net;
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
// Enable TLS 1.2 for .NET 4.5
ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; // TLS 1.2
}
}
Important Remarks:
Security Warning: TLS 1.0/1.1 are deprecated and vulnerable. Enabling TLS 1.2 is mandatory for QuickBooks API compliance and to avoid errors like The underlying connection was closed.
Upgrade Recommendation: If possible, upgrade to .NET Framework 4.6.1 or higher, where TLS 1.2 is enabled by default, to enhance security and simplify configuration.
Testing TLS: Use tools like SSLLabs to verify TLS 1.2 support or check API call logs for protocol errors. Fallback Risk: Never fall back to TLS 1.0/1.1, as QuickBooks will reject such connections, and it exposes your app to security risks.
Production Consideration: For production apps on .NET 4.5, ensure TLS 1.2 is consistently enabled across all environments, and monitor for updates to Intuit’s security requirements.
If you’re stuck with .NET 4.5 due to legacy constraints, the above code ensures compatibility, but prioritize upgrading for long-term security and compliance.
QuickBooks Developer Account Setup
You’ll need an Intuit Developer account to create a QuickBooks app and access the sandbox. We’ll cover the detailed setup next.
3. Step-by-Step QuickBooks Account and App Configuration
Proper configuration of your QuickBooks account and app is crucial for a successful integration. Let’s go through each step.
Creating an Intuit Developer Account
Sign Up:
Enter your details (name, email, password) and verify your email address.
Log in to the Intuit Developer Portal.
Explore the Dashboard:
Setting Up a Sandbox Environment
QuickBooks provides a sandbox environment for testing without affecting live data:
Access the Sandbox:
In the portal, go to Dashboard > Sandbox.
If no sandbox company exists, click Add a sandbox company.
QuickBooks will create a sandbox company with sample data (e.g., customers, invoices).
Note the Realm ID:
Each sandbox company has a unique Realm ID (Company ID), required for API calls.
Find it in the Sandbox section of the portal.
Test the Sandbox:
Creating a QuickBooks App
To integrate with QuickBooks, you need an app in the Intuit Developer Portal to obtain OAuth credentials (Client ID and Client Secret):
Create a New App:
In the My Apps Dashboard, click + Create an App.
Select QuickBooks Online and Payments as the platform.
Name your app (e.g., “MyQBOIntegration”).
Choose the scope: com.intuit.quickbooks.accounting (for invoices and purchases).
Get Development Keys:
Get Production Keys:
For production, you’ll need to complete an App Assessment Questionnaire (covered later).
Once approved, access production keys under Production > Keys & OAuth.
Configuring Redirect URIs
The redirect URI is where QuickBooks sends the authorization code after the user grants access:
Add a Redirect URI:
In the app’s Keys & OAuth section, under Redirect URIs, click Add URI.
For local testing, use https://localhost:44300/Callback.aspx.
For production, use a publicly accessible URL (e.g., https://yourdomain.com/Callback.aspx).
Local Testing with Ngrok:
QuickBooks requires HTTPS for redirect URIs. For local testing, use Ngrok to create a temporary public URL:
Copy the Ngrok URL (e.g., https://abc123.ngrok.io) and append your callback path (e.g., https://abc123.ngrok.io/Callback.aspx).
Add this to the Redirect URIs in the portal.
Save Changes:
App Assessment for Production
To use your app in production, Intuit requires an App Assessment Questionnaire:
Access the Questionnaire:
In Production > Keys & OAuth, click Go to the app assessment questionnaire.
Provide details about your app (e.g., purpose, data usage, security measures).
Compliance Requirements:
Intuit mandates HTTPS, secure token storage, and TLS 1.2 (critical for .NET 4.5).
Describe your app’s hosting environment and security practices.
Approval Process:
Submit the questionnaire and wait for review (typically 1-2 weeks).
Upon approval, you’ll receive production Client ID and Client Secret.
4. Setting Up Your ASP.NET WebForms Project
Let’s set up the ASP.NET WebForms project in .NET Framework 4.5.
Creating the Project in .NET 4.5
Open Visual Studio:
Create a new project: File > New > Project.
Select ASP.NET Web Application (.NET Framework).
Choose Web Forms, name the project (e.g., QBOIntegration), and set Framework to 4.5.
Configure HTTPS:
In Solution Explorer, right-click the project and select Properties.
Under Web, enable SSL Enabled and note the HTTPS URL (e.g., https://localhost:44300).
Enable TLS 1.2:
Installing Required NuGet Packages
Install the following NuGet packages, ensuring compatibility with .NET 4.5:
Install-Package Intuit.Ipp.OAuth2PlatformClient -Version 4.0.0
Install-Package Newtonsoft.Json -Version 9.0.1
Install-Package System.Net.Http -Version 4.3.4
Note:
Intuit.Ipp.OAuth2PlatformClient 4.0.0 is stable for .NET 4.5.
Newtonsoft.Json 9.0.1 avoids dependency conflicts.
System.Net.Http 4.3.4 ensures HTTP client compatibility.
5. Implementing OAuth 2.0 in ASP.NET WebForms
We’ll implement the OAuth 2.0 flow using two WebForms pages: Default.aspx (to initiate the flow) and Callback.aspx (to handle the redirect).
Step 1: Initiating the OAuth Flow
Create Default.aspx to redirect the user to QuickBooks’ authorization page.
Default.aspx:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="QBOIntegration.Default" %>
<!DOCTYPE html>
<html>
<head>
<title>QuickBooks Integration</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
h2 { color: #2c3e50; }
.btn { background-color: #3498db; color: white; padding: 10px 20px; border: none; cursor: pointer; }
.btn:hover { background-color: #2980b9; }
</style>
</head>
<body>
<form id="form1" runat="server">
<div>
<h2>Connect to QuickBooks</h2>
<asp:Button ID="btnConnect" runat="server" Text="Connect to QuickBooks" CssClass="btn" OnClick="btnConnect_Click" />
</div>
</form>
</body>
</html>
Default.aspx.cs:
using Intuit.Ipp.OAuth2PlatformClient;
using System;
using System.Web.UI;
namespace QBOIntegration
{
public partial class Default : Page
{
private static readonly string ClientId = "YOUR_CLIENT_ID"; // From Intuit Developer Portal
private static readonly string ClientSecret = "YOUR_CLIENT_SECRET"; // From Intuit Developer Portal
private static readonly string RedirectUri = "https://localhost:44300/Callback.aspx";
private static readonly string Environment = "sandbox"; // or "production"
protected void Page_Load(object sender, EventArgs e)
{
}
protected void btnConnect_Click(object sender, EventArgs e)
{
var oauthClient = new OAuth2Client(ClientId, ClientSecret, RedirectUri, Environment);
var scopes = new List<OidcScopes> { OidcScopes.Accounting };
var authorizationUrl = oauthClient.GetAuthorizationURL(scopes);
Response.Redirect(authorizationUrl);
}
}
}
Explanation:
The OAuth2Client is initialized with your Client ID, Client Secret, Redirect URI, and environment.
GetAuthorizationURL generates a URL with the com.intuit.quickbooks.accounting scope, redirecting the user to QuickBooks’ login page.
Basic CSS enhances the button’s appearance for a modern look.
Step 2: Handling the Callback
Create Callback.aspx to process the authorization code and Realm ID.
Callback.aspx:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Callback.aspx.cs" Inherits="QBOIntegration.Callback" %>
<!DOCTYPE html>
<html>
<head>
<title>QuickBooks Callback</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
h2 { color: #2c3e50; }
.error { color: #e74c3c; }
.success { color: #27ae60; }
</style>
</head>
<body>
<form id="form1" runat="server">
<div>
<h2>Processing QuickBooks Authorization...</h2>
<asp:Label ID="lblStatus" runat="server" />
</div>
</form>
</body>
</html>
Callback.aspx.cs:
using Intuit.Ipp.OAuth2PlatformClient;
using System;
using System.Threading.Tasks;
using System.Web.UI;
namespace QBOIntegration
{
public partial class Callback : Page
{
private static readonly string ClientId = "YOUR_CLIENT_ID";
private static readonly string ClientSecret = "YOUR_CLIENT_SECRET";
private static readonly string RedirectUri = "https://localhost:44300/Callback.aspx";
private static readonly string Environment = "sandbox";
protected async void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
string code = Request.QueryString["code"];
string realmId = Request.QueryString["realmId"];
string state = Request.QueryString["state"];
if (!string.IsNullOrEmpty(code) && !string.IsNullOrEmpty(realmId))
{
try
{
var oauthClient = new OAuth2Client(ClientId, ClientSecret, RedirectUri, Environment);
var tokenResponse = await oauthClient.RequestTokenFromCodeAsync(code, RedirectUri);
// Store tokens and Realm ID securely
Session["AccessToken"] = tokenResponse.AccessToken;
Session["RefreshToken"] = tokenResponse.RefreshToken;
Session["RealmId"] = realmId;
Session["TokenExpiry"] = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn);
lblStatus.Text = "<span class='success'>Authorization successful! You can now make API calls.</span>";
}
catch (Exception ex)
{
lblStatus.Text = $"<span class='error'>Error: {ex.Message}</span>";
}
}
else
{
lblStatus.Text = "<span class='error'>Authorization failed. Invalid code or Realm ID.</span>";
}
}
}
}
}
Explanation:
The callback URL includes code, realmId, and state as query parameters.
RequestTokenFromCodeAsync exchanges the code for access and refresh tokens.
Tokens and Realm ID are stored in the session for simplicity (use a database in production).
CSS classes (success, error) improve the UI’s visual feedback.
Step 3: Exchanging the Authorization Code for Tokens
The RequestTokenFromCodeAsync method retrieves:
Access Token: Used for API calls (expires in 1 hour).
Refresh Token: Used to obtain a new access token (expires in 100 days).
Expiry Information: Tracks token lifecycle.
Step 4: Refreshing Tokens
Access tokens expire after 1 hour, so we need to refresh them. Create TokenRefresh.aspx:
TokenRefresh.aspx.cs:
using Intuit.Ipp.OAuth2PlatformClient;
using System;
using System.Threading.Tasks;
using System.Web.UI;
namespace QBOIntegration
{
public partial class TokenRefresh : Page
{
private static readonly string ClientId = "YOUR_CLIENT_ID";
private static readonly string ClientSecret = "YOUR_CLIENT_SECRET";
private static readonly string RedirectUri = "https://localhost:44300/Callback.aspx";
private static readonly string Environment = "sandbox";
protected async void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
try
{
string refreshToken = Session["RefreshToken"]?.ToString();
if (string.IsNullOrEmpty(refreshToken))
{
Response.Write("<span style='color: #e74c3c;'>No refresh token available.</span>");
return;
}
var oauthClient = new OAuth2Client(ClientId, ClientSecret, RedirectUri, Environment);
var tokenResponse = await oauthClient.RefreshTokenAsync(refreshToken);
// Update stored tokens
Session["AccessToken"] = tokenResponse.AccessToken;
Session["RefreshToken"] = tokenResponse.RefreshToken;
Session["TokenExpiry"] = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn);
Response.Write("<span style='color: #27ae60;'>Token refreshed successfully!</span>");
}
catch (Exception ex)
{
Response.Write($"<span style='color: #e74c3c;'>Error refreshing token: {ex.Message}</span>");
}
}
}
}
}
Explanation:
RefreshTokenAsync sends the refresh token to QuickBooks to obtain a new access token.
Updated tokens and expiry are stored in the session.
Inline CSS provides visual feedback.
6. Building a QuickBooks Utility Class
To make the integration reusable and maintainable, let’s create a QuickBooksUtility class that encapsulates OAuth, token management, and API calls, optimized for .NET 4.5.
Utility Class Structure
The class includes methods for:
QuickBooksUtility.cs:
using Intuit.Ipp.OAuth2PlatformClient;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using System.Net;
namespace QBOIntegration
{
public class QuickBooksUtility
{
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string _redirectUri;
private readonly string _environment;
private readonly OAuth2Client _oauthClient;
private readonly HttpClient _httpClient;
public QuickBooksUtility(string clientId, string clientSecret, string redirectUri, string environment = "sandbox")
{
// Ensure TLS 1.2 for .NET 4.5
ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; // TLS 1.2
_clientId = clientId;
_clientSecret = clientSecret;
_redirectUri = redirectUri;
_environment = environment;
_oauthClient = new OAuth2Client(_clientId, _clientSecret, _redirectUri, _environment);
_httpClient = new HttpClient();
}
// Get authorization URL
public string GetAuthorizationUrl()
{
var scopes = new List<OidcScopes> { OidcScopes.Accounting };
return _oauthClient.GetAuthorizationURL(scopes);
}
// Exchange authorization code for tokens
public async Task<TokenResponse> ExchangeCodeForTokensAsync(string code)
{
try
{
return await _oauthClient.RequestTokenFromCodeAsync(code, _redirectUri);
}
catch (Exception ex)
{
LogError($"Failed to exchange code for tokens: {ex.Message}");
throw new Exception($"Failed to exchange code for tokens: {ex.Message}", ex);
}
}
// Refresh access token
public async Task<TokenResponse> RefreshTokenAsync(string refreshToken)
{
try
{
return await _oauthClient.RefreshTokenAsync(refreshToken);
}
catch (Exception ex)
{
LogError($"Failed to refresh token: {ex.Message}");
throw new Exception($"Failed to refresh token: {ex.Message}", ex);
}
}
// Make API call (generic)
public async Task<string> MakeApiCallAsync(string accessToken, string realmId, string endpoint, string method = "GET", string payload = null)
{
try
{
string baseUrl = _environment == "sandbox"
? "https://sandbox-quickbooks.api.intuit.com"
: "https://quickbooks.api.intuit.com";
string url = $"{baseUrl}/v3/company/{realmId}/{endpoint}";
var request = new HttpRequestMessage(new HttpMethod(method), url);
request.Headers.Add("Authorization", $"Bearer {accessToken}");
request.Headers.Add("Accept", "application/json");
if (!string.IsNullOrEmpty(payload))
{
request.Content = new StringContent(payload, System.Text.Encoding.UTF8, "application/json");
}
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException ex)
{
LogError($"API call failed: {ex.Message}");
throw new Exception($"API call failed: {ex.Message}", ex);
}
}
// Store tokens (example: in-memory, replace with database in production)
public void StoreTokens(string accessToken, string refreshToken, string realmId, long expiresIn)
{
System.Web.HttpContext.Current.Session["AccessToken"] = accessToken;
System.Web.HttpContext.Current.Session["RefreshToken"] = refreshToken;
System.Web.HttpContext.Current.Session["RealmId"] = realmId;
System.Web.HttpContext.Current.Session["TokenExpiry"] = DateTime.UtcNow.AddSeconds(expiresIn);
}
// Get stored tokens
public (string AccessToken, string RefreshToken, string RealmId, DateTime? TokenExpiry) GetStoredTokens()
{
return (
System.Web.HttpContext.Current.Session["AccessToken"]?.ToString(),
System.Web.HttpContext.Current.Session["RefreshToken"]?.ToString(),
System.Web.HttpContext.Current.Session["RealmId"]?.ToString(),
System.Web.HttpContext.Current.Session["TokenExpiry"] as DateTime?
);
}
// Log errors (example: to file, replace with proper logging in production)
private void LogError(string message)
{
System.IO.File.AppendAllText("error.log", $"{DateTime.Now}: {message}\n");
}
}
public class TokenResponse
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public long ExpiresIn { get; set; }
public long RefreshTokenExpiresIn { get; set; }
}
}
Example Usage
Update Default.aspx.cs to use the utility class:
protected void btnConnect_Click(object sender, EventArgs e)
{
var utility = new QuickBooksUtility(ClientId, ClientSecret, RedirectUri, Environment);
string authorizationUrl = utility.GetAuthorizationUrl();
Response.Redirect(authorizationUrl);
}
Update Callback.aspx.cs:
protected async void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
string code = Request.QueryString["code"];
string realmId = Request.QueryString["realmId"];
if (!string.IsNullOrEmpty(code) && !string.IsNullOrEmpty(realmId))
{
try
{
var utility = new QuickBooksUtility(ClientId, ClientSecret, RedirectUri, Environment);
var tokenResponse = await utility.ExchangeCodeForTokensAsync(code);
utility.StoreTokens(tokenResponse.AccessToken, tokenResponse.RefreshToken, realmId, tokenResponse.ExpiresIn);
lblStatus.Text = "<span class='success'>Authorization successful!</span>";
}
catch (Exception ex)
{
lblStatus.Text = $"<span class='error'>Error: {ex.Message}</span>";
}
}
else
{
lblStatus.Text = "<span class='error'>Authorization failed.</span>";
}
}
}
Explanation:
The QuickBooksUtility class is compatible with .NET 4.5, enforces TLS 1.2, and includes error logging.
It centralizes OAuth and API logic, making the code modular and reusable.
The LogError method provides basic logging (replace with a robust logging framework in production).
7. Posting Invoice and Purchase Data to QuickBooks
With OAuth in place, let’s post invoice and purchase data to QuickBooks.
Invoice Creation Example
Create CreateInvoice.aspx to post an invoice.
CreateInvoice.aspx.cs:
using Newtonsoft.Json;
using System;
using System.Threading.Tasks;
using System.Web.UI;
namespace QBOIntegration
{
public partial class CreateInvoice : Page
{
private static readonly string ClientId = "YOUR_CLIENT_ID";
private static readonly string ClientSecret = "YOUR_CLIENT_SECRET";
private static readonly string RedirectUri = "https://localhost:44300/Callback.aspx";
private static readonly string Environment = "sandbox";
protected async void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
try
{
var utility = new QuickBooksUtility(ClientId, ClientSecret, RedirectUri, Environment);
var (accessToken, refreshToken, realmId, tokenExpiry) = utility.GetStoredTokens();
if (string.IsNullOrEmpty(accessToken))
{
Response.Write("<span style='color: #e74c3c;'>No access token available. Please authorize first.</span>");
return;
}
// Refresh token if expired
if (tokenExpiry <= DateTime.UtcNow)
{
var tokenResponse = await utility.RefreshTokenAsync(refreshToken);
utility.StoreTokens(tokenResponse.AccessToken, tokenResponse.RefreshToken, realmId, tokenResponse.ExpiresIn);
accessToken = tokenResponse.AccessToken;
}
// Sample invoice payload
var invoice = new
{
Line = new[]
{
new
{
Amount = 100.00,
DetailType = "SalesItemLineDetail",
SalesItemLineDetail = new
{
ItemRef = new { value = "1" } // Replace with valid Item ID from sandbox
}
}
},
CustomerRef = new { value = "1" } // Replace with valid Customer ID from sandbox
};
string payload = JsonConvert.SerializeObject(invoice);
string response = await utility.MakeApiCallAsync(accessToken, realmId, "invoice", "POST", payload);
dynamic invoiceResponse = JsonConvert.DeserializeObject(response);
Response.Write($"<span style='color: #27ae60;'>Invoice created with ID: {invoiceResponse.Invoice.Id}</span>");
}
catch (Exception ex)
{
Response.Write($"<span style='color: #e74c3c;'>Error creating invoice: {ex.Message}</span>");
}
}
}
}
}
Explanation:
Checks token expiry and refreshes if needed.
Posts an invoice with a sample payload. Replace ItemRef and CustomerRef with valid IDs from your sandbox (find these in the sandbox UI or via the API).
Parses the response to display the invoice ID.
Purchase Data Posting Example
Create CreatePurchase.aspx to post a purchase.
CreatePurchase.aspx.cs:
using Newtonsoft.Json;
using System;
using System.Threading.Tasks;
using System.Web.UI;
namespace QBOIntegration
{
public partial class CreatePurchase : Page
{
private static readonly string ClientId = "YOUR_CLIENT_ID";
private static readonly string ClientSecret = "YOUR_CLIENT_SECRET";
private static readonly string RedirectUri = "https://localhost:44300/Callback.aspx";
private static readonly string Environment = "sandbox";
protected async void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
try
{
var utility = new QuickBooksUtility(ClientId, ClientSecret, RedirectUri, Environment);
var (accessToken, refreshToken, realmId, tokenExpiry) = utility.GetStoredTokens();
if (string.IsNullOrEmpty(accessToken))
{
Response.Write("<span style='color: #e74c3c;'>No access token available. Please authorize first.</span>");
return;
}
// Refresh token if expired
if (tokenExpiry <= DateTime.UtcNow)
{
var tokenResponse = await utility.RefreshTokenAsync(refreshToken);
utility.StoreTokens(tokenResponse.AccessToken, tokenResponse.RefreshToken, realmId, tokenResponse.ExpiresIn);
accessToken = tokenResponse.AccessToken;
}
// Sample purchase payload
var purchase = new
{
AccountRef = new { value = "33" }, // Replace with valid Account ID from sandbox
PaymentType = "Cash",
Line = new[]
{
new
{
Amount = 50.00,
DetailType = "AccountBasedExpenseLineDetail",
AccountBasedExpenseLineDetail = new
{
AccountRef = new { value = "33" } // Replace with valid Account ID from sandbox
}
}
}
};
string payload = JsonConvert.SerializeObject(purchase);
string response = await utility.MakeApiCallAsync(accessToken, realmId, "purchase", "POST", payload);
dynamic purchaseResponse = JsonConvert.DeserializeObject(response);
Response.Write($"<span style='color: #27ae60;'>Purchase created with ID: {purchaseResponse.Purchase.Id}</span>");
}
catch (Exception ex)
{
Response.Write($"<span style='color: #e74c3c;'>Error creating purchase: {ex.Message}</span>");
}
}
}
}
}
Explanation:
Similar to invoice creation, with token refresh logic.
Posts a purchase with a sample payload. Replace AccountRef with a valid ID from your sandbox.
Parses the response to display the purchase ID.
8. Exception Handling and Edge Cases
A production-ready integration requires robust error handling. Let’s cover common issues and solutions.
Common OAuth Errors
Invalid Client ID or Secret:
Invalid Redirect URI:
Cause: The redirect URI in your code doesn’t match the one in the portal.
Solution: Ensure the URI matches exactly, including HTTPS and trailing slashes.
Authorization Code Expired:
Example Handling:
public async Task<TokenResponse> ExchangeCodeForTokensAsync(string code)
{
try
{
return await _oauthClient.RequestTokenFromCodeAsync(code, _redirectUri);
}
catch (Intuit.Ipp.OAuth2PlatformClient.OAuthException ex)
{
if (ex.Message.Contains("invalid_client"))
{
LogError("Invalid Client ID or Secret.");
throw new Exception("Invalid Client ID or Secret. Check your credentials.");
}
if (ex.Message.Contains("invalid_grant"))
{
LogError("Authorization code expired or invalid.");
throw new Exception("Authorization code expired or invalid. Please re-authorize.");
}
LogError($"OAuth error: {ex.Message}");
throw new Exception($"OAuth error: {ex.Message}", ex);
}
}
Rate Limiting
QuickBooks enforces rate limits (e.g., 500 requests per minute in production). Exceeding these returns a 429 Too Many Requests error.
Solution:
public async Task<string> MakeApiCallAsync(string accessToken, string realmId, string endpoint, string method = "GET", string payload = null)
{
int retries = 0;
int maxRetries = 3;
int delay = 1000; // Start with 1 second
while (retries < maxRetries)
{
try
{
string baseUrl = _environment == "sandbox"
? "https://sandbox-quickbooks.api.intuit.com"
: "https://quickbooks.api.intuit.com";
string url = $"{baseUrl}/v3/company/{realmId}/{endpoint}";
var request = new HttpRequestMessage(new HttpMethod(method), url);
request.Headers.Add("Authorization", $"Bearer {accessToken}");
request.Headers.Add("Accept", "application/json");
if (!string.IsNullOrEmpty(payload))
{
request.Content = new StringContent(payload, System.Text.Encoding.UTF8, "application/json");
}
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
catch (HttpRequestException ex) when ((int?)ex.StatusCode == 429)
{
retries++;
LogError($"Rate limit hit. Retrying after {delay}ms (attempt {retries}/{maxRetries})");
await Task.Delay(delay);
delay *= 2; // Exponential backoff
}
catch (HttpRequestException ex)
{
LogError($"API call failed: {ex.Message}");
throw new Exception($"API call failed: {ex.Message}", ex);
}
}
LogError("Rate limit exceeded after retries.");
throw new Exception("Rate limit exceeded after retries.");
}
Token Expiry and Refresh Failures
Handle access token expiry (1 hour) and refresh token expiry (100 days):
Solution:
public async Task<string> MakeApiCallAsync(string accessToken, string realmId, string endpoint, string method = "GET", string payload = null)
{
var (currentAccessToken, refreshToken, _, tokenExpiry) = GetStoredTokens();
if (tokenExpiry <= DateTime.UtcNow)
{
LogError("Access token expired. Attempting to refresh.");
var tokenResponse = await RefreshTokenAsync(refreshToken);
accessToken = tokenResponse.AccessToken;
StoreTokens(tokenResponse.AccessToken, tokenResponse.RefreshToken, realmId, tokenResponse.ExpiresIn);
}
// Proceed with API call
// ... (rest of the method as above)
}
Handle refresh token expiry:
public async Task<TokenResponse> RefreshTokenAsync(string refreshToken)
{
try
{
return await _oauthClient.RefreshTokenAsync(refreshToken);
}
catch (Intuit.Ipp.OAuth2PlatformClient.OAuthException ex)
{
if (ex.Message.Contains("invalid_grant"))
{
LogError("Refresh token expired.");
throw new Exception("Refresh token expired. Please re-authorize the application.");
}
LogError($"Failed to refresh token: {ex.Message}");
throw new Exception($"Failed to refresh token: {ex.Message}", ex);
}
}
Sandbox vs. Production Differences
API Base URL:
Data: Sandbox uses sample data; production uses live data.
Rate Limits: Sandbox has stricter limits (e.g., 40 emails/day).
Solution:
9. Advanced Scenarios
Let’s explore advanced scenarios to make your integration robust and scalable.
Multi-Tenant Support
If your app serves multiple QuickBooks companies (e.g., for different clients), you need to manage tokens and Realm IDs per tenant.
Solution:
public class QuickBooksToken
{
public int TenantId { get; set; }
public string RealmId { get; set; }
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public DateTime AccessTokenExpiry { get; set; }
public DateTime RefreshTokenExpiry { get; set; }
}
Modify StoreTokens and GetStoredTokens:
public void StoreTokens(string accessToken, string refreshToken, string realmId, long expiresIn, int tenantId)
{
using (var db = new YourDbContext())
{
var token = db.QuickBooksTokens.FirstOrDefault(t => t.TenantId == tenantId && t.RealmId == realmId);
if (token == null)
{
token = new QuickBooksToken
{
TenantId = tenantId,
RealmId = realmId
};
db.QuickBooksTokens.Add(token);
}
token.AccessToken = accessToken;
token.RefreshToken = refreshToken;
token.AccessTokenExpiry = DateTime.UtcNow.AddSeconds(expiresIn);
token.RefreshTokenExpiry = DateTime.UtcNow.AddDays(100);
db.SaveChanges();
LogError($"Stored tokens for TenantId: {tenantId}, RealmId: {realmId}");
}
}
public QuickBooksToken GetStoredTokens(int tenantId, string realmId)
{
using (var db = new YourDbContext())
{
var token = db.QuickBooksTokens.FirstOrDefault(t => t.TenantId == tenantId && t.RealmId == realmId);
if (token == null)
{
LogError($"No tokens found for TenantId: {tenantId}, RealmId: {realmId}");
}
return token;
}
}
Update MakeApiCallAsync to use tenant-specific tokens:
public async Task<string> MakeApiCallAsync(int tenantId, string realmId, string endpoint, string method = "GET", string payload = null)
{
var token = GetStoredTokens(tenantId, realmId);
if (token == null || string.IsNullOrEmpty(token.AccessToken))
{
throw new Exception("No valid access token available.");
}
if (token.AccessTokenExpiry <= DateTime.UtcNow)
{
var tokenResponse = await RefreshTokenAsync(token.RefreshToken);
StoreTokens(tokenResponse.AccessToken, tokenResponse.RefreshToken, realmId, tokenResponse.ExpiresIn, tenantId);
token.AccessToken = tokenResponse.AccessToken;
}
return await MakeApiCallAsync(token.AccessToken, realmId, endpoint, method, payload);
}
Webhook Integration
QuickBooks supports webhooks for real-time notifications (e.g., when an invoice is created).
Configure Webhooks:
In the Intuit Developer Portal, go to your app’s Webhooks section.
Specify a publicly accessible endpoint (e.g., https://yourdomain.com/Webhook.aspx).
Select events (e.g., Invoice.Created, Purchase.Created).
Create a Webhook Handler:
Webhook.aspx.cs:
using Newtonsoft.Json;
using System;
using System.IO;
using System.Web.UI;
namespace QBOIntegration
{
public partial class Webhook : Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (Request.HttpMethod == "POST")
{
try
{
using (var reader = new StreamReader(Request.InputStream))
{
string payload = reader.ReadToEnd();
var webhookData = JsonConvert.DeserializeObject<dynamic>(payload);
// Process webhook data
foreach (var eventNotification in webhookData.eventNotifications)
{
string realmId = eventNotification.realmId;
foreach (var dataChangeEvent in eventNotification.dataChangeEvent.entities)
{
string entityName = dataChangeEvent.name;
string entityId = dataChangeEvent.id;
string operation = dataChangeEvent.operation;
// Log or process the event
LogWebhookEvent(realmId, entityName, entityId, operation);
}
}
Response.StatusCode = 200;
}
}
catch (Exception ex)
{
Response.StatusCode = 500;
Response.Write($"<span style='color: #e74c3c;'>Error processing webhook: {ex.Message}</span>");
}
}
}
private void LogWebhookEvent(string realmId, string entityName, string entityId, string operation)
{
// Implement logging (e.g., to database or file)
string logMessage = $"Webhook received: RealmId={realmId}, Entity={entityName}, Id={entityId}, Operation={operation}";
System.IO.File.AppendAllText("webhook.log", $"{DateTime.Now}: {logMessage}\n");
}
}
}
Secure the Webhook:
Verify the webhook signature using Intuit’s provided hash (see Intuit’s documentation).
Use HTTPS for the endpoint.
Example signature verification:
private bool VerifyWebhookSignature(string payload, string intuitSignature)
{
string verifierToken = "YOUR_WEBHOOK_VERIFIER_TOKEN"; // From Intuit Developer Portal
string computedSignature = Convert.ToBase64String(
System.Security.Cryptography.HMACSHA256.Create()
.ComputeHash(System.Text.Encoding.UTF8.GetBytes(payload + verifierToken)));
return computedSignature == intuitSignature;
}
Handling Regional Variations
QuickBooks supports multiple regions (e.g., US, UK, Canada, France), each with unique requirements (e.g., France requires a journal code for journal entries).
Solution:
public async Task<string> GetCompanyInfoAsync(string accessToken, string realmId)
{
try
{
string response = await MakeApiCallAsync(accessToken, realmId, "companyinfo/" + realmId);
dynamic companyInfo = JsonConvert.DeserializeObject(response);
return companyInfo.CompanyInfo.Country;
}
catch (Exception ex)
{
LogError($"Failed to get company info: {ex.Message}");
throw;
}
}
Adjust payloads based on the region:
public async Task<string> CreateJournalEntryAsync(string accessToken, string realmId, string country)
{
var journalEntry = new
{
Line = new[]
{
new
{
Amount = 100.00,
DetailType = "JournalEntryLineDetail",
JournalEntryLineDetail = new
{
PostingType = "Debit",
AccountRef = new { value = "1" } // Replace with valid Account ID
}
}
}
};
if (country == "FR")
{
journalEntry = new
{
Line = new[]
{
new
{
Amount = 100.00,
DetailType = "JournalEntryLineDetail",
JournalEntryLineDetail = new
{
PostingType = "Debit",
AccountRef = new { value = "1" },
JournalCode = "JRNL001" // Required for France
}
}
}
};
}
string payload = JsonConvert.SerializeObject(journalEntry);
return await MakeApiCallAsync(accessToken, realmId, "journalentry", "POST", payload);
}
Custom Redirect URI Logic
For dynamic redirect URIs (e.g., per tenant), generate URIs programmatically:
public string GetAuthorizationUrl(int tenantId)
{
string dynamicRedirectUri = $"https://yourdomain.com/Callback.aspx?tenantId={tenantId}";
var oauthClient = new OAuth2Client(_clientId, _clientSecret, dynamicRedirectUri, _environment);
var scopes = new List<OidcScopes> { OidcScopes.Accounting };
return oauthClient.GetAuthorizationURL(scopes);
}
Ensure all possible URIs are registered in the Intuit Developer Portal.
Example: Register https://yourdomain.com/Callback.aspx?tenantId=* as a wildcard if supported, or list specific tenant IDs.
10. Testing and Debugging
Testing in the Sandbox
Authorize the App:
Test API Calls:
Use CreateInvoice.aspx and CreatePurchase.aspx to post data.
Verify the data in the sandbox company’s UI (e.g., check the Invoices or Expenses section).
Use Postman:
Debugging OAuth Issues
Check Logs:
Common Issues:
401 Unauthorized: Invalid or expired access token. Refresh the token.
400 Bad Request: Incorrect payload format or missing required fields. Validate against QuickBooks API documentation.
403 Forbidden: Missing scope or insufficient permissions. Ensure the com.intuit.quickbooks.accounting scope is included.
TLS Errors: If you see The underlying connection was closed, verify TLS 1.2 is enabled.
Use Browser Developer Tools:
Open the browser’s Network tab to inspect the redirect URL and verify query parameters (code, realmId, state).
Check for CORS or SSL issues if using Ngrok.
Verifying API Responses
Parse API responses to confirm successful data posting:
string response = await utility.MakeApiCallAsync(accessToken, realmId, "invoice", "POST", payload);
dynamic invoice = JsonConvert.DeserializeObject(response);
string invoiceId = invoice.Invoice.Id;
Response.Write($"<span style='color: #27ae60;'>Invoice created with ID: {invoiceId}</span>");
Log the full response for debugging:
LogError($"API Response: {response}");
11. Best Practices and Security Considerations
Storing Tokens Securely
Avoid Session Storage: In production, store tokens in a secure database with encryption.
Use Encryption: Encrypt tokens using AES or a similar algorithm.
Access Control: Restrict database access to authorized users.
Example (using Entity Framework):
public void StoreTokensSecurely(string accessToken, string refreshToken, string realmId, long expiresIn, int tenantId)
{
using (var db = new YourDbContext())
{
var token = new QuickBooksToken
{
TenantId = tenantId,
RealmId = realmId,
AccessToken = Encrypt(accessToken),
RefreshToken = Encrypt(refreshToken),
AccessTokenExpiry = DateTime.UtcNow.AddSeconds(expiresIn),
RefreshTokenExpiry = DateTime.UtcNow.AddDays(100)
};
db.QuickBooksTokens.Add(token);
db.SaveChanges();
LogError($"Stored encrypted tokens for TenantId: {tenantId}, RealmId: {realmId}");
}
}
private string Encrypt(string data)
{
// Implement AES encryption
// Example placeholder (use a proper encryption library)
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(data));
}
enforced in QuickBooksUtility).
App Assessment: Complete the questionnaire for production use, detailing your security measures.
TLS Security Note for .NET 4.5
As emphasized in the Prerequisites, .NET 4.5 requires explicit TLS 1.2 enablement due to its default use of outdated TLS 1.0/1.1 protocols. This is a critical consideration for legacy applications:
Why It Matters: QuickBooks rejects connections using TLS 1.0/1.1, and these protocols are vulnerable to attacks like POODLE and BEAST.
Production Risk: Running .NET 4.5 in production without TLS 1.2 risks connection failures and security breaches.
Upgrade Path: Migrating to .NET Framework 4.6.1 or higher eliminates the need for manual TLS configuration and aligns with modern security standards.
Monitoring: Regularly check Intuit’s API documentation for updates to TLS requirements, as they may mandate newer protocols (e.g., TLS 1.3) in the future.
If you must use .NET 4.5, the ServicePointManager.SecurityProtocol setting in Global.asax.cs and QuickBooksUtility ensures compatibility, but treat this as a temporary solution.
Next Steps
Stay Tuned for Part 3: Our next post will dive into advanced QuickBooks API features, performance optimization, and scaling your integration.
Invoice Data Posting Details.
No comments:
Post a Comment
Thanks for your valuable comment...........
Md. Mominul Islam