
Usando la API REST de QuickBooks a través de VBA – Parte 1
Como muchos saben, he publicado un par de artículos sobre la implementación de API REST usando VBA:
- API REST REST de Outlook
- API de Google REST
- Monday.com REST API
Una vez que obtenga el concepto básico y el marco construido, se vuelve relativamente simple implementar casi cualquier API REST (¡siempre que la documentación sea buena!).
Entonces, una vez más, haciendo un trabajo de investigación para una aplicación web para un cliente, me pidieron que analizara la integración de QuickBooks a través de PHP. Una vez que lo hice, decidí mirar si se pudiera hacer lo mismo en VBA usando mi marco básico.
En primer lugar, no sé si soy solo yo (y podría ser), pero en comparación con Monday.com, Google, Xero, … por alguna razón, he encontrado que QuickBooks es más difícil de ponerse en funcionamiento inicialmente funcional. ¡Su parque infantil API apesta! Especialmente cuando lo compara con el Explorador de API Graph de Microsoft o el Playground API de Monday.com. ¡También me resultó más difícil localizar la documentación inicialmente y recurrir mucho más a las búsquedas en línea!
Dicho esto, una vez que sepa, los conceptos básicos siguen siendo los mismos y el proceso es relativamente sencillo.
Descripción general del proceso.
- Consulta ‘con su credencial para obtener un código de autorización
- Con esa consulta del código de autorización ‘para obtener un token y un token de actualización
- El uso de lo siguiente para consultar a su empresa.
- ‘Para entorno de sandboxed/de desarrollo
- ‘t.com/v3/company/’ para la producción
- El token de acceso es válido por 1 hora.
- El token de actualización es válido por 100 días, pero se restablece cada vez que regenere/actualiza el token de acceso para que sea fundamental capturarlo siempre cuando lo haga.
Para este proceso, debe haber establecido una cuenta y tener su:
Para el proceso de autenticación
- ID de cliente
- Redirigir URL
- ‘Mientras estén en desarrollo
- ‘Lo que sea que hayas definido’ cuando estén en producción
- Secreto del cliente
Para trabajar con la API REST y su empresa
- Token de acceso generado por el proceso de autenticación anterior
- ID de reino
Mi marco típico
En el pasado, para Say Google o MS, he podido usar el control del navegador web (heredado) para realizar el proceso de autenticación. Sin embargo, la URL de autenticación de QuickBooks no se presenta correctamente en el WBC y solo funciona en el control moderno del navegador web.
En este caso, quería proporcionar una solución a la audiencia más amplia posible, por lo que opté por generar la URL a través del código y presionarla a mi navegador web para obtener el código de autenticación para que luego obtenga el token para las operaciones normales, pero si desea usar fácilmente mi marco típico de API de Google/Microsoft con el MWBC para hacer la autenticación dentro de un formulario de acceso.
La estructura de la tabla
En mi configuración, elegí tener 2 tablas:
- QB_ACCOUNTINFO
Esto se utiliza para almacenar la información básica de la cuenta (ID del cliente, secreto, redirigir URI, ID de reino y código de autenticación) - QB_OAUTH2
Esto se utiliza para almacenar el acceso y actualizar los tokens
La estructura básica de las tablas es:
CREATE TABLE QB_OAuth2 ( EntryID AUTOINCREMENT PRIMARY KEY, client_id TEXT(255) NOT NULL, client_secret TEXT(255) NOT NULL, scope TEXT(255) NOT NULL, access_token LONGTEXT, refresh_token TEXT(255), token_type TEXT(255), token_creation DATETIME, expires_in INTEGER ); CREATE TABLE QB_AccountInfo ( EntryID AUTOINCREMENT PRIMARY KEY, client_id TEXT(255) NOT NULL, client_secret TEXT(255) NOT NULL, redirect_uri TEXT(255) NOT NULL, realm_id TEXT(255), auth_code Text(255) );
Mi módulo básico de QuickBooks
Ahora hay muchas formas de atacar esto, pero para este ejemplo elegí crear un módulo y construir una serie de procedimientos.
Mi módulo está compuesto por los siguientes procedimientos:
- QB_GETAUTHCODE
- QB_Token_get
- QB_Token_Refresh
- QB_Token_Revoke
- QB_Taken_ISValid
- OAUTH2_ACCOUNTINFO_LOAD
- OAUTH2_ACCOUNTINFO_AUTHCODE_CLEAR
- OAuth2_Token_Save
- Oauth2_token_load
- OAUTH2_TOKIN_CLEAR
- GenerateCacheBuster
- Código urlen
- Parsejson2
Public Const bQBDebugMode = True Public Const sAccountTable = "QB_AccountInfo" Public Const sCredentialTable = "QB_OAuth2" 'Public Const sAuthenticationForm = "QB_Authentication" Type Auth auth_code As String access_token As String client_id As String client_secret As String redirect_uri As String realm_id As String expires_in As Long refresh_token As String scope As String token_creation As Date token_type As String End Type Public OAuth2 As Auth Sub QB_GetAuthCode() Dim sClientID As String Dim sRedirectURI As String Dim sScope As String Dim sEndPoint As String Dim sAuthURL As String If OAuth2.client_id = "" Then Call OAuth2_AccountInfo_Load sClientID = OAuth2.client_id sRedirectURI = OAuth2.redirect_uri sScope = "com.intuit.quickbooks.accounting" 'Could be a Procedure Argument sEndPoint = " sAuthURL = sEndPoint & "?" & _ "client_id=" & sClientID & _ "&redirect_uri=" & sRedirectURI & _ "&response_type=code" & _ "&scope=" & sScope & _ "&state=" & "state_" & Format(Now, "yyyymmddhhmmss") & "_" & Rnd() If bQBDebugMode Then Debug.Print "QB_GetAuthCode: " & sAuthURL Application.FollowHyperlink sAuthURL 'For Access 'Shell "cmd /c start " & authURL, vbHide 'More generic MsgBox "Complete authorization in the browser, then enter the Auth Code and Realm ID in the form." DoCmd.OpenForm "QB_AccountInfo", acNormal, , , , acDialog End Sub Function QB_Token_Get() As Boolean Dim oHTTP As Object Dim sResponse As String Dim sStatus As String Dim sClientID As String Dim sClientSecret As String Dim sRedirectURI As String Dim sAuthCode As String Dim sEndPoint As String Dim sRequest As String Call OAuth2_AccountInfo_Load 'Refresh to ensure we get the AuthCode sClientID = OAuth2.client_id sClientSecret = OAuth2.client_secret sRedirectURI = OAuth2.redirect_uri sAuthCode = OAuth2.auth_code If sAuthCode = "" Then MsgBox "No Auth Code!" Exit Function End If sEndPoint = " sRequest = "grant_type=authorization_code" & _ "&code=" & sAuthCode & _ "&redirect_uri=" & sRedirectURI Set oHTTP = CreateObject("MSXML2.ServerXMLHTTP.6.0") With oHTTP .Open "POST", sEndPoint, False .setRequestHeader "Authorization", "Basic " & EncodeBase64(sClientID & ":" & sClientSecret) .setRequestHeader "Content-Type", "application/x-www-form-urlencoded" .send sRequest sResponse = .responseText sStatus = .Status End With If sStatus = 200 Then Debug.Print "New Access Token: " & ParseJson2(sResponse, "access_token") Debug.Print "New Refresh Token: " & ParseJson2(sResponse, "refresh_token") OAuth2.access_token = ParseJson2(sResponse, "access_token") OAuth2.refresh_token = ParseJson2(sResponse, "refresh_token") Call OAuth2_Token_Save QB_Token_Get = True Else Debug.Print "Token Exchange Failed: " & sResponse End If Set oHTTP = Nothing End Function Function QB_Token_Refresh() As Boolean Dim oHTTP As Object Dim sResponse As String Dim sStatus As String Dim sClientID As String Dim sClientSecret As String Dim sRedirectURI As String Dim sRefreshToken As String Dim sAuthorizationHeader As String Dim sEndPoint As String Dim sRequest As String ' Initialize values Call OAuth2_AccountInfo_Load sClientID = OAuth2.client_id sClientSecret = OAuth2.client_secret Call OAuth2_Token_Load sRefreshToken = OAuth2.refresh_token sEndPoint = " sRequest = "grant_type=refresh_token" & _ "&refresh_token=" & sRefreshToken sAuthorizationHeader = "Basic " & EncodeBase64(sClientID & ":" & sClientSecret) Set oHTTP = CreateObject("MSXML2.ServerXMLHTTP.6.0") With oHTTP .Open "POST", sEndPoint, False .setRequestHeader "Authorization", sAuthorizationHeader .setRequestHeader "Content-Type", "application/x-www-form-urlencoded" .setRequestHeader "Accept", "application/json" .send sRequest sResponse = .responseText sStatus = .Status End With If sStatus = 200 Then Debug.Print "New Access Token: " & ParseJson2(sResponse, "access_token") Debug.Print "New Refresh Token: " & ParseJson2(sResponse, "refresh_token") OAuth2.access_token = ParseJson2(sResponse, "access_token") OAuth2.refresh_token = ParseJson2(sResponse, "refresh_token") Call OAuth2_Token_Save QB_Token_Refresh = True Else Debug.Print "Error: " & sResponse End If Set oHTTP = Nothing End Function Function QB_Token_Revoke() As Boolean Dim oHTTP As Object 'New MSXML2.ServerXMLHTTP60 Dim sResponse As String Dim sStatus As String Dim sClientID As String Dim sClientSecret As String Dim sRefreshToken As String Dim sEndPoint As String Dim sAuthorizationHeader As String Dim sRequest As String Call OAuth2_AccountInfo_Load sClientID = OAuth2.client_id sClientSecret = OAuth2.client_secret Call OAuth2_Token_Load sRefreshToken = OAuth2.refresh_token sEndPoint = " sAuthorizationHeader = "Basic " & EncodeBase64(sClientID & ":" & sClientSecret) sRequest = "token=" & sRefreshToken Set oHTTP = CreateObject("MSXML2.ServerXMLHTTP.6.0") With oHTTP .Open "POST", sEndPoint, False .setRequestHeader "Authorization", sAuthorizationHeader .setRequestHeader "Content-Type", "application/x-www-form-urlencoded" .send sRequest sResponse = .responseText sStatus = .Status End With If sStatus = 200 Then Debug.Print "Token revoked successfully." 'Wipe credentials from tables! Call OAuth2_Token_Clear Call OAuth2_AccountInfo_AuthCode_Clear QB_Token_Revoke = True Else Debug.Print "Error: " & sResponse End If Set oHTTP = Nothing End Function Function QB_Token_IsValid() As Boolean Call OAuth2_Token_Load If OAuth2.token_creation = "12:00:00 AM" Then Exit Function If DateAdd("s", OAuth2.expires_in, OAuth2.token_creation) > Now Then QB_Token_IsValid = True End Function Public Function OAuth2_AccountInfo_Load() On Error GoTo Error_Handler Dim rs As DAO.Recordset Set rs = oCurrentDb.OpenRecordset("SELECT * FROM (" & sAccountTable & ")", dbOpenSnapshot) If rs.RecordCount > 0 Then With OAuth2 .client_id = Nz(rs!(client_id)) .client_secret = Nz(rs!(client_secret)) .redirect_uri = Nz(rs!(redirect_uri)) .realm_id = Nz(rs!(realm_id)) .auth_code = Nz(rs!(auth_code)) End With End If Error_Handler_Exit: On Error Resume Next rs.Close Set rs = Nothing Exit Function Error_Handler: MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _ "Error Source: OAuth2_StoredCredentials_Load" & vbCrLf & _ "Error Number: " & Err.Number & vbCrLf & _ "Error Description: " & Err.Description & _ Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _ , vbOKOnly + vbCritical, "An Error has Occurred!" Resume Error_Handler_Exit End Function Public Sub OAuth2_AccountInfo_AuthCode_Clear() On Error GoTo Error_Handler Dim rs As DAO.Recordset Set rs = oCurrentDb.OpenRecordset("SELECT * FROM (" & sAccountTable & ")") With rs If .RecordCount <> 0 Then .Edit !(auth_code) = Null .Update End If End With OAuth2.auth_code = "" Error_Handler_Exit: On Error Resume Next rs.Close Set rs = Nothing Exit Sub Error_Handler: MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _ "Error Source: OAuth2_AccountInfo_AuthCode_Clear" & vbCrLf & _ "Error Number: " & Err.Number & vbCrLf & _ "Error Description: " & Err.Description & _ Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _ , vbOKOnly + vbCritical, "An Error has Occurred!" Resume Error_Handler_Exit End Sub Public Sub OAuth2_Token_Save() On Error GoTo Error_Handler Dim rs As DAO.Recordset Set rs = oCurrentDb.OpenRecordset("SELECT * FROM (" & sCredentialTable & ")") With rs If .RecordCount = 0 Then .AddNew Else .Edit End If !(access_token) = Nz(OAuth2.access_token) 'Expires in 1 hour! !(refresh_token) = Nz(OAuth2.refresh_token) 'Expires in 100 days, but gets renewed when new Access Token is issued !(token_creation) = Now '#4/6/2025 1:22:00 PM# !(expires_in) = 3600 .Update End With Error_Handler_Exit: On Error Resume Next rs.Close Set rs = Nothing Exit Sub Error_Handler: MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _ "Error Source: OAuth2_Token_Save" & vbCrLf & _ "Error Number: " & Err.Number & vbCrLf & _ "Error Description: " & Err.Description & _ Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _ , vbOKOnly + vbCritical, "An Error has Occurred!" Resume Error_Handler_Exit End Sub Public Function OAuth2_Token_Load() On Error GoTo Error_Handler Dim rs As DAO.Recordset Set rs = oCurrentDb.OpenRecordset("SELECT * FROM (" & sCredentialTable & ")", dbOpenSnapshot) If rs.RecordCount > 0 Then With OAuth2 .access_token = Nz(rs!(access_token)) .refresh_token = Nz(rs!(refresh_token)) .token_creation = Nz(rs!(token_creation)) .expires_in = Nz(rs!(expires_in), 0) End With End If Error_Handler_Exit: On Error Resume Next rs.Close Set rs = Nothing Exit Function Error_Handler: MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _ "Error Source: OAuth2_Token_Load" & vbCrLf & _ "Error Number: " & Err.Number & vbCrLf & _ "Error Description: " & Err.Description & _ Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _ , vbOKOnly + vbCritical, "An Error has Occurred!" Resume Error_Handler_Exit End Function Public Sub OAuth2_Token_Clear() On Error GoTo Error_Handler Dim rs As DAO.Recordset Set rs = oCurrentDb.OpenRecordset("SELECT * FROM (" & sCredentialTable & ")") With rs If .RecordCount <> 0 Then .Edit !(access_token) = Null !(refresh_token) = Null !(token_creation) = Null !(expires_in) = Null ' !(scope) = Null ' !(token_type) = Null .Update End If End With ' OAuth2.client_id = "" ' OAuth2.client_secret = "" OAuth2.access_token = "" OAuth2.refresh_token = "" OAuth2.token_creation = "12:00:00 AM" OAuth2.expires_in = 0 ' OAuth2.scope = "" ' OAuth2.token_type = "" Error_Handler_Exit: On Error Resume Next rs.Close Set rs = Nothing Exit Sub Error_Handler: MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _ "Error Source: OAuth2_Token_Clear" & vbCrLf & _ "Error Number: " & Err.Number & vbCrLf & _ "Error Description: " & Err.Description & _ Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _ , vbOKOnly + vbCritical, "An Error has Occurred!" Resume Error_Handler_Exit End Sub Function GenereateCacheBuster() As String GenereateCacheBuster = "cachebuster=" & Format(Now, "yyyymmddhhmmss") & "_" & Rnd() End Function Function URLEncode(ByVal Text As String) As String Dim i As Integer Dim Char As String Dim EncodedText As String ' Loop through each character in the input text For i = 1 To Len(Text) Char = Mid(Text, i, 1) ' Check if the character is alphanumeric or one of these safe characters: - _ . ~ If Char Like "(A-Za-z0-9-_.~)" Then EncodedText = EncodedText & Char Else ' Convert unsafe characters to % followed by their ASCII value in hex EncodedText = EncodedText & "%" & Right("0" & Hex(Asc(Char)), 2) End If Next i URLEncode = EncodedText End Function Function ParseJson2(json As String, key As String) As String ' Simple JSON parser for this specific use case Dim regex As Object Set regex = CreateObject("VBScript.RegExp") regex.Pattern = """(" & key & ")"":""((^"")+)""" If regex.Test(json) Then ParseJson2 = regex.Execute(json)(0).SubMatches(1) End If End Function
Empezando
Lo primero que debe hacer es poblar la tabla QB_AccountInfo con la información de su empresa de QuickBooks:
- ID de cliente
- Secreto del cliente
Paso 1
El procedimiento QB_GETAUTHCODE () iniciará su navegador web, donde deberá autenticarse utilizando su nombre de usuario y contraseña para ingresar a QuickBooks (esta vez). Después de hacerlo, se le puede pedir que autorice el acceso a su empresa a su aplicación y finalmente será redirigido a una página que le proporcionará un código de autorización e ID de reino. Debe copiar esos valores y pegarlos en la tabla QB_ACCountInfo.
Paso 2
Ahora, después de que estemos listos para que nuestros tokens realmente comenzaran a trabajar con nuestros datos de QuickBooks. Para hacer esto, simplemente necesitamos ejecutar el procedimiento QB_Token_Get (). Recuperará los datos necesarios de la tabla QB_ACCountInfo y los pasará a QuickBooks y solicitará tokens. Si tiene éxito, los registrará en el QB_OAuth2 para todas las futuras llamadas API.
Una vez que tenga sus tokens, puede ejecutar indefinidamente siempre que use el sistema una vez cada 100 días, que el vencimiento del token de actualización. Pero siempre que lo use dentro de los 100 días, continuará actualizando el token de acceso, lo que le permite trabajar sin necesidad de autenticarse nunca más. ¡Esta es la belleza de las fichas!
JSON VS XML
Simplemente quería mencionar que en mis ejemplos trabajaré con JSON, pero puede trabajar igualmente con XML si lo prefiere. Verá, controle el tipo de respuesta que QuickBooks le proporciona cuando establece el encabezado Aceptar en su llamada API.
Solicitando JSON
.setRequestHeader "Accept", "application/json"
Solicitando XML
.setRequestHeader "Accept", "application/xml"
Estallido de caché
Como descubrí por las malas, el cliente HTTP parece almacenar información y, por lo tanto, si realiza la misma llamada, puede recibir los mismos datos que su llamada anterior, incluso si los datos cambiaban. Hay un par de enfoques que se pueden emplear para trabajar en torno a este problema.
Un enfoque es agregar una cadena única a cada solicitud, por lo que la solicitud es única y, por lo tanto, no existe un caché para esa solicitud exacta. Esto se conoce como caché. Es por eso que tengo y empleo un procedimiento llamado GenerateCacheBuster. Entonces, si ves que agrego GenerateCacheBuster () al final de ciertos puntos finales, ¡es por esa razón!
Posibles áreas de mejora
Lo primero que sugeriría a cualquiera que siga este camino es … Primero obtenga el marco básico funcional. Debería ser bastante sencillo. Una vez que esté en funcionamiento, recomendaría implementar variables de objetos de autouración (SHOV) para los objetos utilizados en todo el código (Base de datos, HTTP, …).
Si realmente desea llevar las cosas al siguiente nivel, especialmente para la Parte 2 de esta serie, sería crear un procedimiento de solicitud de envío HTTP como el que he usado anteriormente con las API de Google y Microsoft REST, ya que al hacerlo, simplificaría la codificación de procedimientos posteriores utilizados para funcionar con sus datos QuickBooks.
Además, muchos de mis procedimientos carecen de manejo de errores, esto se desarrolló rápidamente como una prueba de concepto y para compartir con usted, e idealmente debe agregarlo a cada procedimiento.
Por último, recomiendo encriptar la información que se almacena en las tablas de 2 QuickBooks para protegerlas de los ojos indiscretas. Obviamente, todavía está protegido de la autenticación inicial porque necesitan conocer su nombre de usuario y contraseña, pero si ya ha autenticado y recuperado tokens válidos, si alguien puede obtenerlos de su tabla, puede hacer cualquier cosa que deseen con sus datos.
Algunos recursos sobre el tema

Continuar leyendo

Más por venir …